[DRE-commits] [ruby-diaspora-vines] 01/08: Imported Upstream version 0.2.0.develop.4
Praveen Arimbrathodiyil
praveen at moszumanska.debian.org
Tue Jun 7 08:58:15 UTC 2016
This is an automated email from the git hooks/post-receive script.
praveen pushed a commit to branch master
in repository ruby-diaspora-vines.
commit 42da6ee05763349ddd95dc2c628addff5efa69e7
Author: sudheesh <sudheeshshetty at gmail.com>
Date: Wed Feb 3 08:55:58 2016 +0530
Imported Upstream version 0.2.0.develop.4
---
Gemfile | 5 +
LICENSE | 19 +
README.md | 10 +
Rakefile | 23 +
bin/vines | 4 +
checksums.yaml.gz | Bin 0 -> 269 bytes
conf/certs/README | 39 +
conf/certs/ca-bundle.crt | 3895 ++++++++++++++++++++
conf/config.rb | 50 +
lib/vines.rb | 216 ++
lib/vines/cli.rb | 103 +
lib/vines/cluster.rb | 246 ++
lib/vines/cluster/connection.rb | 26 +
lib/vines/cluster/publisher.rb | 55 +
lib/vines/cluster/pubsub.rb | 92 +
lib/vines/cluster/sessions.rb | 125 +
lib/vines/cluster/subscriber.rb | 133 +
lib/vines/command/cert.rb | 50 +
lib/vines/command/restart.rb | 12 +
lib/vines/command/start.rb | 28 +
lib/vines/command/stop.rb | 18 +
lib/vines/config.rb | 236 ++
lib/vines/config/diaspora.rb | 37 +
lib/vines/config/host.rb | 137 +
lib/vines/config/port.rb | 132 +
lib/vines/config/pubsub.rb | 108 +
lib/vines/contact.rb | 115 +
lib/vines/daemon.rb | 78 +
lib/vines/error.rb | 150 +
lib/vines/jid.rb | 95 +
lib/vines/kit.rb | 30 +
lib/vines/log.rb | 28 +
lib/vines/node.rb | 31 +
lib/vines/router.rb | 184 +
lib/vines/stanza.rb | 175 +
lib/vines/stanza/dialback.rb | 28 +
lib/vines/stanza/iq.rb | 48 +
lib/vines/stanza/iq/auth.rb | 18 +
lib/vines/stanza/iq/disco_info.rb | 45 +
lib/vines/stanza/iq/disco_items.rb | 29 +
lib/vines/stanza/iq/error.rb | 16 +
lib/vines/stanza/iq/ping.rb | 16 +
lib/vines/stanza/iq/private_storage.rb | 83 +
lib/vines/stanza/iq/query.rb | 10 +
lib/vines/stanza/iq/result.rb | 16 +
lib/vines/stanza/iq/roster.rb | 140 +
lib/vines/stanza/iq/session.rb | 17 +
lib/vines/stanza/iq/vcard.rb | 56 +
lib/vines/stanza/iq/version.rb | 25 +
lib/vines/stanza/message.rb | 43 +
lib/vines/stanza/presence.rb | 182 +
lib/vines/stanza/presence/error.rb | 23 +
lib/vines/stanza/presence/probe.rb | 37 +
lib/vines/stanza/presence/subscribe.rb | 42 +
lib/vines/stanza/presence/subscribed.rb | 51 +
lib/vines/stanza/presence/unavailable.rb | 15 +
lib/vines/stanza/presence/unsubscribe.rb | 38 +
lib/vines/stanza/presence/unsubscribed.rb | 38 +
lib/vines/stanza/pubsub.rb | 22 +
lib/vines/stanza/pubsub/create.rb | 39 +
lib/vines/stanza/pubsub/delete.rb | 41 +
lib/vines/stanza/pubsub/publish.rb | 66 +
lib/vines/stanza/pubsub/subscribe.rb | 44 +
lib/vines/stanza/pubsub/unsubscribe.rb | 30 +
lib/vines/storage.rb | 291 ++
lib/vines/storage/local.rb | 151 +
lib/vines/storage/null.rb | 51 +
lib/vines/storage/sql.rb | 346 ++
lib/vines/store.rb | 152 +
lib/vines/stream.rb | 309 ++
lib/vines/stream/client.rb | 88 +
lib/vines/stream/client/auth.rb | 74 +
lib/vines/stream/client/auth_restart.rb | 29 +
lib/vines/stream/client/bind.rb | 64 +
lib/vines/stream/client/bind_restart.rb | 30 +
lib/vines/stream/client/closed.rb | 13 +
lib/vines/stream/client/ready.rb | 17 +
lib/vines/stream/client/session.rb | 210 ++
lib/vines/stream/client/start.rb | 27 +
lib/vines/stream/client/tls.rb | 38 +
lib/vines/stream/component.rb | 58 +
lib/vines/stream/component/handshake.rb | 26 +
lib/vines/stream/component/ready.rb | 23 +
lib/vines/stream/component/start.rb | 19 +
lib/vines/stream/http.rb | 185 +
lib/vines/stream/http/auth.rb | 22 +
lib/vines/stream/http/bind.rb | 32 +
lib/vines/stream/http/bind_restart.rb | 37 +
lib/vines/stream/http/ready.rb | 29 +
lib/vines/stream/http/request.rb | 193 +
lib/vines/stream/http/session.rb | 128 +
lib/vines/stream/http/sessions.rb | 65 +
lib/vines/stream/http/start.rb | 23 +
lib/vines/stream/parser.rb | 79 +
lib/vines/stream/sasl.rb | 128 +
lib/vines/stream/server.rb | 207 ++
lib/vines/stream/server/auth.rb | 30 +
lib/vines/stream/server/auth_method.rb | 66 +
lib/vines/stream/server/auth_restart.rb | 39 +
lib/vines/stream/server/final_restart.rb | 21 +
lib/vines/stream/server/outbound/auth.rb | 65 +
.../stream/server/outbound/auth_dialback_result.rb | 39 +
lib/vines/stream/server/outbound/auth_external.rb | 33 +
.../stream/server/outbound/auth_external_result.rb | 32 +
lib/vines/stream/server/outbound/auth_restart.rb | 27 +
lib/vines/stream/server/outbound/authoritative.rb | 48 +
lib/vines/stream/server/outbound/final_features.rb | 28 +
lib/vines/stream/server/outbound/final_restart.rb | 20 +
lib/vines/stream/server/outbound/start.rb | 20 +
lib/vines/stream/server/outbound/tls_result.rb | 34 +
lib/vines/stream/server/ready.rb | 24 +
lib/vines/stream/server/start.rb | 40 +
lib/vines/stream/state.rb | 46 +
lib/vines/token_bucket.rb | 55 +
lib/vines/user.rb | 125 +
lib/vines/version.rb | 6 +
lib/vines/xmpp_server.rb | 25 +
metadata.yml | 456 +++
test/cluster/publisher_test.rb | 57 +
test/cluster/sessions_test.rb | 47 +
test/cluster/subscriber_test.rb | 111 +
test/config/host_test.rb | 358 ++
test/config/pubsub_test.rb | 187 +
test/config_test.rb | 753 ++++
test/contact_test.rb | 102 +
test/error_test.rb | 58 +
test/ext/nokogiri.rb | 14 +
test/jid_test.rb | 147 +
test/kit_test.rb | 31 +
test/router_test.rb | 243 ++
test/stanza/iq/disco_info_test.rb | 80 +
test/stanza/iq/disco_items_test.rb | 49 +
test/stanza/iq/private_storage_test.rb | 184 +
test/stanza/iq/roster_test.rb | 229 ++
test/stanza/iq/session_test.rb | 25 +
test/stanza/iq/vcard_test.rb | 146 +
test/stanza/iq/version_test.rb | 64 +
test/stanza/iq_test.rb | 70 +
test/stanza/message_test.rb | 127 +
test/stanza/presence/probe_test.rb | 50 +
test/stanza/presence/subscribe_test.rb | 83 +
test/stanza/pubsub/create_test.rb | 116 +
test/stanza/pubsub/delete_test.rb | 169 +
test/stanza/pubsub/publish_test.rb | 309 ++
test/stanza/pubsub/subscribe_test.rb | 205 ++
test/stanza/pubsub/unsubscribe_test.rb | 148 +
test/stanza_test.rb | 85 +
test/storage/local_test.rb | 59 +
test/storage/mock_redis.rb | 97 +
test/storage/null_test.rb | 29 +
test/storage/sql_schema.rb | 186 +
test/storage/sql_test.rb | 290 ++
test/storage/storage_tests.rb | 182 +
test/store_test.rb | 164 +
test/stream/client/auth_test.rb | 137 +
test/stream/client/ready_test.rb | 47 +
test/stream/client/session_test.rb | 27 +
test/stream/component/handshake_test.rb | 52 +
test/stream/component/ready_test.rb | 103 +
test/stream/component/start_test.rb | 39 +
test/stream/http/auth_test.rb | 70 +
test/stream/http/ready_test.rb | 86 +
test/stream/http/request_test.rb | 194 +
test/stream/http/sessions_test.rb | 49 +
test/stream/http/start_test.rb | 50 +
test/stream/parser_test.rb | 122 +
test/stream/sasl_test.rb | 195 +
test/stream/server/auth_method_test.rb | 124 +
test/stream/server/auth_test.rb | 70 +
.../server/outbound/auth_dialback_result_test.rb | 52 +
test/stream/server/outbound/auth_external_test.rb | 105 +
test/stream/server/outbound/auth_restart_test.rb | 77 +
test/stream/server/outbound/auth_test.rb | 113 +
test/stream/server/outbound/authoritative_test.rb | 86 +
test/stream/server/outbound/start_test.rb | 45 +
test/stream/server/ready_test.rb | 122 +
test/stream/server/start_test.rb | 105 +
test/test_helper.rb | 51 +
test/token_bucket_test.rb | 44 +
test/user_test.rb | 101 +
180 files changed, 19886 insertions(+)
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..e88e5df
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,5 @@
+source "https://rubygems.org"
+
+gemspec
+
+gem 'pronto', :git => 'https://github.com/Zauberstuhl/pronto.git'
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..778f659
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2010-2014 Negative Code
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3f4b086
--- /dev/null
+++ b/README.md
@@ -0,0 +1,10 @@
+Diaspora XMPP Integration
+=========================
+
+**master** [  ](https://travis-ci.org/diaspora/vines)
+**develop** [  ](https://travis-ci.org/diaspora/vines)
+
+This XMPP server was forked from [Negativecode](http://www.getvines.org/)
+and was slimmed down for [Diaspora](https://diasporafoundation.org) usage only!
+
+**DO NOT** use this vines version unless you know what you're doing!!
diff --git a/Rakefile b/Rakefile
new file mode 100644
index 0000000..2b549f9
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,23 @@
+# encoding: UTF-8
+
+require 'rake'
+require 'rake/clean'
+require 'rake/testtask'
+
+CLOBBER.include('pkg')
+
+directory 'pkg'
+
+desc 'Build distributable packages'
+task :build => [:pkg] do
+ system 'gem build vines.gemspec && mv vines-*.gem pkg/'
+end
+
+Rake::TestTask.new(:test) do |test|
+ test.libs << 'test'
+ test.libs << 'test/storage'
+ test.pattern = 'test/**/*_test.rb'
+ test.warning = false
+end
+
+task :default => [:clobber, :test, :build]
diff --git a/bin/vines b/bin/vines
new file mode 100755
index 0000000..dd89747
--- /dev/null
+++ b/bin/vines
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+
+require 'vines'
+Vines::CLI.start
diff --git a/checksums.yaml.gz b/checksums.yaml.gz
new file mode 100644
index 0000000..a6e1b66
Binary files /dev/null and b/checksums.yaml.gz differ
diff --git a/conf/certs/README b/conf/certs/README
new file mode 100644
index 0000000..07dda20
--- /dev/null
+++ b/conf/certs/README
@@ -0,0 +1,39 @@
+The certs/ directory contains the TLS certificates required for encrypting
+client to server and server to server XMPP connections. TLS encryption
+is mandatory for these streams so this directory must be configured properly.
+
+The ca-bundle.crt file contains root Certificate Authority (CA) certificates.
+These are used to validate certificates presented during TLS handshake
+negotiation. The source for this file is the cacert.pem file available
+at http://curl.haxx.se/docs/caextract.html.
+
+Any self-signed CA certificate placed in this directory will be considered
+a trusted certificate. For example, let's say you're running the wonderland.lit
+XMPP server and would like to allow verona.lit to connect a server to server
+stream. The verona.lit server hasn't purchased a legitimate TLS certificate
+from a CA known in ca-bundle.crt. Instead, they've created a self-signed
+certificate and sent it to you. Place the certificate in this directory
+with a name of verona.lit.crt and it will be trusted. TLS connections from
+verona.lit will now work.
+
+For TLS connections to work for a virtual host, two files are needed in this
+directory: <vhost_domain>.key and <vhost_domain>.crt. The key file must contain
+a PEM encoded private key. Do not give this file to anyone. This is your
+private key so keep it private. The crt file must contain a PEM encoded TLS
+certificate. This contains your public key so feel free to share it with other
+servers.
+
+For example, when you add a new virtual host named wonderland.lit to
+the XMPP server, you need to run the 'vines cert wonderland.lit' command to
+generate the private key file and the self-signed certificate file in this
+directory. After running that command you will have a certs/wonderland.lit.key
+and a certs/wonderland.lit.crt file.
+
+Alternatively, you can purchase a TLS certificate from a CA (e.g. RapidSSL,
+VeriSign, etc.) and place it in this directory. This will avoid the hassles
+of managing self-signed certificates.
+
+Certificates for wildcard domains, like *.wonderland.lit, can be placed in this
+directory with a name of wonderland.lit.crt with a matching wonderland.lit.key
+file. The wildcard files will be used to secure connections to any subdomain
+under wonderland.lit (tea.wonderland.lit, party.wonderland.lit, etc).
diff --git a/conf/certs/ca-bundle.crt b/conf/certs/ca-bundle.crt
new file mode 100644
index 0000000..99b310b
--- /dev/null
+++ b/conf/certs/ca-bundle.crt
@@ -0,0 +1,3895 @@
+##
+## ca-bundle.crt -- Bundle of CA Root Certificates
+##
+## Certificate data from Mozilla as of: Sat Dec 29 20:03:40 2012
+##
+## This is a bundle of X.509 certificates of public Certificate Authorities
+## (CA). These were automatically extracted from Mozilla's root certificates
+## file (certdata.txt). This file can be found in the mozilla source tree:
+## http://mxr.mozilla.org/mozilla/source/security/nss/lib/ckfw/builtins/certdata.txt?raw=1
+##
+## It contains the certificates in PEM format and therefore
+## can be directly used with curl / libcurl / php_curl, or with
+## an Apache+mod_ssl webserver for SSL client authentication.
+## Just configure this file as the SSLCACertificateFile.
+##
+
+# @(#) $RCSfile: certdata.txt,v $ $Revision: 1.87 $ $Date: 2012/12/29 16:32:45 $
+
+GTE CyberTrust Global Root
+==========================
+-----BEGIN CERTIFICATE-----
+MIICWjCCAcMCAgGlMA0GCSqGSIb3DQEBBAUAMHUxCzAJBgNVBAYTAlVTMRgwFgYDVQQKEw9HVEUg
+Q29ycG9yYXRpb24xJzAlBgNVBAsTHkdURSBDeWJlclRydXN0IFNvbHV0aW9ucywgSW5jLjEjMCEG
+A1UEAxMaR1RFIEN5YmVyVHJ1c3QgR2xvYmFsIFJvb3QwHhcNOTgwODEzMDAyOTAwWhcNMTgwODEz
+MjM1OTAwWjB1MQswCQYDVQQGEwJVUzEYMBYGA1UEChMPR1RFIENvcnBvcmF0aW9uMScwJQYDVQQL
+Ex5HVEUgQ3liZXJUcnVzdCBTb2x1dGlvbnMsIEluYy4xIzAhBgNVBAMTGkdURSBDeWJlclRydXN0
+IEdsb2JhbCBSb290MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVD6C28FCc6HrHiM3dFw4u
+sJTQGz0O9pTAipTHBsiQl8i4ZBp6fmw8U+E3KHNgf7KXUwefU/ltWJTSr41tiGeA5u2ylc9yMcql
+HHK6XALnZELn+aks1joNrI1CqiQBOeacPwGFVw1Yh0X404Wqk2kmhXBIgD8SFcd5tB8FLztimQID
+AQABMA0GCSqGSIb3DQEBBAUAA4GBAG3rGwnpXtlR22ciYaQqPEh346B8pt5zohQDhT37qw4wxYMW
+M4ETCJ57NE7fQMh017l93PR2VX2bY1QY6fDq81yx2YtCHrnAlU66+tXifPVoYb+O7AWXX1uw16OF
+NMQkpw0PlZPvy5TYnh+dXIVtx6quTx8itc2VrbqnzPmrC3p/
+-----END CERTIFICATE-----
+
+Thawte Server CA
+================
+-----BEGIN CERTIFICATE-----
+MIIDEzCCAnygAwIBAgIBATANBgkqhkiG9w0BAQQFADCBxDELMAkGA1UEBhMCWkExFTATBgNVBAgT
+DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3dGUgQ29uc3Vs
+dGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcGA1UE
+AxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5j
+b20wHhcNOTYwODAxMDAwMDAwWhcNMjAxMjMxMjM1OTU5WjCBxDELMAkGA1UEBhMCWkExFTATBgNV
+BAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3dGUgQ29u
+c3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcG
+A1UEAxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0
+ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANOkUG7I/1Zr5s9dtuoMaHVHoqrC2oQl
+/Kj0R1HahbUgdJSGHg91yekIYfUGbTBuFRkC6VLAYttNmZ7iagxEOM3+vuNkCXDF/rFrKbYvScg7
+1CcEJRCXL+eQbcAoQpnXTEPew/UhbVSfXcNY4cDk2VuwuNy0e982OsK1ZiIS1ocNAgMBAAGjEzAR
+MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAB/pMaVz7lcxG7oWDTSEwjsrZqG9J
+GubaUeNgcGyEYRGhGshIPllDfU+VPaGLtwtimHp1it2ITk6eQNuozDJ0uW8NxuOzRAvZim+aKZuZ
+GCg70eNAKJpaPNW15yAbi8qkq43pUdniTCxZqdq5snUb9kLy78fyGPmJvKP/iiMucEc=
+-----END CERTIFICATE-----
+
+Thawte Premium Server CA
+========================
+-----BEGIN CERTIFICATE-----
+MIIDJzCCApCgAwIBAgIBATANBgkqhkiG9w0BAQQFADCBzjELMAkGA1UEBhMCWkExFTATBgNVBAgT
+DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3dGUgQ29uc3Vs
+dGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UE
+AxMYVGhhd3RlIFByZW1pdW0gU2VydmVyIENBMSgwJgYJKoZIhvcNAQkBFhlwcmVtaXVtLXNlcnZl
+ckB0aGF3dGUuY29tMB4XDTk2MDgwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgc4xCzAJBgNVBAYT
+AlpBMRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEdMBsGA1UEChMU
+VGhhd3RlIENvbnN1bHRpbmcgY2MxKDAmBgNVBAsTH0NlcnRpZmljYXRpb24gU2VydmljZXMgRGl2
+aXNpb24xITAfBgNVBAMTGFRoYXd0ZSBQcmVtaXVtIFNlcnZlciBDQTEoMCYGCSqGSIb3DQEJARYZ
+cHJlbWl1bS1zZXJ2ZXJAdGhhd3RlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0jY2
+aovXwlue2oFBYo847kkEVdbQ7xwblRZH7xhINTpS9CtqBo87L+pW46+GjZ4X9560ZXUCTe/LCaIh
+Udib0GfQug2SBhRz1JPLlyoAnFxODLz6FVL88kRu2hFKbgifLy3j+ao6hnO2RlNYyIkFvYMRuHM/
+qgeN9EJN50CdHDcCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQQFAAOBgQAm
+SCwWwlj66BZ0DKqqX1Q/8tfJeGBeXm43YyJ3Nn6yF8Q0ufUIhfzJATj/Tb7yFkJD57taRvvBxhEf
+8UqwKEbJw8RCfbz6q1lu1bdRiBHjpIUZa4JMpAwSremkrj/xw0llmozFyD4lt5SZu5IycQfwhl7t
+UCemDaYj+bvLpgcUQg==
+-----END CERTIFICATE-----
+
+Equifax Secure CA
+=================
+-----BEGIN CERTIFICATE-----
+MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJVUzEQMA4GA1UE
+ChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlmaWNhdGUgQXV0aG9yaXR5
+MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoT
+B0VxdWlmYXgxLTArBgNVBAsTJEVxdWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCB
+nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPR
+fM6fBeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+AcJkVV5MW
+8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kCAwEAAaOCAQkwggEFMHAG
+A1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UE
+CxMkRXF1aWZheCBTZWN1cmUgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoG
+A1UdEAQTMBGBDzIwMTgwODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvS
+spXXR9gjIBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQFMAMB
+Af8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUAA4GBAFjOKer89961
+zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y7qj/WsjTVbJmcVfewCHrPSqnI0kB
+BIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee95
+70+sB3c4
+-----END CERTIFICATE-----
+
+Digital Signature Trust Co. Global CA 1
+=======================================
+-----BEGIN CERTIFICATE-----
+MIIDKTCCApKgAwIBAgIENnAVljANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJVUzEkMCIGA1UE
+ChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMREwDwYDVQQLEwhEU1RDQSBFMTAeFw05ODEy
+MTAxODEwMjNaFw0xODEyMTAxODQwMjNaMEYxCzAJBgNVBAYTAlVTMSQwIgYDVQQKExtEaWdpdGFs
+IFNpZ25hdHVyZSBUcnVzdCBDby4xETAPBgNVBAsTCERTVENBIEUxMIGdMA0GCSqGSIb3DQEBAQUA
+A4GLADCBhwKBgQCgbIGpzzQeJN3+hijM3oMv+V7UQtLodGBmE5gGHKlREmlvMVW5SXIACH7TpWJE
+NySZj9mDSI+ZbZUTu0M7LklOiDfBu1h//uG9+LthzfNHwJmm8fOR6Hh8AMthyUQncWlVSn5JTe2i
+o74CTADKAqjuAQIxZA9SLRN0dja1erQtcQIBA6OCASQwggEgMBEGCWCGSAGG+EIBAQQEAwIABzBo
+BgNVHR8EYTBfMF2gW6BZpFcwVTELMAkGA1UEBhMCVVMxJDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0
+dXJlIFRydXN0IENvLjERMA8GA1UECxMIRFNUQ0EgRTExDTALBgNVBAMTBENSTDEwKwYDVR0QBCQw
+IoAPMTk5ODEyMTAxODEwMjNagQ8yMDE4MTIxMDE4MTAyM1owCwYDVR0PBAQDAgEGMB8GA1UdIwQY
+MBaAFGp5fpFpRhgTCgJ3pVlbYJglDqL4MB0GA1UdDgQWBBRqeX6RaUYYEwoCd6VZW2CYJQ6i+DAM
+BgNVHRMEBTADAQH/MBkGCSqGSIb2fQdBAAQMMAobBFY0LjADAgSQMA0GCSqGSIb3DQEBBQUAA4GB
+ACIS2Hod3IEGtgllsofIH160L+nEHvI8wbsEkBFKg05+k7lNQseSJqBcNJo4cvj9axY+IO6CizEq
+kzaFI4iKPANo08kJD038bKTaKHKTDomAsH3+gG9lbRgzl4vCa4nuYD3Im+9/KzJic5PLPON74nZ4
+RbyhkwS7hp86W0N6w4pl
+-----END CERTIFICATE-----
+
+Digital Signature Trust Co. Global CA 3
+=======================================
+-----BEGIN CERTIFICATE-----
+MIIDKTCCApKgAwIBAgIENm7TzjANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJVUzEkMCIGA1UE
+ChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMREwDwYDVQQLEwhEU1RDQSBFMjAeFw05ODEy
+MDkxOTE3MjZaFw0xODEyMDkxOTQ3MjZaMEYxCzAJBgNVBAYTAlVTMSQwIgYDVQQKExtEaWdpdGFs
+IFNpZ25hdHVyZSBUcnVzdCBDby4xETAPBgNVBAsTCERTVENBIEUyMIGdMA0GCSqGSIb3DQEBAQUA
+A4GLADCBhwKBgQC/k48Xku8zExjrEH9OFr//Bo8qhbxe+SSmJIi2A7fBw18DW9Fvrn5C6mYjuGOD
+VvsoLeE4i7TuqAHhzhy2iCoiRoX7n6dwqUcUP87eZfCocfdPJmyMvMa1795JJ/9IKn3oTQPMx7JS
+xhcxEzu1TdvIxPbDDyQq2gyd55FbgM2UnQIBA6OCASQwggEgMBEGCWCGSAGG+EIBAQQEAwIABzBo
+BgNVHR8EYTBfMF2gW6BZpFcwVTELMAkGA1UEBhMCVVMxJDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0
+dXJlIFRydXN0IENvLjERMA8GA1UECxMIRFNUQ0EgRTIxDTALBgNVBAMTBENSTDEwKwYDVR0QBCQw
+IoAPMTk5ODEyMDkxOTE3MjZagQ8yMDE4MTIwOTE5MTcyNlowCwYDVR0PBAQDAgEGMB8GA1UdIwQY
+MBaAFB6CTShlgDzJQW6sNS5ay97u+DlbMB0GA1UdDgQWBBQegk0oZYA8yUFurDUuWsve7vg5WzAM
+BgNVHRMEBTADAQH/MBkGCSqGSIb2fQdBAAQMMAobBFY0LjADAgSQMA0GCSqGSIb3DQEBBQUAA4GB
+AEeNg61i8tuwnkUiBbmi1gMOOHLnnvx75pO2mqWilMg0HZHRxdf0CiUPPXiBng+xZ8SQTGPdXqfi
+up/1902lMXucKS1M/mQ+7LZT/uqb7YLbdHVLB3luHtgZg3Pe9T7Qtd7nS2h9Qy4qIOF+oHhEngj1
+mPnHfxsb1gYgAlihw6ID
+-----END CERTIFICATE-----
+
+Verisign Class 3 Public Primary Certification Authority
+=======================================================
+-----BEGIN CERTIFICATE-----
+MIICPDCCAaUCEHC65B0Q2Sk0tjjKewPMur8wDQYJKoZIhvcNAQECBQAwXzELMAkGA1UEBhMCVVMx
+FzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmltYXJ5
+IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVow
+XzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAz
+IFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUA
+A4GNADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhEBarsAx94
+f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/isI19wKTakyYbnsZogy1Ol
+hec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0GCSqGSIb3DQEBAgUAA4GBALtMEivPLCYA
+TxQT3ab7/AoRhIzzKBxnki98tsX63/Dolbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59Ah
+WM1pF+NEHJwZRDmJXNycAA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2Omuf
+Tqj/ZA1k
+-----END CERTIFICATE-----
+
+Verisign Class 1 Public Primary Certification Authority - G2
+============================================================
+-----BEGIN CERTIFICATE-----
+MIIDAjCCAmsCEEzH6qqYPnHTkxD4PTqJkZIwDQYJKoZIhvcNAQEFBQAwgcExCzAJBgNVBAYTAlVT
+MRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMSBQdWJsaWMgUHJpbWFy
+eSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2ln
+biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVz
+dCBOZXR3b3JrMB4XDTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVT
+MRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMSBQdWJsaWMgUHJpbWFy
+eSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2ln
+biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVz
+dCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCq0Lq+Fi24g9TK0g+8djHKlNgd
+k4xWArzZbxpvUjZudVYKVdPfQ4chEWWKfo+9Id5rMj8bhDSVBZ1BNeuS65bdqlk/AVNtmU/t5eIq
+WpDBucSmFc/IReumXY6cPvBkJHalzasab7bYe1FhbqZ/h8jit+U03EGI6glAvnOSPWvndQIDAQAB
+MA0GCSqGSIb3DQEBBQUAA4GBAKlPww3HZ74sy9mozS11534Vnjty637rXC0Jh9ZrbWB85a7FkCMM
+XErQr7Fd88e2CtvgFZMN3QO8x3aKtd1Pw5sTdbgBwObJW2uluIncrKTdcu1OofdPvAbT6shkdHvC
+lUGcZXNY8ZCaPGqxmMnEh7zPRW1F4m4iP/68DzFc6PLZ
+-----END CERTIFICATE-----
+
+Verisign Class 2 Public Primary Certification Authority - G2
+============================================================
+-----BEGIN CERTIFICATE-----
+MIIDAzCCAmwCEQC5L2DMiJ+hekYJuFtwbIqvMA0GCSqGSIb3DQEBBQUAMIHBMQswCQYDVQQGEwJV
+UzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xPDA6BgNVBAsTM0NsYXNzIDIgUHVibGljIFByaW1h
+cnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMjE6MDgGA1UECxMxKGMpIDE5OTggVmVyaVNp
+Z24sIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1
+c3QgTmV0d29yazAeFw05ODA1MTgwMDAwMDBaFw0yODA4MDEyMzU5NTlaMIHBMQswCQYDVQQGEwJV
+UzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xPDA6BgNVBAsTM0NsYXNzIDIgUHVibGljIFByaW1h
+cnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMjE6MDgGA1UECxMxKGMpIDE5OTggVmVyaVNp
+Z24sIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1
+c3QgTmV0d29yazCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAp4gBIXQs5xoD8JjhlzwPIQjx
+nNuX6Zr8wgQGE75fUsjMHiwSViy4AWkszJkfrbCWrnkE8hM5wXuYuggs6MKEEyyqaekJ9MepAqRC
+wiNPStjwDqL7MWzJ5m+ZJwf15vRMeJ5t60aG+rmGyVTyssSv1EYcWskVMP8NbPUtDm3Of3cCAwEA
+ATANBgkqhkiG9w0BAQUFAAOBgQByLvl/0fFx+8Se9sVeUYpAmLho+Jscg9jinb3/7aHmZuovCfTK
+1+qlK5X2JGCGTUQug6XELaDTrnhpb3LabK4I8GOSN+a7xDAXrXfMSTWqz9iP0b63GJZHc2pUIjRk
+LbYWm1lbtFFZOrMLFPQS32eg9K0yZF6xRnInjBJ7xUS0rg==
+-----END CERTIFICATE-----
+
+Verisign Class 3 Public Primary Certification Authority - G2
+============================================================
+-----BEGIN CERTIFICATE-----
+MIIDAjCCAmsCEH3Z/gfPqB63EHln+6eJNMYwDQYJKoZIhvcNAQEFBQAwgcExCzAJBgNVBAYTAlVT
+MRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMgUHJpbWFy
+eSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2ln
+biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVz
+dCBOZXR3b3JrMB4XDTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVT
+MRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMgUHJpbWFy
+eSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2ln
+biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVz
+dCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMXtERXVxp0KvTuWpMmR9ZmDCO
+FoUgRm1HP9SFIIThbbP4pO0M8RcPO/mn+SXXwc+EY/J8Y8+iR/LGWzOOZEAEaMGAuWQcRXfH2G71
+lSk8UOg013gfqLptQ5GVj0VXXn7F+8qkBOvqlzdUMG+7AUcyM83cV5tkaWH4mx0ciU9cZwIDAQAB
+MA0GCSqGSIb3DQEBBQUAA4GBAFFNzb5cy5gZnBWyATl4Lk0PZ3BwmcYQWpSkU01UbSuvDV1Ai2TT
+1+7eVmGSX6bEHRBhNtMsJzzoKQm5EWR0zLVznxxIqbxhAe7iF6YM40AIOw7n60RzKprxaZLvcRTD
+Oaxxp5EJb+RxBrO6WVcmeQD2+A2iMzAo1KpYoJ2daZH9
+-----END CERTIFICATE-----
+
+GlobalSign Root CA
+==================
+-----BEGIN CERTIFICATE-----
+MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkGA1UEBhMCQkUx
+GTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jvb3QgQ0ExGzAZBgNVBAMTEkds
+b2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAwMDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNV
+BAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYD
+VQQDExJHbG9iYWxTaWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDa
+DuaZjc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavpxy0Sy6sc
+THAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp1Wrjsok6Vjk4bwY8iGlb
+Kk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdGsnUOhugZitVtbNV4FpWi6cgKOOvyJBNP
+c1STE4U6G7weNLWLBYy5d4ux2x8gkasJU26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrX
+gzT/LCrBbBlDSgeF59N89iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
+HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0BAQUF
+AAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOzyj1hTdNGCbM+w6Dj
+Y1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE38NflNUVyRRBnMRddWQVDf9VMOyG
+j/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymPAbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhH
+hm4qxFYxldBniYUr+WymXUadDKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveC
+X4XSQRjbgbMEHMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A==
+-----END CERTIFICATE-----
+
+GlobalSign Root CA - R2
+=======================
+-----BEGIN CERTIFICATE-----
+MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4GA1UECxMXR2xv
+YmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2Jh
+bFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxT
+aWduIFJvb3QgQ0EgLSBSMjETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2ln
+bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6
+ErPLv4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8eoLrvozp
+s6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklqtTleiDTsvHgMCJiEbKjN
+S7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzdC9XZzPnqJworc5HGnRusyMvo4KD0L5CL
+TfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pazq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6C
+ygPCm48CAwEAAaOBnDCBmTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E
+FgQUm+IHV2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5nbG9i
+YWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG3lm0mi3f3BmGLjAN
+BgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4GsJ0/WwbgcQ3izDJr86iw8bmEbTUsp
+9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu
+01yiPqFbQfXf5WRDLenVOavSot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG7
+9G+dwfCMNYxdAfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7
+TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg==
+-----END CERTIFICATE-----
+
+ValiCert Class 1 VA
+===================
+-----BEGIN CERTIFICATE-----
+MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRp
+b24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs
+YXNzIDEgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZh
+bGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNTIy
+MjM0OFoXDTE5MDYyNTIyMjM0OFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0
+d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENsYXNzIDEg
+UG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0
+LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMIGfMA0GCSqGSIb3DQEBAQUA
+A4GNADCBiQKBgQDYWYJ6ibiWuqYvaG9YLqdUHAZu9OqNSLwxlBfw8068srg1knaw0KWlAdcAAxIi
+GQj4/xEjm84H9b9pGib+TunRf50sQB1ZaG6m+FiwnRqP0z/x3BkGgagO4DrdyFNFCQbmD3DD+kCm
+DuJWBQ8YTfwggtFzVXSNdnKgHZ0dwN0/cQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAFBoPUn0LBwG
+lN+VYH+Wexf+T3GtZMjdd9LvWVXoP+iOBSoh8gfStadS/pyxtuJbdxdA6nLWI8sogTLDAHkY7FkX
+icnGah5xyf23dKUlRWnFSKsZ4UWKJWsZ7uW7EvV/96aNUcPwnXS3qT6gpf+2SQMT2iLM7XGCK5nP
+Orf1LXLI
+-----END CERTIFICATE-----
+
+ValiCert Class 2 VA
+===================
+-----BEGIN CERTIFICATE-----
+MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRp
+b24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs
+YXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZh
+bGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAw
+MTk1NFoXDTE5MDYyNjAwMTk1NFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0
+d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENsYXNzIDIg
+UG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0
+LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMIGfMA0GCSqGSIb3DQEBAQUA
+A4GNADCBiQKBgQDOOnHK5avIWZJV16vYdA757tn2VUdZZUcOBVXc65g2PFxTXdMwzzjsvUGJ7SVC
+CSRrCl6zfN1SLUzm1NZ9WlmpZdRJEy0kTRxQb7XBhVQ7/nHk01xC+YDgkRoKWzk2Z/M/VXwbP7Rf
+ZHM047QSv4dk+NoS/zcnwbNDu+97bi5p9wIDAQABMA0GCSqGSIb3DQEBBQUAA4GBADt/UG9vUJSZ
+SWI4OB9L+KXIPqeCgfYrx+jFzug6EILLGACOTb2oWH+heQC1u+mNr0HZDzTuIYEZoDJJKPTEjlbV
+UjP9UNV+mWwD5MlM/Mtsq2azSiGM5bUMMj4QssxsodyamEwCW/POuZ6lcg5Ktz885hZo+L7tdEy8
+W9ViH0Pd
+-----END CERTIFICATE-----
+
+RSA Root Certificate 1
+======================
+-----BEGIN CERTIFICATE-----
+MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRp
+b24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs
+YXNzIDMgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZh
+bGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAw
+MjIzM1oXDTE5MDYyNjAwMjIzM1owgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0
+d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENsYXNzIDMg
+UG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0
+LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMIGfMA0GCSqGSIb3DQEBAQUA
+A4GNADCBiQKBgQDjmFGWHOjVsQaBalfDcnWTq8+epvzzFlLWLU2fNUSoLgRNB0mKOCn1dzfnt6td
+3zZxFJmP3MKS8edgkpfs2Ejcv8ECIMYkpChMMFp2bbFc893enhBxoYjHW5tBbcqwuI4V7q0zK89H
+BFx1cQqYJJgpp0lZpd34t0NiYfPT4tBVPwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAFa7AliEZwgs
+3x/be0kz9dNnnfS0ChCzycUs4pJqcXgn8nCDQtM+z6lU9PHYkhaM0QTLS6vJn0WuPIqpsHEzXcjF
+V9+vqDWzf4mH6eglkrh/hXqu1rweN1gqZ8mRzyqBPu3GOd/APhmcGcwTTYJBtYze4D1gCCAPRX5r
+on+jjBXu
+-----END CERTIFICATE-----
+
+Verisign Class 1 Public Primary Certification Authority - G3
+============================================================
+-----BEGIN CERTIFICATE-----
+MIIEGjCCAwICEQCLW3VWhFSFCwDPrzhIzrGkMA0GCSqGSIb3DQEBBQUAMIHKMQswCQYDVQQGEwJV
+UzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0IE5ldHdv
+cmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl
+IG9ubHkxRTBDBgNVBAMTPFZlcmlTaWduIENsYXNzIDEgUHVibGljIFByaW1hcnkgQ2VydGlmaWNh
+dGlvbiBBdXRob3JpdHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQsw
+CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRy
+dXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhv
+cml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWduIENsYXNzIDEgUHVibGljIFByaW1hcnkg
+Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
+ggEBAN2E1Lm0+afY8wR4nN493GwTFtl63SRRZsDHJlkNrAYIwpTRMx/wgzUfbhvI3qpuFU5UJ+/E
+bRrsC+MO8ESlV8dAWB6jRx9x7GD2bZTIGDnt/kIYVt/kTEkQeE4BdjVjEjbdZrwBBDajVWjVojYJ
+rKshJlQGrT/KFOCsyq0GHZXi+J3x4GD/wn91K0zM2v6HmSHquv4+VNfSWXjbPG7PoBMAGrgnoeS+
+Z5bKoMWznN3JdZ7rMJpfo83ZrngZPyPpXNspva1VyBtUjGP26KbqxzcSXKMpHgLZ2x87tNcPVkeB
+FQRKr4Mn0cVYiMHd9qqnoxjaaKptEVHhv2Vrn5Z20T0CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEA
+q2aN17O6x5q25lXQBfGfMY1aqtmqRiYPce2lrVNWYgFHKkTp/j90CxObufRNG7LRX7K20ohcs5/N
+y9Sn2WCVhDr4wTcdYcrnsMXlkdpUpqwxga6X3s0IrLjAl4B/bnKk52kTlWUfxJM8/XmPBNQ+T+r3
+ns7NZ3xPZQL/kYVUc8f/NveGLezQXk//EZ9yBta4GvFMDSZl4kSAHsef493oCtrspSCAaWihT37h
+a88HQfqDjrw43bAuEbFrskLMmrz5SCJ5ShkPshw+IHTZasO+8ih4E1Z5T21Q6huwtVexN2ZYI/Pc
+D98Kh8TvhgXVOBRgmaNL3gaWcSzy27YfpO8/7g==
+-----END CERTIFICATE-----
+
+Verisign Class 2 Public Primary Certification Authority - G3
+============================================================
+-----BEGIN CERTIFICATE-----
+MIIEGTCCAwECEGFwy0mMX5hFKeewptlQW3owDQYJKoZIhvcNAQEFBQAwgcoxCzAJBgNVBAYTAlVT
+MRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29y
+azE6MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ug
+b25seTFFMEMGA1UEAxM8VmVyaVNpZ24gQ2xhc3MgMiBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0
+aW9uIEF1dGhvcml0eSAtIEczMB4XDTk5MTAwMTAwMDAwMFoXDTM2MDcxNjIzNTk1OVowgcoxCzAJ
+BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1
+c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9y
+aXplZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNpZ24gQ2xhc3MgMiBQdWJsaWMgUHJpbWFyeSBD
+ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEczMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEArwoNwtUs22e5LeWUJ92lvuCwTY+zYVY81nzD9M0+hsuiiOLh2KRpxbXiv8GmR1BeRjmL1Za6
+tW8UvxDOJxOeBUebMXoT2B/Z0wI3i60sR/COgQanDTAM6/c8DyAd3HJG7qUCyFvDyVZpTMUYwZF7
+C9UTAJu878NIPkZgIIUq1ZC2zYugzDLdt/1AVbJQHFauzI13TccgTacxdu9okoqQHgiBVrKtaaNS
+0MscxCM9H5n+TOgWY47GCI72MfbS+uV23bUckqNJzc0BzWjNqWm6o+sdDZykIKbBoMXRRkwXbdKs
+Zj+WjOCE1Db/IlnF+RFgqF8EffIa9iVCYQ/ESrg+iQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQA0
+JhU8wI1NQ0kdvekhktdmnLfexbjQ5F1fdiLAJvmEOjr5jLX77GDx6M4EsMjdpwOPMPOY36TmpDHf
+0xwLRtxyID+u7gU8pDM/CzmscHhzS5kr3zDCVLCoO1Wh/hYozUK9dG6A2ydEp85EXdQbkJgNHkKU
+sQAsBNB0owIFImNjzYO1+8FtYmtpdf1dcEG59b98377BMnMiIYtYgXsVkXq642RIsH/7NiXaldDx
+JBQX3RiAa0YjOVT1jmIJBB2UkKab5iXiQkWquJCtvgiPqQtCGJTPcjnhsUPgKM+351psE2tJs//j
+GHyJizNdrDPXp/naOlXJWBD5qu9ats9LS98q
+-----END CERTIFICATE-----
+
+Verisign Class 3 Public Primary Certification Authority - G3
+============================================================
+-----BEGIN CERTIFICATE-----
+MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQswCQYDVQQGEwJV
+UzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0IE5ldHdv
+cmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl
+IG9ubHkxRTBDBgNVBAMTPFZlcmlTaWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNh
+dGlvbiBBdXRob3JpdHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQsw
+CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRy
+dXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhv
+cml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWduIENsYXNzIDMgUHVibGljIFByaW1hcnkg
+Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
+ggEBAMu6nFL8eB8aHm8bN3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1
+EUGO+i2tKmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGukxUc
+cLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBmCC+Vk7+qRy+oRpfw
+EuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJXwzw3sJ2zq/3avL6QaaiMxTJ5Xpj
+055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWuimi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEA
+ERSWwauSCPc/L8my/uRan2Te2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5f
+j267Cz3qWhMeDGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC
+/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565pF4ErWjfJXir0
+xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGtTxzhT5yvDwyd93gN2PQ1VoDa
+t20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ==
+-----END CERTIFICATE-----
+
+Verisign Class 4 Public Primary Certification Authority - G3
+============================================================
+-----BEGIN CERTIFICATE-----
+MIIEGjCCAwICEQDsoKeLbnVqAc/EfMwvlF7XMA0GCSqGSIb3DQEBBQUAMIHKMQswCQYDVQQGEwJV
+UzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0IE5ldHdv
+cmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl
+IG9ubHkxRTBDBgNVBAMTPFZlcmlTaWduIENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNh
+dGlvbiBBdXRob3JpdHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQsw
+CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRy
+dXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhv
+cml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWduIENsYXNzIDQgUHVibGljIFByaW1hcnkg
+Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
+ggEBAK3LpRFpxlmr8Y+1GQ9Wzsy1HyDkniYlS+BzZYlZ3tCD5PUPtbut8XzoIfzk6AzufEUiGXaS
+tBO3IFsJ+mGuqPKljYXCKtbeZjbSmwL0qJJgfJxptI8kHtCGUvYynEFYHiK9zUVilQhu0GbdU6LM
+8BDcVHOLBKFGMzNcF0C5nk3T875Vg+ixiY5afJqWIpA7iCXy0lOIAgwLePLmNxdLMEYH5IBtptiW
+Lugs+BGzOA1mppvqySNb247i8xOOGlktqgLw7KSHZtzBP/XYufTsgsbSPZUd5cBPhMnZo0QoBmrX
+Razwa2rvTl/4EYIeOGM0ZlDUPpNz+jDDZq3/ky2X7wMCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEA
+j/ola09b5KROJ1WrIhVZPMq1CtRK26vdoV9TxaBXOcLORyu+OshWv8LZJxA6sQU8wHcxuzrTBXtt
+mhwwjIDLk5Mqg6sFUYICABFna/OIYUdfA5PVWw3g8dShMjWFsjrbsIKr0csKvE+MW8VLADsfKoKm
+fjaF3H48ZwC15DtS4KjrXRX5xm3wrR0OhbepmnMUWluPQSjA1egtTaRezarZ7c7c2NU8Qh0XwRJd
+RTjDOPP8hS6DRkiy1yBfkjaP53kPmF6Z6PDQpLv1U70qzlmwr25/bLvSHgCwIe34QWKCudiyxLtG
+UPMxxY8BqHTr9Xgn2uf3ZkPznoM+IKrDNWCRzg==
+-----END CERTIFICATE-----
+
+Entrust.net Secure Server CA
+============================
+-----BEGIN CERTIFICATE-----
+MIIE2DCCBEGgAwIBAgIEN0rSQzANBgkqhkiG9w0BAQUFADCBwzELMAkGA1UEBhMCVVMxFDASBgNV
+BAoTC0VudHJ1c3QubmV0MTswOQYDVQQLEzJ3d3cuZW50cnVzdC5uZXQvQ1BTIGluY29ycC4gYnkg
+cmVmLiAobGltaXRzIGxpYWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRl
+ZDE6MDgGA1UEAxMxRW50cnVzdC5uZXQgU2VjdXJlIFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1dGhv
+cml0eTAeFw05OTA1MjUxNjA5NDBaFw0xOTA1MjUxNjM5NDBaMIHDMQswCQYDVQQGEwJVUzEUMBIG
+A1UEChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5jb3JwLiBi
+eSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBFbnRydXN0Lm5ldCBMaW1p
+dGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENlcnRpZmljYXRpb24gQXV0
+aG9yaXR5MIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQDNKIM0VBuJ8w+vN5Ex/68xYMmo6LIQ
+aO2f55M28Qpku0f1BBc/I0dNxScZgSYMVHINiC3ZH5oSn7yzcdOAGT9HZnuMNSjSuQrfJNqc1lB5
+gXpa0zf3wkrYKZImZNHkmGw6AIr1NJtl+O3jEP/9uElY3KDegjlrgbEWGWG5VLbmQwIBA6OCAdcw
+ggHTMBEGCWCGSAGG+EIBAQQEAwIABzCCARkGA1UdHwSCARAwggEMMIHeoIHboIHYpIHVMIHSMQsw
+CQYDVQQGEwJVUzEUMBIGA1UEChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5l
+dC9DUFMgaW5jb3JwLiBieSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBF
+bnRydXN0Lm5ldCBMaW1pdGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENl
+cnRpZmljYXRpb24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMCmgJ6AlhiNodHRwOi8vd3d3LmVu
+dHJ1c3QubmV0L0NSTC9uZXQxLmNybDArBgNVHRAEJDAigA8xOTk5MDUyNTE2MDk0MFqBDzIwMTkw
+NTI1MTYwOTQwWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAU8BdiE1U9s/8KAGv7UISX8+1i0Bow
+HQYDVR0OBBYEFPAXYhNVPbP/CgBr+1CEl/PtYtAaMAwGA1UdEwQFMAMBAf8wGQYJKoZIhvZ9B0EA
+BAwwChsEVjQuMAMCBJAwDQYJKoZIhvcNAQEFBQADgYEAkNwwAvpkdMKnCqV8IY00F6j7Rw7/JXyN
+Ewr75Ji174z4xRAN95K+8cPV1ZVqBLssziY2ZcgxxufuP+NXdYR6Ee9GTxj005i7qIcyunL2POI9
+n9cd2cNgQ4xYDiKWL2KjLB+6rQXvqzJ4h6BUcxm1XAX5Uj5tLUUL9wqT6u0G+bI=
+-----END CERTIFICATE-----
+
+Entrust.net Premium 2048 Secure Server CA
+=========================================
+-----BEGIN CERTIFICATE-----
+MIIEXDCCA0SgAwIBAgIEOGO5ZjANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChMLRW50cnVzdC5u
+ZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBpbmNvcnAuIGJ5IHJlZi4gKGxp
+bWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNV
+BAMTKkVudHJ1c3QubmV0IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQx
+NzUwNTFaFw0xOTEyMjQxODIwNTFaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3
+d3d3LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTEl
+MCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEGA1UEAxMqRW50cnVzdC5u
+ZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEArU1LqRKGsuqjIAcVFmQqK0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOL
+Gp18EzoOH1u3Hs/lJBQesYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSr
+hRSGlVuXMlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVTXTzW
+nLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/HoZdenoVve8AjhUi
+VBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH4QIDAQABo3QwcjARBglghkgBhvhC
+AQEEBAMCAAcwHwYDVR0jBBgwFoAUVeSB0RGAvtiJuQijMfmhJAkWuXAwHQYDVR0OBBYEFFXkgdER
+gL7YibkIozH5oSQJFrlwMB0GCSqGSIb2fQdBAAQQMA4bCFY1LjA6NC4wAwIEkDANBgkqhkiG9w0B
+AQUFAAOCAQEAWUesIYSKF8mciVMeuoCFGsY8Tj6xnLZ8xpJdGGQC49MGCBFhfGPjK50xA3B20qMo
+oPS7mmNz7W3lKtvtFKkrxjYR0CvrB4ul2p5cGZ1WEvVUKcgF7bISKo30Axv/55IQh7A6tcOdBTcS
+o8f0FbnVpDkWm1M6I5HxqIKiaohowXkCIryqptau37AUX7iH0N18f3v/rxzP5tsHrV7bhZ3QKw0z
+2wTR5klAEyt2+z7pnIkPFc4YsIV4IU9rTw76NmfNB/L/CNDi3tm/Kq+4h4YhPATKt5Rof8886ZjX
+OP/swNlQ8C5LWK5Gb9Auw2DaclVyvUxFnmG6v4SBkgPR0ml8xQ==
+-----END CERTIFICATE-----
+
+Baltimore CyberTrust Root
+=========================
+-----BEGIN CERTIFICATE-----
+MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJRTESMBAGA1UE
+ChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYDVQQDExlCYWx0aW1vcmUgQ3li
+ZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoXDTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMC
+SUUxEjAQBgNVBAoTCUJhbHRpbW9yZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFs
+dGltb3JlIEN5YmVyVHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKME
+uyKrmD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjrIZ3AQSsB
+UnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeKmpYcqWe4PwzV9/lSEy/C
+G9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSuXmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9
+XbIGevOF6uvUA65ehD5f/xXtabz5OTZydc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjpr
+l3RjM71oGDHweI12v/yejl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoI
+VDaGezq1BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEB
+BQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT929hkTI7gQCvlYpNRh
+cL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3WgxjkzSswF07r51XgdIGn9w/xZchMB5
+hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsa
+Y71k5h+3zvDyny67G7fyUIhzksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9H
+RCwBXbsdtTLSR9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp
+-----END CERTIFICATE-----
+
+Equifax Secure Global eBusiness CA
+==================================
+-----BEGIN CERTIFICATE-----
+MIICkDCCAfmgAwIBAgIBATANBgkqhkiG9w0BAQQFADBaMQswCQYDVQQGEwJVUzEcMBoGA1UEChMT
+RXF1aWZheCBTZWN1cmUgSW5jLjEtMCsGA1UEAxMkRXF1aWZheCBTZWN1cmUgR2xvYmFsIGVCdXNp
+bmVzcyBDQS0xMB4XDTk5MDYyMTA0MDAwMFoXDTIwMDYyMTA0MDAwMFowWjELMAkGA1UEBhMCVVMx
+HDAaBgNVBAoTE0VxdWlmYXggU2VjdXJlIEluYy4xLTArBgNVBAMTJEVxdWlmYXggU2VjdXJlIEds
+b2JhbCBlQnVzaW5lc3MgQ0EtMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuucXkAJlsTRV
+PEnCUdXfp9E3j9HngXNBUmCbnaEXJnitx7HoJpQytd4zjTov2/KaelpzmKNc6fuKcxtc58O/gGzN
+qfTWK8D3+ZmqY6KxRwIP1ORROhI8bIpaVIRw28HFkM9yRcuoWcDNM50/o5brhTMhHD4ePmBudpxn
+hcXIw2ECAwEAAaNmMGQwEQYJYIZIAYb4QgEBBAQDAgAHMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0j
+BBgwFoAUvqigdHJQa0S3ySPY+6j/s1draGwwHQYDVR0OBBYEFL6ooHRyUGtEt8kj2Puo/7NXa2hs
+MA0GCSqGSIb3DQEBBAUAA4GBADDiAVGqx+pf2rnQZQ8w1j7aDRRJbpGTJxQx78T3LUX47Me/okEN
+I7SS+RkAZ70Br83gcfxaz2TE4JaY0KNA4gGK7ycH8WUBikQtBmV1UsCGECAhX2xrD2yuCRyv8qIY
+NMR1pHMc8Y3c7635s3a0kr/clRAevsvIO1qEYBlWlKlV
+-----END CERTIFICATE-----
+
+Equifax Secure eBusiness CA 1
+=============================
+-----BEGIN CERTIFICATE-----
+MIICgjCCAeugAwIBAgIBBDANBgkqhkiG9w0BAQQFADBTMQswCQYDVQQGEwJVUzEcMBoGA1UEChMT
+RXF1aWZheCBTZWN1cmUgSW5jLjEmMCQGA1UEAxMdRXF1aWZheCBTZWN1cmUgZUJ1c2luZXNzIENB
+LTEwHhcNOTkwNjIxMDQwMDAwWhcNMjAwNjIxMDQwMDAwWjBTMQswCQYDVQQGEwJVUzEcMBoGA1UE
+ChMTRXF1aWZheCBTZWN1cmUgSW5jLjEmMCQGA1UEAxMdRXF1aWZheCBTZWN1cmUgZUJ1c2luZXNz
+IENBLTEwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAM4vGbwXt3fek6lfWg0XTzQaDJj0ItlZ
+1MRoRvC0NcWFAyDGr0WlIVFFQesWWDYyb+JQYmT5/VGcqiTZ9J2DKocKIdMSODRsjQBuWqDZQu4a
+IZX5UkxVWsUPOE9G+m34LjXWHXzr4vCwdYDIqROsvojvOm6rXyo4YgKwEnv+j6YDAgMBAAGjZjBk
+MBEGCWCGSAGG+EIBAQQEAwIABzAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFEp4MlIR21kW
+Nl7fwRQ2QGpHfEyhMB0GA1UdDgQWBBRKeDJSEdtZFjZe38EUNkBqR3xMoTANBgkqhkiG9w0BAQQF
+AAOBgQB1W6ibAxHm6VZMzfmpTMANmvPMZWnmJXbMWbfWVMMdzZmsGd20hdXgPfxiIKeES1hl8eL5
+lSE/9dR+WB5Hh1Q+WKG1tfgq73HnvMP2sUlG4tega+VWeponmHxGYhTnyfxuAxJ5gDgdSIKN/Bf+
+KpYrtWKmpj29f5JZzVoqgrI3eQ==
+-----END CERTIFICATE-----
+
+Equifax Secure eBusiness CA 2
+=============================
+-----BEGIN CERTIFICATE-----
+MIIDIDCCAomgAwIBAgIEN3DPtTANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJVUzEXMBUGA1UE
+ChMORXF1aWZheCBTZWN1cmUxJjAkBgNVBAsTHUVxdWlmYXggU2VjdXJlIGVCdXNpbmVzcyBDQS0y
+MB4XDTk5MDYyMzEyMTQ0NVoXDTE5MDYyMzEyMTQ0NVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoT
+DkVxdWlmYXggU2VjdXJlMSYwJAYDVQQLEx1FcXVpZmF4IFNlY3VyZSBlQnVzaW5lc3MgQ0EtMjCB
+nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA5Dk5kx5SBhsoNviyoynF7Y6yEb3+6+e0dMKP/wXn
+2Z0GvxLIPw7y1tEkshHe0XMJitSxLJgJDR5QRrKDpkWNYmi7hRsgcDKqQM2mll/EcTc/BPO3QSQ5
+BxoeLmFYoBIL5aXfxavqN3HMHMg3OrmXUqesxWoklE6ce8/AatbfIb0CAwEAAaOCAQkwggEFMHAG
+A1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORXF1aWZheCBTZWN1cmUx
+JjAkBgNVBAsTHUVxdWlmYXggU2VjdXJlIGVCdXNpbmVzcyBDQS0yMQ0wCwYDVQQDEwRDUkwxMBoG
+A1UdEAQTMBGBDzIwMTkwNjIzMTIxNDQ1WjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUUJ4L6q9e
+uSBIplBqy/3YIHqngnYwHQYDVR0OBBYEFFCeC+qvXrkgSKZQasv92CB6p4J2MAwGA1UdEwQFMAMB
+Af8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUAA4GBAAyGgq3oThr1
+jokn4jVYPSm0B482UJW/bsGe68SQsoWou7dC4A8HOd/7npCy0cE+U58DRLB+S/Rv5Hwf5+Kx5Lia
+78O9zt4LMjTZ3ijtM2vE1Nc9ElirfQkty3D1E4qUoSek1nDFbZS1yX2doNLGCEnZZpum0/QL3MUm
+V+GRMOrN
+-----END CERTIFICATE-----
+
+AddTrust Low-Value Services Root
+================================
+-----BEGIN CERTIFICATE-----
+MIIEGDCCAwCgAwIBAgIBATANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQGEwJTRTEUMBIGA1UEChML
+QWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYDVQQDExhBZGRU
+cnVzdCBDbGFzcyAxIENBIFJvb3QwHhcNMDAwNTMwMTAzODMxWhcNMjAwNTMwMTAzODMxWjBlMQsw
+CQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBO
+ZXR3b3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwggEiMA0GCSqGSIb3DQEB
+AQUAA4IBDwAwggEKAoIBAQCWltQhSWDia+hBBwzexODcEyPNwTXH+9ZOEQpnXvUGW2ulCDtbKRY6
+54eyNAbFvAWlA3yCyykQruGIgb3WntP+LVbBFc7jJp0VLhD7Bo8wBN6ntGO0/7Gcrjyvd7ZWxbWr
+oulpOj0OM3kyP3CCkplhbY0wCI9xP6ZIVxn4JdxLZlyldI+Yrsj5wAYi56xz36Uu+1LcsRVlIPo1
+Zmne3yzxbrww2ywkEtvrNTVokMsAsJchPXQhI2U0K7t4WaPW4XY5mqRJjox0r26kmqPZm9I4XJui
+GMx1I4S+6+JNM3GOGvDC+Mcdoq0Dlyz4zyXG9rgkMbFjXZJ/Y/AlyVMuH79NAgMBAAGjgdIwgc8w
+HQYDVR0OBBYEFJWxtPCUtr3H2tERCSG+wa9J/RB7MAsGA1UdDwQEAwIBBjAPBgNVHRMBAf8EBTAD
+AQH/MIGPBgNVHSMEgYcwgYSAFJWxtPCUtr3H2tERCSG+wa9J/RB7oWmkZzBlMQswCQYDVQQGEwJT
+RTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSEw
+HwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBACxt
+ZBsfzQ3duQH6lmM0MkhHma6X7f1yFqZzR1r0693p9db7RcwpiURdv0Y5PejuvE1Uhh4dbOMXJ0Ph
+iVYrqW9yTkkz43J8KiOavD7/KCrto/8cI7pDVwlnTUtiBi34/2ydYB7YHEt9tTEv2dB8Xfjea4MY
+eDdXL+gzB2ffHsdrKpV2ro9Xo/D0UrSpUwjP4E/TelOL/bscVjby/rK25Xa71SJlpz/+0WatC7xr
+mYbvP33zGDLKe8bjq2RGlfgmadlVg3sslgf/WSxEo8bl6ancoWOAWiFeIc9TVPC6b4nbqKqVz4vj
+ccweGyBECMB6tkD9xOQ14R0WHNC8K47Wcdk=
+-----END CERTIFICATE-----
+
+AddTrust External Root
+======================
+-----BEGIN CERTIFICATE-----
+MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEUMBIGA1UEChML
+QWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYD
+VQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEw
+NDgzOFowbzELMAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRU
+cnVzdCBFeHRlcm5hbCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0Eg
+Um9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvtH7xsD821
++iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9uMq/NzgtHj6RQa1wVsfw
+Tz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzXmk6vBbOmcZSccbNQYArHE504B4YCqOmo
+aSYYkKtMsE8jqzpPhNjfzp/haW+710LXa0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy
+2xSoRcRdKn23tNbE7qzNE0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv7
+7+ldU9U0WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYDVR0P
+BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0Jvf6xCZU7wO94CTL
+VBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRUcnVzdCBBQjEmMCQGA1UECxMdQWRk
+VHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsxIjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENB
+IFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZl
+j7DYd7usQWxHYINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5
+6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvCNr4TDea9Y355
+e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEXc4g/VhsxOBi0cQ+azcgOno4u
+G+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5amnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ=
+-----END CERTIFICATE-----
+
+AddTrust Public Services Root
+=============================
+-----BEGIN CERTIFICATE-----
+MIIEFTCCAv2gAwIBAgIBATANBgkqhkiG9w0BAQUFADBkMQswCQYDVQQGEwJTRTEUMBIGA1UEChML
+QWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSAwHgYDVQQDExdBZGRU
+cnVzdCBQdWJsaWMgQ0EgUm9vdDAeFw0wMDA1MzAxMDQxNTBaFw0yMDA1MzAxMDQxNTBaMGQxCzAJ
+BgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5l
+dHdvcmsxIDAeBgNVBAMTF0FkZFRydXN0IFB1YmxpYyBDQSBSb290MIIBIjANBgkqhkiG9w0BAQEF
+AAOCAQ8AMIIBCgKCAQEA6Rowj4OIFMEg2Dybjxt+A3S72mnTRqX4jsIMEZBRpS9mVEBV6tsfSlbu
+nyNu9DnLoblv8n75XYcmYZ4c+OLspoH4IcUkzBEMP9smcnrHAZcHF/nXGCwwfQ56HmIexkvA/X1i
+d9NEHif2P0tEs7c42TkfYNVRknMDtABp4/MUTu7R3AnPdzRGULD4EfL+OHn3Bzn+UZKXC1sIXzSG
+Aa2Il+tmzV7R/9x98oTaunet3IAIx6eH1lWfl2royBFkuucZKT8Rs3iQhCBSWxHveNCD9tVIkNAw
+HM+A+WD+eeSI8t0A65RF62WUaUC6wNW0uLp9BBGo6zEFlpROWCGOn9Bg/QIDAQABo4HRMIHOMB0G
+A1UdDgQWBBSBPjfYkrAfd59ctKtzquf2NGAv+jALBgNVHQ8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB
+/zCBjgYDVR0jBIGGMIGDgBSBPjfYkrAfd59ctKtzquf2NGAv+qFopGYwZDELMAkGA1UEBhMCU0Ux
+FDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQLExRBZGRUcnVzdCBUVFAgTmV0d29yazEgMB4G
+A1UEAxMXQWRkVHJ1c3QgUHVibGljIENBIFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBAAP3FUr4
+JNojVhaTdt02KLmuG7jD8WS6IBh4lSknVwW8fCr0uVFV2ocC3g8WFzH4qnkuCRO7r7IgGRLlk/lL
++YPoRNWyQSW/iHVv/xD8SlTQX/D67zZzfRs2RcYhbbQVuE7PnFylPVoAjgbjPGsye/Kf8Lb93/Ao
+GEjwxrzQvzSAlsJKsW2Ox5BF3i9nrEUEo3rcVZLJR2bYGozH7ZxOmuASu7VqTITh4SINhwBk/ox9
+Yjllpu9CtoAlEmEBqCQTcAARJl/6NVDFSMwGR+gn2HCNX2TmoUQmXiLsks3/QppEIW1cxeMiHV9H
+EufOX1362KqxMy3ZdvJOOjMMK7MtkAY=
+-----END CERTIFICATE-----
+
+AddTrust Qualified Certificates Root
+====================================
+-----BEGIN CERTIFICATE-----
+MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJTRTEUMBIGA1UEChML
+QWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSMwIQYDVQQDExpBZGRU
+cnVzdCBRdWFsaWZpZWQgQ0EgUm9vdDAeFw0wMDA1MzAxMDQ0NTBaFw0yMDA1MzAxMDQ0NTBaMGcx
+CzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQ
+IE5ldHdvcmsxIzAhBgNVBAMTGkFkZFRydXN0IFF1YWxpZmllZCBDQSBSb290MIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5B6a/twJWoekn0e+EV+vhDTbYjx5eLfpMLXsDBwqxBb/4Oxx
+64r1EW7tTw2R0hIYLUkVAcKkIhPHEWT/IhKauY5cLwjPcWqzZwFZ8V1G87B4pfYOQnrjfxvM0PC3
+KP0q6p6zsLkEqv32x7SxuCqg+1jxGaBvcCV+PmlKfw8i2O+tCBGaKZnhqkRFmhJePp1tUvznoD1o
+L/BLcHwTOK28FSXx1s6rosAx1i+f4P8UWfyEk9mHfExUE+uf0S0R+Bg6Ot4l2ffTQO2kBhLEO+GR
+wVY18BTcZTYJbqukB8c10cIDMzZbdSZtQvESa0NvS3GU+jQd7RNuyoB/mC9suWXY6QIDAQABo4HU
+MIHRMB0GA1UdDgQWBBQ5lYtii1zJ1IC6WA+XPxUIQ8yYpzALBgNVHQ8EBAMCAQYwDwYDVR0TAQH/
+BAUwAwEB/zCBkQYDVR0jBIGJMIGGgBQ5lYtii1zJ1IC6WA+XPxUIQ8yYp6FrpGkwZzELMAkGA1UE
+BhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQLExRBZGRUcnVzdCBUVFAgTmV0d29y
+azEjMCEGA1UEAxMaQWRkVHJ1c3QgUXVhbGlmaWVkIENBIFJvb3SCAQEwDQYJKoZIhvcNAQEFBQAD
+ggEBABmrder4i2VhlRO6aQTvhsoToMeqT2QbPxj2qC0sVY8FtzDqQmodwCVRLae/DLPt7wh/bDxG
+GuoYQ992zPlmhpwsaPXpF/gxsxjE1kh9I0xowX67ARRvxdlu3rsEQmr49lx95dr6h+sNNVJn0J6X
+dgWTP5XHAeZpVTh/EGGZyeNfpso+gmNIquIISD6q8rKFYqa0p9m9N5xotS1WfbC3P6CxB9bpT9ze
+RXEwMn8bLgn5v1Kh7sKAPgZcLlVAwRv1cEWw3F369nJad9Jjzc9YiQBCYz95OdBEsIJuQRno3eDB
+iFrRHnGTHyQwdOUeqN48Jzd/g66ed8/wMLH/S5noxqE=
+-----END CERTIFICATE-----
+
+Entrust Root Certification Authority
+====================================
+-----BEGIN CERTIFICATE-----
+MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMCVVMxFjAUBgNV
+BAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0Lm5ldC9DUFMgaXMgaW5jb3Jw
+b3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMWKGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsG
+A1UEAxMkRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0
+MloXDTI2MTEyNzIwNTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMu
+MTkwNwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSByZWZlcmVu
+Y2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNVBAMTJEVudHJ1c3QgUm9v
+dCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+ALaVtkNC+sZtKm9I35RMOVcF7sN5EUFoNu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYsz
+A9u3g3s+IIRe7bJWKKf44LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOww
+Cj0Yzfv9KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGIrb68
+j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi94DkZfs0Nw4pgHBN
+rziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOBsDCBrTAOBgNVHQ8BAf8EBAMCAQYw
+DwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAigA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1
+MzQyWjAfBgNVHSMEGDAWgBRokORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DH
+hmak8fdLQ/uEvW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA
+A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9tO1KzKtvn1ISM
+Y/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6ZuaAGAT/3B+XxFNSRuzFVJ7yVTa
+v52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTS
+W3iDVuycNsMm4hH2Z0kdkquM++v/eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0
+tHuu2guQOHXvgR1m0vdXcDazv/wor3ElhVsT/h5/WrQ8
+-----END CERTIFICATE-----
+
+RSA Security 2048 v3
+====================
+-----BEGIN CERTIFICATE-----
+MIIDYTCCAkmgAwIBAgIQCgEBAQAAAnwAAAAKAAAAAjANBgkqhkiG9w0BAQUFADA6MRkwFwYDVQQK
+ExBSU0EgU2VjdXJpdHkgSW5jMR0wGwYDVQQLExRSU0EgU2VjdXJpdHkgMjA0OCBWMzAeFw0wMTAy
+MjIyMDM5MjNaFw0yNjAyMjIyMDM5MjNaMDoxGTAXBgNVBAoTEFJTQSBTZWN1cml0eSBJbmMxHTAb
+BgNVBAsTFFJTQSBTZWN1cml0eSAyMDQ4IFYzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEAt49VcdKA3XtpeafwGFAyPGJn9gqVB93mG/Oe2dJBVGutn3y+Gc37RqtBaB4Y6lXIL5F4iSj7
+Jylg/9+PjDvJSZu1pJTOAeo+tWN7fyb9Gd3AIb2E0S1PRsNO3Ng3OTsor8udGuorryGlwSMiuLgb
+WhOHV4PR8CDn6E8jQrAApX2J6elhc5SYcSa8LWrg903w8bYqODGBDSnhAMFRD0xS+ARaqn1y07iH
+KrtjEAMqs6FPDVpeRrc9DvV07Jmf+T0kgYim3WBU6JU2PcYJk5qjEoAAVZkZR73QpXzDuvsf9/UP
++Ky5tfQ3mBMY3oVbtwyCO4dvlTlYMNpuAWgXIszACwIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/
+MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQHw1EwpKrpRa41JPr/JCwz0LGdjDAdBgNVHQ4E
+FgQUB8NRMKSq6UWuNST6/yQsM9CxnYwwDQYJKoZIhvcNAQEFBQADggEBAF8+hnZuuDU8TjYcHnmY
+v/3VEhF5Ug7uMYm83X/50cYVIeiKAVQNOvtUudZj1LGqlk2iQk3UUx+LEN5/Zb5gEydxiKRz44Rj
+0aRV4VCT5hsOedBnvEbIvz8XDZXmxpBp3ue0L96VfdASPz0+f00/FGj1EVDVwfSQpQgdMWD/YIwj
+VAqv/qFuxdF6Kmh4zx6CCiC0H63lhbJqaHVOrSU3lIW+vaHU6rcMSzyd6BIA8F+sDeGscGNz9395
+nzIlQnQFgCi/vcEkllgVsRch6YlL2weIZ/QVrXA+L02FO8K32/6YaCOJ4XQP3vTFhGMpG8zLB8kA
+pKnXwiJPZ9d37CAFYd4=
+-----END CERTIFICATE-----
+
+GeoTrust Global CA
+==================
+-----BEGIN CERTIFICATE-----
+MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVTMRYwFAYDVQQK
+Ew1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9iYWwgQ0EwHhcNMDIwNTIxMDQw
+MDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5j
+LjEbMBkGA1UEAxMSR2VvVHJ1c3QgR2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEA2swYYzD99BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjo
+BbdqfnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDviS2Aelet
+8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU1XupGc1V3sjs0l44U+Vc
+T4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+bw8HHa8sHo9gOeL6NlMTOdReJivbPagU
+vTLrGAMoUgRx5aszPeE4uwc2hGKceeoWMPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTAD
+AQH/MB0GA1UdDgQWBBTAephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVk
+DBF9qn1luMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKInZ57Q
+zxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfStQWVYrmm3ok9Nns4
+d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcFPseKUgzbFbS9bZvlxrFUaKnjaZC2
+mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Unhw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6p
+XE0zX5IJL4hmXXeXxx12E6nV5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvm
+Mw==
+-----END CERTIFICATE-----
+
+GeoTrust Global CA 2
+====================
+-----BEGIN CERTIFICATE-----
+MIIDZjCCAk6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBEMQswCQYDVQQGEwJVUzEWMBQGA1UEChMN
+R2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3QgR2xvYmFsIENBIDIwHhcNMDQwMzA0MDUw
+MDAwWhcNMTkwMzA0MDUwMDAwWjBEMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5j
+LjEdMBsGA1UEAxMUR2VvVHJ1c3QgR2xvYmFsIENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
+ggEKAoIBAQDvPE1APRDfO1MA4Wf+lGAVPoWI8YkNkMgoI5kF6CsgncbzYEbYwbLVjDHZ3CB5JIG/
+NTL8Y2nbsSpr7iFY8gjpeMtvy/wWUsiRxP89c96xPqfCfWbB9X5SJBri1WeR0IIQ13hLTytCOb1k
+LUCgsBDTOEhGiKEMuzozKmKY+wCdE1l/bztyqu6mD4b5BWHqZ38MN5aL5mkWRxHCJ1kDs6ZgwiFA
+Vvqgx306E+PsV8ez1q6diYD3Aecs9pYrEw15LNnA5IZ7S4wMcoKK+xfNAGw6EzywhIdLFnopsk/b
+HdQL82Y3vdj2V7teJHq4PIu5+pIaGoSe2HSPqht/XvT+RSIhAgMBAAGjYzBhMA8GA1UdEwEB/wQF
+MAMBAf8wHQYDVR0OBBYEFHE4NvICMVNHK266ZUapEBVYIAUJMB8GA1UdIwQYMBaAFHE4NvICMVNH
+K266ZUapEBVYIAUJMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQUFAAOCAQEAA/e1K6tdEPx7
+srJerJsOflN4WT5CBP51o62sgU7XAotexC3IUnbHLB/8gTKY0UvGkpMzNTEv/NgdRN3ggX+d6Yvh
+ZJFiCzkIjKx0nVnZellSlxG5FntvRdOW2TF9AjYPnDtuzywNA0ZF66D0f0hExghAzN4bcLUprbqL
+OzRldRtxIR0sFAqwlpW41uryZfspuk/qkZN0abby/+Ea0AzRdoXLiiW9l14sbxWZJue2Kf8i7MkC
+x1YAzUm5s2x7UwQa4qjJqhIFI8LO57sEAszAR6LkxCkvW0VXiVHuPOtSCP8HNR6fNWpHSlaY0VqF
+H4z1Ir+rzoPz4iIprn2DQKi6bA==
+-----END CERTIFICATE-----
+
+GeoTrust Universal CA
+=====================
+-----BEGIN CERTIFICATE-----
+MIIFaDCCA1CgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJVUzEWMBQGA1UEChMN
+R2VvVHJ1c3QgSW5jLjEeMBwGA1UEAxMVR2VvVHJ1c3QgVW5pdmVyc2FsIENBMB4XDTA0MDMwNDA1
+MDAwMFoXDTI5MDMwNDA1MDAwMFowRTELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IElu
+Yy4xHjAcBgNVBAMTFUdlb1RydXN0IFVuaXZlcnNhbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
+ADCCAgoCggIBAKYVVaCjxuAfjJ0hUNfBvitbtaSeodlyWL0AG0y/YckUHUWCq8YdgNY96xCcOq9t
+JPi8cQGeBvV8Xx7BDlXKg5pZMK4ZyzBIle0iN430SppyZj6tlcDgFgDgEB8rMQ7XlFTTQjOgNB0e
+RXbdT8oYN+yFFXoZCPzVx5zw8qkuEKmS5j1YPakWaDwvdSEYfyh3peFhF7em6fgemdtzbvQKoiFs
+7tqqhZJmr/Z6a4LauiIINQ/PQvE1+mrufislzDoR5G2vc7J2Ha3QsnhnGqQ5HFELZ1aD/ThdDc7d
+8Lsrlh/eezJS/R27tQahsiFepdaVaH/wmZ7cRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7V
+qnJNk22CDtucvc+081xdVHppCZbW2xHBjXWotM85yM48vCR85mLK4b19p71XZQvk/iXttmkQ3Cga
+Rr0BHdCXteGYO8A3ZNY9lO4L4fUorgtWv3GLIylBjobFS1J72HGrH4oVpjuDWtdYAVHGTEHZf9hB
+Z3KiKN9gg6meyHv8U3NyWfWTehd2Ds735VzZC1U0oqpbtWpU5xPKV+yXbfReBi9Fi1jUIxaS5BZu
+KGNZMN9QAZxjiRqf2xeUgnA3wySemkfWWspOqGmJch+RbNt+nhutxx9z3SxPGWX9f5NAEC7S8O08
+ni4oPmkmM8V7AgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNq7LqqwDLiIJlF0
+XG0D08DYj3rWMB8GA1UdIwQYMBaAFNq7LqqwDLiIJlF0XG0D08DYj3rWMA4GA1UdDwEB/wQEAwIB
+hjANBgkqhkiG9w0BAQUFAAOCAgEAMXjmx7XfuJRAyXHEqDXsRh3ChfMoWIawC/yOsjmPRFWrZIRc
+aanQmjg8+uUfNeVE44B5lGiku8SfPeE0zTBGi1QrlaXv9z+ZhP015s8xxtxqv6fXIwjhmF7DWgh2
+qaavdy+3YL1ERmrvl/9zlcGO6JP7/TG37FcREUWbMPEaiDnBTzynANXH/KttgCJwpQzgXQQpAvvL
+oJHRfNbDflDVnVi+QTjruXU8FdmbyUqDWcDaU/0zuzYYm4UPFd3uLax2k7nZAY1IEKj79TiG8dsK
+xr2EoyNB3tZ3b4XUhRxQ4K5RirqNPnbiucon8l+f725ZDQbYKxek0nxru18UGkiPGkzns0ccjkxF
+KyDuSN/n3QmOGKjaQI2SJhFTYXNd673nxE0pN2HrrDktZy4W1vUAg4WhzH92xH3kt0tm7wNFYGm2
+DFKWkoRepqO1pD4r2czYG0eq8kTaT/kD6PAUyz/zg97QwVTjt+gKN02LIFkDMBmhLMi9ER/frslK
+xfMnZmaGrGiR/9nmUxwPi1xpZQomyB40w11Re9epnAahNt3ViZS82eQtDF4JbAiXfKM9fJP/P6EU
+p8+1Xevb2xzEdt+Iub1FBZUbrvxGakyvSOPOrg/SfuvmbJxPgWp6ZKy7PtXny3YuxadIwVyQD8vI
+P/rmMuGNG2+k5o7Y+SlIis5z/iw=
+-----END CERTIFICATE-----
+
+GeoTrust Universal CA 2
+=======================
+-----BEGIN CERTIFICATE-----
+MIIFbDCCA1SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBHMQswCQYDVQQGEwJVUzEWMBQGA1UEChMN
+R2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVyc2FsIENBIDIwHhcNMDQwMzA0
+MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3Qg
+SW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVyc2FsIENBIDIwggIiMA0GCSqGSIb3DQEBAQUA
+A4ICDwAwggIKAoICAQCzVFLByT7y2dyxUxpZKeexw0Uo5dfR7cXFS6GqdHtXr0om/Nj1XqduGdt0
+DE81WzILAePb63p3NeqqWuDW6KFXlPCQo3RWlEQwAx5cTiuFJnSCegx2oG9NzkEtoBUGFF+3Qs17
+j1hhNNwqCPkuwwGmIkQcTAeC5lvO0Ep8BNMZcyfwqph/Lq9O64ceJHdqXbboW0W63MOhBW9Wjo8Q
+JqVJwy7XQYci4E+GymC16qFjwAGXEHm9ADwSbSsVsaxLse4YuU6W3Nx2/zu+z18DwPw76L5GG//a
+QMJS9/7jOvdqdzXQ2o3rXhhqMcceujwbKNZrVMaqW9eiLBsZzKIC9ptZvTdrhrVtgrrY6slWvKk2
+WP0+GfPtDCapkzj4T8FdIgbQl+rhrcZV4IErKIM6+vR7IVEAvlI4zs1meaj0gVbi0IMJR1FbUGrP
+20gaXT73y/Zl92zxlfgCOzJWgjl6W70viRu/obTo/3+NjN8D8WBOWBFM66M/ECuDmgFz2ZRthAAn
+ZqzwcEAJQpKtT5MNYQlRJNiS1QuUYbKHsu3/mjX/hVTK7URDrBs8FmtISgocQIgfksILAAX/8sgC
+SqSqqcyZlpwvWOB94b67B9xfBHJcMTTD7F8t4D1kkCLm0ey4Lt1ZrtmhN79UNdxzMk+MBB4zsslG
+8dhcyFVQyWi9qLo2CQIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR281Xh+qQ2
++/CfXGJx7Tz0RzgQKzAfBgNVHSMEGDAWgBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAOBgNVHQ8BAf8E
+BAMCAYYwDQYJKoZIhvcNAQEFBQADggIBAGbBxiPz2eAubl/oz66wsCVNK/g7WJtAJDday6sWSf+z
+dXkzoS9tcBc0kf5nfo/sm+VegqlVHy/c1FEHEv6sFj4sNcZj/NwQ6w2jqtB8zNHQL1EuxBRa3ugZ
+4T7GzKQp5y6EqgYweHZUcyiYWTjgAA1i00J9IZ+uPTqM1fp3DRgrFg5fNuH8KrUwJM/gYwx7WBr+
+mbpCErGR9Hxo4sjoryzqyX6uuyo9DRXcNJW2GHSoag/HtPQTxORb7QrSpJdMKu0vbBKJPfEncKpq
+A1Ihn0CoZ1Dy81of398j9tx4TuaYT1U6U+Pv8vSfx3zYWK8pIpe44L2RLrB27FcRz+8pRPPphXpg
+Y+RdM4kX2TGq2tbzGDVyz4crL2MjhF2EjD9XoIj8mZEoJmmZ1I+XRL6O1UixpCgp8RW04eWe3fiP
+pm8m1wk8OhwRDqZsN/etRIcsKMfYdIKz0G9KV7s1KSegi+ghp4dkNl3M2Basx7InQJJVOCiNUW7d
+FGdTbHFcJoRNdVq2fmBWqU2t+5sel/MN2dKXVHfaPRK34B7vCAas+YWH6aLcr34YEoP9VhdBLtUp
+gn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwXQMAJKOSLakhT2+zNVVXxxvjpoixMptEm
+X36vWkzaH6byHCx+rgIW0lbQL1dTR+iS
+-----END CERTIFICATE-----
+
+UTN-USER First-Network Applications
+===================================
+-----BEGIN CERTIFICATE-----
+MIIEZDCCA0ygAwIBAgIQRL4Mi1AAJLQR0zYwS8AzdzANBgkqhkiG9w0BAQUFADCBozELMAkGA1UE
+BhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEeMBwGA1UEChMVVGhl
+IFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xKzAp
+BgNVBAMTIlVUTi1VU0VSRmlyc3QtTmV0d29yayBBcHBsaWNhdGlvbnMwHhcNOTkwNzA5MTg0ODM5
+WhcNMTkwNzA5MTg1NzQ5WjCBozELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5T
+YWx0IExha2UgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho
+dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xKzApBgNVBAMTIlVUTi1VU0VSRmlyc3QtTmV0d29yayBB
+cHBsaWNhdGlvbnMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCz+5Gh5DZVhawGNFug
+mliy+LUPBXeDrjKxdpJo7CNKyXY/45y2N3kDuatpjQclthln5LAbGHNhSuh+zdMvZOOmfAz6F4Cj
+DUeJT1FxL+78P/m4FoCHiZMlIJpDgmkkdihZNaEdwH+DBmQWICzTSaSFtMBhf1EI+GgVkYDLpdXu
+Ozr0hAReYFmnjDRy7rh4xdE7EkpvfmUnuaRVxblvQ6TFHSyZwFKkeEwVs0CYCGtDxgGwenv1axwi
+P8vv/6jQOkt2FZ7S0cYu49tXGzKiuG/ohqY/cKvlcJKrRB5AUPuco2LkbG6gyN7igEL66S/ozjIE
+j3yNtxyjNTwV3Z7DrpelAgMBAAGjgZEwgY4wCwYDVR0PBAQDAgHGMA8GA1UdEwEB/wQFMAMBAf8w
+HQYDVR0OBBYEFPqGydvguul49Uuo1hXf8NPhahQ8ME8GA1UdHwRIMEYwRKBCoECGPmh0dHA6Ly9j
+cmwudXNlcnRydXN0LmNvbS9VVE4tVVNFUkZpcnN0LU5ldHdvcmtBcHBsaWNhdGlvbnMuY3JsMA0G
+CSqGSIb3DQEBBQUAA4IBAQCk8yXM0dSRgyLQzDKrm5ZONJFUICU0YV8qAhXhi6r/fWRRzwr/vH3Y
+IWp4yy9Rb/hCHTO967V7lMPDqaAt39EpHx3+jz+7qEUqf9FuVSTiuwL7MT++6LzsQCv4AdRWOOTK
+RIK1YSAhZ2X28AvnNPilwpyjXEAfhZOVBt5P1CeptqX8Fs1zMT+4ZSfP1FMa8Kxun08FDAOBp4Qp
+xFq9ZFdyrTvPNximmMatBrTcCKME1SmklpoSZ0qMYEWd8SOasACcaLWYUNPvji6SZbFIPiG+FTAq
+DbUMo2s/rn9X9R+WfN9v3YIwLGUbQErNaLly7HF27FSOH4UMAWr6pjisH8SE
+-----END CERTIFICATE-----
+
+America Online Root Certification Authority 1
+=============================================
+-----BEGIN CERTIFICATE-----
+MIIDpDCCAoygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEcMBoGA1UEChMT
+QW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBPbmxpbmUgUm9vdCBDZXJ0aWZp
+Y2F0aW9uIEF1dGhvcml0eSAxMB4XDTAyMDUyODA2MDAwMFoXDTM3MTExOTIwNDMwMFowYzELMAkG
+A1UEBhMCVVMxHDAaBgNVBAoTE0FtZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2Eg
+T25saW5lIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMTCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBAKgv6KRpBgNHw+kqmP8ZonCaxlCyfqXfaE0bfA+2l2h9LaaLl+lkhsmj76CG
+v2BlnEtUiMJIxUo5vxTjWVXlGbR0yLQFOVwWpeKVBeASrlmLojNoWBym1BW32J/X3HGrfpq/m44z
+DyL9Hy7nBzbvYjnF3cu6JRQj3gzGPTzOggjmZj7aUTsWOqMFf6Dch9Wc/HKpoH145LcxVR5lu9Rh
+sCFg7RAycsWSJR74kEoYeEfffjA3PlAb2xzTa5qGUwew76wGePiEmf4hjUyAtgyC9mZweRrTT6PP
+8c9GsEsPPt2IYriMqQkoO3rHl+Ee5fSfwMCuJKDIodkP1nsmgmkyPacCAwEAAaNjMGEwDwYDVR0T
+AQH/BAUwAwEB/zAdBgNVHQ4EFgQUAK3Zo/Z59m50qX8zPYEX10zPM94wHwYDVR0jBBgwFoAUAK3Z
+o/Z59m50qX8zPYEX10zPM94wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBBQUAA4IBAQB8itEf
+GDeC4Liwo+1WlchiYZwFos3CYiZhzRAW18y0ZTTQEYqtqKkFZu90821fnZmv9ov761KyBZiibyrF
+VL0lvV+uyIbqRizBs73B6UlwGBaXCBOMIOAbLjpHyx7kADCVW/RFo8AasAFOq73AI25jP4BKxQft
+3OJvx8Fi8eNy1gTIdGcL+oiroQHIb/AUr9KZzVGTfu0uOMe9zkZQPXLjeSWdm4grECDdpbgyn43g
+Kd8hdIaC2y+CMMbHNYaz+ZZfRtsMRf3zUMNvxsNIrUam4SdHCh0Om7bCd39j8uB9Gr784N/Xx6ds
+sPmuujz9dLQR6FgNgLzTqIA6me11zEZ7
+-----END CERTIFICATE-----
+
+America Online Root Certification Authority 2
+=============================================
+-----BEGIN CERTIFICATE-----
+MIIFpDCCA4ygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEcMBoGA1UEChMT
+QW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBPbmxpbmUgUm9vdCBDZXJ0aWZp
+Y2F0aW9uIEF1dGhvcml0eSAyMB4XDTAyMDUyODA2MDAwMFoXDTM3MDkyOTE0MDgwMFowYzELMAkG
+A1UEBhMCVVMxHDAaBgNVBAoTE0FtZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2Eg
+T25saW5lIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMjCCAiIwDQYJKoZIhvcNAQEBBQAD
+ggIPADCCAgoCggIBAMxBRR3pPU0Q9oyxQcngXssNt79Hc9PwVU3dxgz6sWYFas14tNwC206B89en
+fHG8dWOgXeMHDEjsJcQDIPT/DjsS/5uN4cbVG7RtIuOx238hZK+GvFciKtZHgVdEglZTvYYUAQv8
+f3SkWq7xuhG1m1hagLQ3eAkzfDJHA1zEpYNI9FdWboE2JxhP7JsowtS013wMPgwr38oE18aO6lhO
+qKSlGBxsRZijQdEt0sdtjRnxrXm3gT+9BoInLRBYBbV4Bbkv2wxrkJB+FFk4u5QkE+XRnRTf04JN
+RvCAOVIyD+OEsnpD8l7eXz8d3eOyG6ChKiMDbi4BFYdcpnV1x5dhvt6G3NRI270qv0pV2uh9UPu0
+gBe4lL8BPeraunzgWGcXuVjgiIZGZ2ydEEdYMtA1fHkqkKJaEBEjNa0vzORKW6fIJ/KD3l67Xnfn
+6KVuY8INXWHQjNJsWiEOyiijzirplcdIz5ZvHZIlyMbGwcEMBawmxNJ10uEqZ8A9W6Wa6897Gqid
+FEXlD6CaZd4vKL3Ob5Rmg0gp2OpljK+T2WSfVVcmv2/LNzGZo2C7HK2JNDJiuEMhBnIMoVxtRsX6
+Kc8w3onccVvdtjc+31D1uAclJuW8tf48ArO3+L5DwYcRlJ4jbBeKuIonDFRH8KmzwICMoCfrHRnj
+B453cMor9H124HhnAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFE1FwWg4u3Op
+aaEg5+31IqEjFNeeMB8GA1UdIwQYMBaAFE1FwWg4u3OpaaEg5+31IqEjFNeeMA4GA1UdDwEB/wQE
+AwIBhjANBgkqhkiG9w0BAQUFAAOCAgEAZ2sGuV9FOypLM7PmG2tZTiLMubekJcmnxPBUlgtk87FY
+T15R/LKXeydlwuXK5w0MJXti4/qftIe3RUavg6WXSIylvfEWK5t2LHo1YGwRgJfMqZJS5ivmae2p
++DYtLHe/YUjRYwu5W1LtGLBDQiKmsXeu3mnFzcccobGlHBD7GL4acN3Bkku+KVqdPzW+5X1R+FXg
+JXUjhx5c3LqdsKyzadsXg8n33gy8CNyRnqjQ1xU3c6U1uPx+xURABsPr+CKAXEfOAuMRn0T//Zoy
+zH1kUQ7rVyZ2OuMeIjzCpjbdGe+n/BLzJsBZMYVMnNjP36TMzCmT/5RtdlwTCJfy7aULTd3oyWgO
+ZtMADjMSW7yV5TKQqLPGbIOtd+6Lfn6xqavT4fG2wLHqiMDn05DpKJKUe2h7lyoKZy2FAjgQ5ANh
+1NolNscIWC2hp1GvMApJ9aZphwctREZ2jirlmjvXGKL8nDgQzMY70rUXOm/9riW99XJZZLF0Kjhf
+GEzfz3EEWjbUvy+ZnOjZurGV5gJLIaFb1cFPj65pbVPbAZO1XB4Y3WRayhgoPmMEEf0cjQAPuDff
+Z4qdZqkCapH/E8ovXYO8h5Ns3CRRFgQlZvqz2cK6Kb6aSDiCmfS/O0oxGfm/jiEzFMpPVF/7zvuP
+cX/9XhmgD0uRuMRUvAawRY8mkaKO/qk=
+-----END CERTIFICATE-----
+
+Visa eCommerce Root
+===================
+-----BEGIN CERTIFICATE-----
+MIIDojCCAoqgAwIBAgIQE4Y1TR0/BvLB+WUF1ZAcYjANBgkqhkiG9w0BAQUFADBrMQswCQYDVQQG
+EwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRlcm5hdGlvbmFsIFNlcnZpY2Ug
+QXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNvbW1lcmNlIFJvb3QwHhcNMDIwNjI2MDIxODM2
+WhcNMjIwNjI0MDAxNjEyWjBrMQswCQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMm
+VmlzYSBJbnRlcm5hdGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNv
+bW1lcmNlIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvV95WHm6h2mCxlCfL
+F9sHP4CFT8icttD0b0/Pmdjh28JIXDqsOTPHH2qLJj0rNfVIsZHBAk4ElpF7sDPwsRROEW+1QK8b
+RaVK7362rPKgH1g/EkZgPI2h4H3PVz4zHvtH8aoVlwdVZqW1LS7YgFmypw23RuwhY/81q6UCzyr0
+TP579ZRdhE2o8mCP2w4lPJ9zcc+U30rq299yOIzzlr3xF7zSujtFWsan9sYXiwGd/BmoKoMWuDpI
+/k4+oKsGGelT84ATB+0tvz8KPFUgOSwsAGl0lUq8ILKpeeUYiZGo3BxN77t+Nwtd/jmliFKMAGzs
+GHxBvfaLdXe6YJ2E5/4tAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEG
+MB0GA1UdDgQWBBQVOIMPPyw/cDMezUb+B4wg4NfDtzANBgkqhkiG9w0BAQUFAAOCAQEAX/FBfXxc
+CLkr4NWSR/pnXKUTwwMhmytMiUbPWU3J/qVAtmPN3XEolWcRzCSs00Rsca4BIGsDoo8Ytyk6feUW
+YFN4PMCvFYP3j1IzJL1kk5fui/fbGKhtcbP3LBfQdCVp9/5rPJS+TUtBjE7ic9DjkCJzQ83z7+pz
+zkWKsKZJ/0x9nXGIxHYdkFsd7v3M9+79YKWxehZx0RbQfBI8bGmX265fOZpwLwU8GUYEmSA20GBu
+YQa7FkKMcPcw++DbZqMAAb3mLNqRX6BGi01qnD093QVG/na/oAo85ADmJ7f/hC3euiInlhBx6yLt
+398znM/jra6O1I7mT1GvFpLgXPYHDw==
+-----END CERTIFICATE-----
+
+Certum Root CA
+==============
+-----BEGIN CERTIFICATE-----
+MIIDDDCCAfSgAwIBAgIDAQAgMA0GCSqGSIb3DQEBBQUAMD4xCzAJBgNVBAYTAlBMMRswGQYDVQQK
+ExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBDQTAeFw0wMjA2MTExMDQ2Mzla
+Fw0yNzA2MTExMDQ2MzlaMD4xCzAJBgNVBAYTAlBMMRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8u
+by4xEjAQBgNVBAMTCUNlcnR1bSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM6x
+wS7TT3zNJc4YPk/EjG+AanPIW1H4m9LcuwBcsaD8dQPugfCI7iNS6eYVM42sLQnFdvkrOYCJ5JdL
+kKWoePhzQ3ukYbDYWMzhbGZ+nPMJXlVjhNWo7/OxLjBos8Q82KxujZlakE403Daaj4GIULdtlkIJ
+89eVgw1BS7Bqa/j8D35in2fE7SZfECYPCE/wpFcozo+47UX2bu4lXapuOb7kky/ZR6By6/qmW6/K
+Uz/iDsaWVhFu9+lmqSbYf5VT7QqFiLpPKaVCjF62/IUgAKpoC6EahQGcxEZjgoi2IrHu/qpGWX7P
+NSzVttpd90gzFFS269lvzs2I1qsb2pY7HVkCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkq
+hkiG9w0BAQUFAAOCAQEAuI3O7+cUus/usESSbLQ5PqKEbq24IXfS1HeCh+YgQYHu4vgRt2PRFze+
+GXYkHAQaTOs9qmdvLdTN/mUxcMUbpgIKumB7bVjCmkn+YzILa+M6wKyrO7Do0wlRjBCDxjTgxSvg
+GrZgFCdsMneMvLJymM/NzD+5yCRCFNZX/OYmQ6kd5YCQzgNUKD73P9P4Te1qCjqTE5s7FCMTY5w/
+0YcneeVMUeMBrYVdGjux1XMQpNPyvG5k9VpWkKjHDkx0Dy5xO/fIR/RpbxXyEV6DHpx8Uq79AtoS
+qFlnGNu8cN2bsWntgM6JQEhqDjXKKWYVIZQs6GAqm4VKQPNriiTsBhYscw==
+-----END CERTIFICATE-----
+
+Comodo AAA Services root
+========================
+-----BEGIN CERTIFICATE-----
+MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEbMBkGA1UECAwS
+R3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0Eg
+TGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAw
+MFoXDTI4MTIzMTIzNTk1OVowezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hl
+c3RlcjEQMA4GA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV
+BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
+ggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQuaBtDFcCLNSS1UY8y2bmhG
+C1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe3M/vg4aijJRPn2jymJBGhCfHdr/jzDUs
+i14HZGWCwEiwqJH5YZ92IFCokcdmtet4YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszW
+Y19zjNoFmag4qMsXeDZRrOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjH
+Ypy+g8cmez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQUoBEK
+Iz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wewYDVR0f
+BHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNl
+cy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29tb2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2Vz
+LmNybDANBgkqhkiG9w0BAQUFAAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm
+7l3sAg9g1o1QGE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz
+Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2G9w84FoVxp7Z
+8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsil2D4kF501KKaU73yqWjgom7C
+12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg==
+-----END CERTIFICATE-----
+
+Comodo Secure Services root
+===========================
+-----BEGIN CERTIFICATE-----
+MIIEPzCCAyegAwIBAgIBATANBgkqhkiG9w0BAQUFADB+MQswCQYDVQQGEwJHQjEbMBkGA1UECAwS
+R3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0Eg
+TGltaXRlZDEkMCIGA1UEAwwbU2VjdXJlIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAw
+MDAwMFoXDTI4MTIzMTIzNTk1OVowfjELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFu
+Y2hlc3RlcjEQMA4GA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxJDAi
+BgNVBAMMG1NlY3VyZSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP
+ADCCAQoCggEBAMBxM4KK0HDrc4eCQNUd5MvJDkKQ+d40uaG6EfQlhfPMcm3ye5drswfxdySRXyWP
+9nQ95IDC+DwN879A6vfIUtFyb+/Iq0G4bi4XKpVpDM3SHpR7LZQdqnXXs5jLrLxkU0C8j6ysNstc
+rbvd4JQX7NFc0L/vpZXJkMWwrPsbQ996CF23uPJAGysnnlDOXmWCiIxe004MeuoIkbY2qitC++rC
+oznl2yY4rYsK7hljxxwk3wN42ubqwUcaCwtGCd0C/N7Lh1/XMGNooa7cMqG6vv5Eq2i2pRcV/b3V
+p6ea5EQz6YiO/O1R65NxTq0B50SOqy3LqP4BSUjwwN3HaNiS/j0CAwEAAaOBxzCBxDAdBgNVHQ4E
+FgQUPNiTiMLAggnMAZkGkyDpnnAJY08wDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8w
+gYEGA1UdHwR6MHgwO6A5oDeGNWh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL1NlY3VyZUNlcnRpZmlj
+YXRlU2VydmljZXMuY3JsMDmgN6A1hjNodHRwOi8vY3JsLmNvbW9kby5uZXQvU2VjdXJlQ2VydGlm
+aWNhdGVTZXJ2aWNlcy5jcmwwDQYJKoZIhvcNAQEFBQADggEBAIcBbSMdflsXfcFhMs+P5/OKlFlm
+4J4oqF7Tt/Q05qo5spcWxYJvMqTpjOev/e/C6LlLqqP05tqNZSH7uoDrJiiFGv45jN5bBAS0VPmj
+Z55B+glSzAVIqMk/IQQezkhr/IXownuvf7fM+F86/TXGDe+X3EyrEeFryzHRbPtIgKvcnDe4IRRL
+DXE97IMzbtFuMhbsmMcWi1mmNKsFVy2T96oTy9IT4rcuO81rUBcJaD61JlfutuC23bkpgHl9j6Pw
+pCikFcSF9CfUa7/lXORlAnZUtOM3ZiTTGWHIUhDlizeauan5Hb/qmZJhlv8BzaFfDbxxvA6sCx1H
+RR3B7Hzs/Sk=
+-----END CERTIFICATE-----
+
+Comodo Trusted Services root
+============================
+-----BEGIN CERTIFICATE-----
+MIIEQzCCAyugAwIBAgIBATANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJHQjEbMBkGA1UECAwS
+R3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0Eg
+TGltaXRlZDElMCMGA1UEAwwcVHJ1c3RlZCBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0wNDAxMDEw
+MDAwMDBaFw0yODEyMzEyMzU5NTlaMH8xCzAJBgNVBAYTAkdCMRswGQYDVQQIDBJHcmVhdGVyIE1h
+bmNoZXN0ZXIxEDAOBgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoMEUNvbW9kbyBDQSBMaW1pdGVkMSUw
+IwYDVQQDDBxUcnVzdGVkIENlcnRpZmljYXRlIFNlcnZpY2VzMIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEA33FvNlhTWvI2VFeAxHQIIO0Yfyod5jWaHiWsnOWWfnJSoBVC21ndZHoa0Lh7
+3TkVvFVIxO06AOoxEbrycXQaZ7jPM8yoMa+j49d/vzMtTGo87IvDktJTdyR0nAducPy9C1t2ul/y
+/9c3S0pgePfw+spwtOpZqqPOSC+pw7ILfhdyFgymBwwbOM/JYrc/oJOlh0Hyt3BAd9i+FHzjqMB6
+juljatEPmsbS9Is6FARW1O24zG71++IsWL1/T2sr92AkWCTOJu80kTrV44HQsvAEAtdbtz6SrGsS
+ivnkBbA7kUlcsutT6vifR4buv5XAwAaf0lteERv0xwQ1KdJVXOTt6wIDAQABo4HJMIHGMB0GA1Ud
+DgQWBBTFe1i97doladL3WRaoszLAeydb9DAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB
+/zCBgwYDVR0fBHwwejA8oDqgOIY2aHR0cDovL2NybC5jb21vZG9jYS5jb20vVHJ1c3RlZENlcnRp
+ZmljYXRlU2VydmljZXMuY3JsMDqgOKA2hjRodHRwOi8vY3JsLmNvbW9kby5uZXQvVHJ1c3RlZENl
+cnRpZmljYXRlU2VydmljZXMuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQDIk4E7ibSvuIQSTI3S8Ntw
+uleGFTQQuS9/HrCoiWChisJ3DFBKmwCL2Iv0QeLQg4pKHBQGsKNoBXAxMKdTmw7pSqBYaWcOrp32
+pSxBvzwGa+RZzG0Q8ZZvH9/0BAKkn0U+yNj6NkZEUD+Cl5EfKNsYEYwq5GWDVxISjBc/lDb+XbDA
+BHcTuPQV1T84zJQ6VdCsmPW6AF/ghhmBeC8owH7TzEIK9a5QoNE+xqFx7D+gIIxmOom0jtTYsU0l
+R+4viMi14QVFwL4Ucd56/Y57fU0IlqUSc/AtyjcndBInTMu2l+nZrghtWjlA3QVHdWpaIbOjGM9O
+9y5Xt5hwXsjEeLBi
+-----END CERTIFICATE-----
+
+QuoVadis Root CA
+================
+-----BEGIN CERTIFICATE-----
+MIIF0DCCBLigAwIBAgIEOrZQizANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJCTTEZMBcGA1UE
+ChMQUXVvVmFkaXMgTGltaXRlZDElMCMGA1UECxMcUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0
+eTEuMCwGA1UEAxMlUXVvVmFkaXMgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMTAz
+MTkxODMzMzNaFw0yMTAzMTcxODMzMzNaMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRp
+cyBMaW1pdGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYDVQQD
+EyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEF
+AAOCAQ8AMIIBCgKCAQEAv2G1lVO6V/z68mcLOhrfEYBklbTRvM16z/Ypli4kVEAkOPcahdxYTMuk
+J0KX0J+DisPkBgNbAKVRHnAEdOLB1Dqr1607BxgFjv2DrOpm2RgbaIr1VxqYuvXtdj182d6UajtL
+F8HVj71lODqV0D1VNk7feVcxKh7YWWVJWCCYfqtffp/p1k3sg3Spx2zY7ilKhSoGFPlU5tPaZQeL
+YzcS19Dsw3sgQUSj7cugF+FxZc4dZjH3dgEZyH0DWLaVSR2mEiboxgx24ONmy+pdpibu5cxfvWen
+AScOospUxbF6lR1xHkopigPcakXBpBlebzbNw6Kwt/5cOOJSvPhEQ+aQuwIDAQABo4ICUjCCAk4w
+PQYIKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwczovL29jc3AucXVvdmFkaXNvZmZzaG9y
+ZS5jb20wDwYDVR0TAQH/BAUwAwEB/zCCARoGA1UdIASCAREwggENMIIBCQYJKwYBBAG+WAABMIH7
+MIHUBggrBgEFBQcCAjCBxxqBxFJlbGlhbmNlIG9uIHRoZSBRdW9WYWRpcyBSb290IENlcnRpZmlj
+YXRlIGJ5IGFueSBwYXJ0eSBhc3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJs
+ZSBzdGFuZGFyZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRpb24gcHJh
+Y3RpY2VzLCBhbmQgdGhlIFF1b1ZhZGlzIENlcnRpZmljYXRlIFBvbGljeS4wIgYIKwYBBQUHAgEW
+Fmh0dHA6Ly93d3cucXVvdmFkaXMuYm0wHQYDVR0OBBYEFItLbe3TKbkGGew5Oanwl4Rqy+/fMIGu
+BgNVHSMEgaYwgaOAFItLbe3TKbkGGew5Oanwl4Rqy+/foYGEpIGBMH8xCzAJBgNVBAYTAkJNMRkw
+FwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0
+aG9yaXR5MS4wLAYDVQQDEyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggQ6
+tlCLMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAitQUtf70mpKnGdSkfnIYj9lo
+fFIk3WdvOXrEql494liwTXCYhGHoG+NpGA7O+0dQoE7/8CQfvbLO9Sf87C9TqnN7Az10buYWnuul
+LsS/VidQK2K6vkscPFVcQR0kvoIgR13VRH56FmjffU1RcHhXHTMe/QKZnAzNCgVPx7uOpHX6Sm2x
+gI4JVrmcGmD+XcHXetwReNDWXcG31a0ymQM6isxUJTkxgXsTIlG6Rmyhu576BGxJJnSP0nPrzDCi
+5upZIof4l/UO/erMkqQWxFIY6iHOsfHmhIHluqmGKPJDWl0Snawe2ajlCmqnf6CHKc/yiU3U7MXi
+5nrQNiOKSnQ2+Q==
+-----END CERTIFICATE-----
+
+QuoVadis Root CA 2
+==================
+-----BEGIN CERTIFICATE-----
+MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoT
+EFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMjAeFw0wNjExMjQx
+ODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM
+aW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4IC
+DwAwggIKAoICAQCaGMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6
+XJxgFyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55JWpzmM+Yk
+lvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bBrrcCaoF6qUWD4gXmuVbB
+lDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp+ARz8un+XJiM9XOva7R+zdRcAitMOeGy
+lZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt
+66/3FsvbzSUr5R/7mp/iUcw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1Jdxn
+wQ5hYIizPtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og/zOh
+D7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UHoycR7hYQe7xFSkyy
+BNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuIyV77zGHcizN300QyNQliBJIWENie
+J0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1Ud
+DgQWBBQahGK8SEwzJQTU7tD2A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGU
+a6FJpEcwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT
+ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2fBluornFdLwUv
+Z+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzng/iN/Ae42l9NLmeyhP3ZRPx3
+UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2BlfF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodm
+VjB3pjd4M1IQWK4/YY7yarHvGH5KWWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK
++JDSV6IZUaUtl0HaB0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrW
+IozchLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPRTUIZ3Ph1
+WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWDmbA4CD/pXvk1B+TJYm5X
+f6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0ZohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II
+4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8
+VCLAAVBpQ570su9t+Oza8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u
+-----END CERTIFICATE-----
+
+QuoVadis Root CA 3
+==================
+-----BEGIN CERTIFICATE-----
+MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoT
+EFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMzAeFw0wNjExMjQx
+OTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM
+aW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4IC
+DwAwggIKAoICAQDMV0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNgg
+DhoB4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUrH556VOij
+KTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd8lyyBTNvijbO0BNO/79K
+DDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9CabwvvWhDFlaJKjdhkf2mrk7AyxRllDdLkgbv
+BNDInIjbC3uBr7E9KsRlOni27tyAsdLTmZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwp
+p5ijJUMv7/FfJuGITfhebtfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8
+nT8KKdjcT5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDtWAEX
+MJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZc6tsgLjoC2SToJyM
+Gf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A4iLItLRkT9a6fUg+qGkM17uGcclz
+uD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYDVR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHT
+BgkrBgEEAb5YAAMwgcUwgZMGCCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmlj
+YXRlIGNvbnN0aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0
+aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVudC4wLQYIKwYB
+BQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2NwczALBgNVHQ8EBAMCAQYwHQYD
+VR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4GA1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4
+ywLQoUmkRzBFMQswCQYDVQQGEwJCTTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UE
+AxMSUXVvVmFkaXMgUm9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZV
+qyM07ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSemd1o417+s
+hvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd+LJ2w/w4E6oM3kJpK27z
+POuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2
+Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadNt54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp
+8kokUvd0/bpO5qgdAm6xDYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBC
+bjPsMZ57k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6szHXu
+g/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0jWy10QJLZYxkNc91p
+vGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeTmJlglFwjz1onl14LBQaTNx47aTbr
+qZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK4SVhM7JZG+Ju1zdXtg2pEto=
+-----END CERTIFICATE-----
+
+Security Communication Root CA
+==============================
+-----BEGIN CERTIFICATE-----
+MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMP
+U0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEw
+HhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMP
+U0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEw
+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw
+8yl89f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJDKaVv0uM
+DPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9Ms+k2Y7CI9eNqPPYJayX
+5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/NQV3Is00qVUarH9oe4kA92819uZKAnDfd
+DJZkndwi92SL32HeFZRSFaB9UslLqCHJxrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2
+JChzAgMBAAGjPzA9MB0GA1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYw
+DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vGkl3g
+0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfrUj94nK9NrvjVT8+a
+mCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5Bw+SUEmK3TGXX8npN6o7WWWXlDLJ
+s58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJUJRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ
+6rBK+1YWc26sTfcioU+tHXotRSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAi
+FL39vmwLAw==
+-----END CERTIFICATE-----
+
+Sonera Class 1 Root CA
+======================
+-----BEGIN CERTIFICATE-----
+MIIDIDCCAgigAwIBAgIBJDANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEPMA0GA1UEChMG
+U29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MxIENBMB4XDTAxMDQwNjEwNDkxM1oXDTIxMDQw
+NjEwNDkxM1owOTELMAkGA1UEBhMCRkkxDzANBgNVBAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJh
+IENsYXNzMSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALWJHytPZwp5/8Ue+H88
+7dF+2rDNbS82rDTG29lkFwhjMDMiikzujrsPDUJVyZ0upe/3p4zDq7mXy47vPxVnqIJyY1MPQYx9
+EJUkoVqlBvqSV536pQHydekfvFYmUk54GWVYVQNYwBSujHxVX3BbdyMGNpfzJLWaRpXk3w0LBUXl
+0fIdgrvGE+D+qnr9aTCU89JFhfzyMlsy3uhsXR/LpCJ0sICOXZT3BgBLqdReLjVQCfOAl/QMF645
+2F/NM8EcyonCIvdFEu1eEpOdY6uCLrnrQkFEy0oaAIINnvmLVz5MxxftLItyM19yejhW1ebZrgUa
+HXVFsculJRwSVzb9IjcCAwEAAaMzMDEwDwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQIR+IMi/ZT
+iFIwCwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQCLGrLJXWG04bkruVPRsoWdd44W7hE9
+28Jj2VuXZfsSZ9gqXLar5V7DtxYvyOirHYr9qxp81V9jz9yw3Xe5qObSIjiHBxTZ/75Wtf0HDjxV
+yhbMp6Z3N/vbXB9OWQaHowND9Rart4S9Tu+fMTfwRvFAttEMpWT4Y14h21VOTzF2nBBhjrZTOqMR
+vq9tfB69ri3iDGnHhVNoomG6xT60eVR4ngrHAr5i0RGCS2UvkVrCqIexVmiUefkl98HVrhq4uz2P
+qYo4Ffdz0Fpg0YCw8NzVUM1O7pJIae2yIx4wzMiUyLb1O4Z/P6Yun/Y+LLWSlj7fLJOK/4GMDw9Z
+IRlXvVWa
+-----END CERTIFICATE-----
+
+Sonera Class 2 Root CA
+======================
+-----BEGIN CERTIFICATE-----
+MIIDIDCCAgigAwIBAgIBHTANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEPMA0GA1UEChMG
+U29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MyIENBMB4XDTAxMDQwNjA3Mjk0MFoXDTIxMDQw
+NjA3Mjk0MFowOTELMAkGA1UEBhMCRkkxDzANBgNVBAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJh
+IENsYXNzMiBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJAXSjWdyvANlsdE+hY3
+/Ei9vX+ALTU74W+oZ6m/AxxNjG8yR9VBaKQTBME1DJqEQ/xcHf+Js+gXGM2RX/uJ4+q/Tl18GybT
+dXnt5oTjV+WtKcT0OijnpXuENmmz/V52vaMtmdOQTiMofRhj8VQ7Jp12W5dCsv+u8E7s3TmVToMG
+f+dJQMjFAbJUWmYdPfz56TwKnoG4cPABi+QjVHzIrviQHgCWctRUz2EjvOr7nQKV0ba5cTppCD8P
+tOFCx4j1P5iop7oc4HFx71hXgVB6XGt0Rg6DA5jDjqhu8nYybieDwnPz3BjotJPqdURrBGAgcVeH
+nfO+oJAjPYok4doh28MCAwEAAaMzMDEwDwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQISqCqWITT
+XjwwCwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQBazof5FnIVV0sd2ZvnoiYw7JNn39Yt
+0jSv9zilzqsWuasvfDXLrNAPtEwr/IDva4yRXzZ299uzGxnq9LIR/WFxRL8oszodv7ND6J+/3DEI
+cbCdjdY0RzKQxmUk96BKfARzjzlvF4xytb1LyHr4e4PDKE6cCepnP7JnBBvDFNr450kkkdAdavph
+Oe9r5yF1BgfYErQhIHBCcYHaPJo2vqZbDWpsmh+Re/n570K6Tk6ezAyNlNzZRZxe7EJQY670XcSx
+EtzKO6gunRRaBXW37Ndj4ro1tgQIkejanZz2ZrUYrAqmVCY0M9IbwdR/GjqOC6oybtv8TyWf2TLH
+llpwrN9M
+-----END CERTIFICATE-----
+
+Staat der Nederlanden Root CA
+=============================
+-----BEGIN CERTIFICATE-----
+MIIDujCCAqKgAwIBAgIEAJiWijANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQGEwJOTDEeMBwGA1UE
+ChMVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSYwJAYDVQQDEx1TdGFhdCBkZXIgTmVkZXJsYW5kZW4g
+Um9vdCBDQTAeFw0wMjEyMTcwOTIzNDlaFw0xNTEyMTYwOTE1MzhaMFUxCzAJBgNVBAYTAk5MMR4w
+HAYDVQQKExVTdGFhdCBkZXIgTmVkZXJsYW5kZW4xJjAkBgNVBAMTHVN0YWF0IGRlciBOZWRlcmxh
+bmRlbiBSb290IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmNK1URF6gaYUmHFt
+vsznExvWJw56s2oYHLZhWtVhCb/ekBPHZ+7d89rFDBKeNVU+LCeIQGv33N0iYfXCxw719tV2U02P
+jLwYdjeFnejKScfST5gTCaI+Ioicf9byEGW07l8Y1Rfj+MX94p2i71MOhXeiD+EwR+4A5zN9RGca
+C1Hoi6CeUJhoNFIfLm0B8mBF8jHrqTFoKbt6QZ7GGX+UtFE5A3+y3qcym7RHjm+0Sq7lr7HcsBth
+vJly3uSJt3omXdozSVtSnA71iq3DuD3oBmrC1SoLbHuEvVYFy4ZlkuxEK7COudxwC0barbxjiDn6
+22r+I/q85Ej0ZytqERAhSQIDAQABo4GRMIGOMAwGA1UdEwQFMAMBAf8wTwYDVR0gBEgwRjBEBgRV
+HSAAMDwwOgYIKwYBBQUHAgEWLmh0dHA6Ly93d3cucGtpb3ZlcmhlaWQubmwvcG9saWNpZXMvcm9v
+dC1wb2xpY3kwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSofeu8Y6R0E3QA7Jbg0zTBLL9s+DAN
+BgkqhkiG9w0BAQUFAAOCAQEABYSHVXQ2YcG70dTGFagTtJ+k/rvuFbQvBgwp8qiSpGEN/KtcCFtR
+EytNwiphyPgJWPwtArI5fZlmgb9uXJVFIGzmeafR2Bwp/MIgJ1HI8XxdNGdphREwxgDS1/PTfLbw
+MVcoEoJz6TMvplW0C5GUR5z6u3pCMuiufi3IvKwUv9kP2Vv8wfl6leF9fpb8cbDCTMjfRTTJzg3y
+nGQI0DvDKcWy7ZAEwbEpkcUwb8GpcjPM/l0WFywRaed+/sWDCN+83CI6LiBpIzlWYGeQiy52OfsR
+iJf2fL1LuCAWZwWN4jvBcj+UlTfHXbme2JOhF4//DGYVwSR8MnwDHTuhWEUykw==
+-----END CERTIFICATE-----
+
+TDC Internet Root CA
+====================
+-----BEGIN CERTIFICATE-----
+MIIEKzCCAxOgAwIBAgIEOsylTDANBgkqhkiG9w0BAQUFADBDMQswCQYDVQQGEwJESzEVMBMGA1UE
+ChMMVERDIEludGVybmV0MR0wGwYDVQQLExRUREMgSW50ZXJuZXQgUm9vdCBDQTAeFw0wMTA0MDUx
+NjMzMTdaFw0yMTA0MDUxNzAzMTdaMEMxCzAJBgNVBAYTAkRLMRUwEwYDVQQKEwxUREMgSW50ZXJu
+ZXQxHTAbBgNVBAsTFFREQyBJbnRlcm5ldCBSb290IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEAxLhAvJHVYx/XmaCLDEAedLdInUaMArLgJF/wGROnN4NrXceO+YQwzho7+vvOi20j
+xsNuZp+Jpd/gQlBn+h9sHvTQBda/ytZO5GhgbEaqHF1j4QeGDmUApy6mcca8uYGoOn0a0vnRrEvL
+znWv3Hv6gXPU/Lq9QYjUdLP5Xjg6PEOo0pVOd20TDJ2PeAG3WiAfAzc14izbSysseLlJ28TQx5yc
+5IogCSEWVmb/Bexb4/DPqyQkXsN/cHoSxNK1EKC2IeGNeGlVRGn1ypYcNIUXJXfi9i8nmHj9eQY6
+otZaQ8H/7AQ77hPv01ha/5Lr7K7a8jcDR0G2l8ktCkEiu7vmpwIDAQABo4IBJTCCASEwEQYJYIZI
+AYb4QgEBBAQDAgAHMGUGA1UdHwReMFwwWqBYoFakVDBSMQswCQYDVQQGEwJESzEVMBMGA1UEChMM
+VERDIEludGVybmV0MR0wGwYDVQQLExRUREMgSW50ZXJuZXQgUm9vdCBDQTENMAsGA1UEAxMEQ1JM
+MTArBgNVHRAEJDAigA8yMDAxMDQwNTE2MzMxN1qBDzIwMjEwNDA1MTcwMzE3WjALBgNVHQ8EBAMC
+AQYwHwYDVR0jBBgwFoAUbGQBx/2FbazI2p5QCIUItTxWqFAwHQYDVR0OBBYEFGxkAcf9hW2syNqe
+UAiFCLU8VqhQMAwGA1UdEwQFMAMBAf8wHQYJKoZIhvZ9B0EABBAwDhsIVjUuMDo0LjADAgSQMA0G
+CSqGSIb3DQEBBQUAA4IBAQBOQ8zR3R0QGwZ/t6T609lN+yOfI1Rb5osvBCiLtSdtiaHsmGnc540m
+gwV5dOy0uaOXwTUA/RXaOYE6lTGQ3pfphqiZdwzlWqCE/xIWrG64jcN7ksKsLtB9KOy282A4aW8+
+2ARVPp7MVdK6/rtHBNcK2RYKNCn1WBPVT8+PVkuzHu7TmHnaCB4Mb7j4Fifvwm899qNLPg7kbWzb
+O0ESm70NRyN/PErQr8Cv9u8btRXE64PECV90i9kR+8JWsTz4cMo0jUNAE4z9mQNUecYu6oah9jrU
+Cbz0vGbMPVjQV0kK7iXiQe4T+Zs4NNEA9X7nlB38aQNiuJkFBT1reBK9sG9l
+-----END CERTIFICATE-----
+
+TDC OCES Root CA
+================
+-----BEGIN CERTIFICATE-----
+MIIFGTCCBAGgAwIBAgIEPki9xDANBgkqhkiG9w0BAQUFADAxMQswCQYDVQQGEwJESzEMMAoGA1UE
+ChMDVERDMRQwEgYDVQQDEwtUREMgT0NFUyBDQTAeFw0wMzAyMTEwODM5MzBaFw0zNzAyMTEwOTA5
+MzBaMDExCzAJBgNVBAYTAkRLMQwwCgYDVQQKEwNUREMxFDASBgNVBAMTC1REQyBPQ0VTIENBMIIB
+IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArGL2YSCyz8DGhdfjeebM7fI5kqSXLmSjhFuH
+nEz9pPPEXyG9VhDr2y5h7JNp46PMvZnDBfwGuMo2HP6QjklMxFaaL1a8z3sM8W9Hpg1DTeLpHTk0
+zY0s2RKY+ePhwUp8hjjEqcRhiNJerxomTdXkoCJHhNlktxmW/OwZ5LKXJk5KTMuPJItUGBxIYXvV
+iGjaXbXqzRowwYCDdlCqT9HU3Tjw7xb04QxQBr/q+3pJoSgrHPb8FTKjdGqPqcNiKXEx5TukYBde
+dObaE+3pHx8b0bJoc8YQNHVGEBDjkAB2QMuLt0MJIf+rTpPGWOmlgtt3xDqZsXKVSQTwtyv6e1mO
+3QIDAQABo4ICNzCCAjMwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwgewGA1UdIASB
+5DCB4TCB3gYIKoFQgSkBAQEwgdEwLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuY2VydGlmaWthdC5k
+ay9yZXBvc2l0b3J5MIGdBggrBgEFBQcCAjCBkDAKFgNUREMwAwIBARqBgUNlcnRpZmlrYXRlciBm
+cmEgZGVubmUgQ0EgdWRzdGVkZXMgdW5kZXIgT0lEIDEuMi4yMDguMTY5LjEuMS4xLiBDZXJ0aWZp
+Y2F0ZXMgZnJvbSB0aGlzIENBIGFyZSBpc3N1ZWQgdW5kZXIgT0lEIDEuMi4yMDguMTY5LjEuMS4x
+LjARBglghkgBhvhCAQEEBAMCAAcwgYEGA1UdHwR6MHgwSKBGoESkQjBAMQswCQYDVQQGEwJESzEM
+MAoGA1UEChMDVERDMRQwEgYDVQQDEwtUREMgT0NFUyBDQTENMAsGA1UEAxMEQ1JMMTAsoCqgKIYm
+aHR0cDovL2NybC5vY2VzLmNlcnRpZmlrYXQuZGsvb2Nlcy5jcmwwKwYDVR0QBCQwIoAPMjAwMzAy
+MTEwODM5MzBagQ8yMDM3MDIxMTA5MDkzMFowHwYDVR0jBBgwFoAUYLWF7FZkfhIZJ2cdUBVLc647
++RIwHQYDVR0OBBYEFGC1hexWZH4SGSdnHVAVS3OuO/kSMB0GCSqGSIb2fQdBAAQQMA4bCFY2LjA6
+NC4wAwIEkDANBgkqhkiG9w0BAQUFAAOCAQEACromJkbTc6gJ82sLMJn9iuFXehHTuJTXCRBuo7E4
+A9G28kNBKWKnctj7fAXmMXAnVBhOinxO5dHKjHiIzxvTkIvmI/gLDjNDfZziChmPyQE+dF10yYsc
+A+UYyAFMP8uXBV2YcaaYb7Z8vTd/vuGTJW1v8AqtFxjhA7wHKcitJuj4YfD9IQl+mo6paH1IYnK9
+AOoBmbgGglGBTvH1tJFUuSN6AJqfXY3gPGS5GhKSKseCRHI53OI8xthV9RVOyAUO28bQYqbsFbS1
+AoLbrIyigfCbmTH1ICCoiGEKB5+U/NDXG8wuF/MEJ3Zn61SD/aSQfgY9BKNDLdr8C2LqL19iUw==
+-----END CERTIFICATE-----
+
+UTN DATACorp SGC Root CA
+========================
+-----BEGIN CERTIFICATE-----
+MIIEXjCCA0agAwIBAgIQRL4Mi1AAIbQR0ypoBqmtaTANBgkqhkiG9w0BAQUFADCBkzELMAkGA1UE
+BhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEeMBwGA1UEChMVVGhl
+IFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xGzAZ
+BgNVBAMTElVUTiAtIERBVEFDb3JwIFNHQzAeFw05OTA2MjQxODU3MjFaFw0xOTA2MjQxOTA2MzBa
+MIGTMQswCQYDVQQGEwJVUzELMAkGA1UECBMCVVQxFzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5MR4w
+HAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxITAfBgNVBAsTGGh0dHA6Ly93d3cudXNlcnRy
+dXN0LmNvbTEbMBkGA1UEAxMSVVROIC0gREFUQUNvcnAgU0dDMIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEA3+5YEKIrblXEjr8uRgnn4AgPLit6E5Qbvfa2gI5lBZMAHryv4g+OGQ0SR+ys
+raP6LnD43m77VkIVni5c7yPeIbkFdicZD0/Ww5y0vpQZY/KmEQrrU0icvvIpOxboGqBMpsn0GFlo
+wHDyUwDAXlCCpVZvNvlK4ESGoE1O1kduSUrLZ9emxAW5jh70/P/N5zbgnAVssjMiFdC04MwXwLLA
+9P4yPykqlXvY8qdOD1R8oQ2AswkDwf9c3V6aPryuvEeKaq5xyh+xKrhfQgUL7EYw0XILyulWbfXv
+33i+Ybqypa4ETLyorGkVl73v67SMvzX41MPRKA5cOp9wGDMgd8SirwIDAQABo4GrMIGoMAsGA1Ud
+DwQEAwIBxjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRTMtGzz3/64PGgXYVOktKeRR20TzA9
+BgNVHR8ENjA0MDKgMKAuhixodHRwOi8vY3JsLnVzZXJ0cnVzdC5jb20vVVROLURBVEFDb3JwU0dD
+LmNybDAqBgNVHSUEIzAhBggrBgEFBQcDAQYKKwYBBAGCNwoDAwYJYIZIAYb4QgQBMA0GCSqGSIb3
+DQEBBQUAA4IBAQAnNZcAiosovcYzMB4p/OL31ZjUQLtgyr+rFywJNn9Q+kHcrpY6CiM+iVnJowft
+Gzet/Hy+UUla3joKVAgWRcKZsYfNjGjgaQPpxE6YsjuMFrMOoAyYUJuTqXAJyCyjj98C5OBxOvG0
+I3KgqgHf35g+FFCgMSa9KOlaMCZ1+XtgHI3zzVAmbQQnmt/VDUVHKWss5nbZqSl9Mt3JNjy9rjXx
+EZ4du5A/EkdOjtd+D2JzHVImOBwYSf0wdJrE5SIv2MCN7ZF6TACPcn9d2t0bi0Vr591pl6jFVkwP
+DPafepE39peC4N1xaf92P2BNPM/3mfnGV/TJVTl4uix5yaaIK/QI
+-----END CERTIFICATE-----
+
+UTN USERFirst Email Root CA
+===========================
+-----BEGIN CERTIFICATE-----
+MIIEojCCA4qgAwIBAgIQRL4Mi1AAJLQR0zYlJWfJiTANBgkqhkiG9w0BAQUFADCBrjELMAkGA1UE
+BhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEeMBwGA1UEChMVVGhl
+IFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xNjA0
+BgNVBAMTLVVUTi1VU0VSRmlyc3QtQ2xpZW50IEF1dGhlbnRpY2F0aW9uIGFuZCBFbWFpbDAeFw05
+OTA3MDkxNzI4NTBaFw0xOTA3MDkxNzM2NThaMIGuMQswCQYDVQQGEwJVUzELMAkGA1UECBMCVVQx
+FzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsx
+ITAfBgNVBAsTGGh0dHA6Ly93d3cudXNlcnRydXN0LmNvbTE2MDQGA1UEAxMtVVROLVVTRVJGaXJz
+dC1DbGllbnQgQXV0aGVudGljYXRpb24gYW5kIEVtYWlsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEAsjmFpPJ9q0E7YkY3rs3BYHW8OWX5ShpHornMSMxqmNVNNRm5pELlzkniii8efNIx
+B8dOtINknS4p1aJkxIW9hVE1eaROaJB7HHqkkqgX8pgV8pPMyaQylbsMTzC9mKALi+VuG6JG+ni8
+om+rWV6lL8/K2m2qL+usobNqqrcuZzWLeeEeaYji5kbNoKXqvgvOdjp6Dpvq/NonWz1zHyLmSGHG
+TPNpsaguG7bUMSAsvIKKjqQOpdeJQ/wWWq8dcdcRWdq6hw2v+vPhwvCkxWeM1tZUOt4KpLoDd7Nl
+yP0e03RiqhjKaJMeoYV+9Udly/hNVyh00jT/MLbu9mIwFIws6wIDAQABo4G5MIG2MAsGA1UdDwQE
+AwIBxjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSJgmd9xJ0mcABLtFBIfN49rgRufTBYBgNV
+HR8EUTBPME2gS6BJhkdodHRwOi8vY3JsLnVzZXJ0cnVzdC5jb20vVVROLVVTRVJGaXJzdC1DbGll
+bnRBdXRoZW50aWNhdGlvbmFuZEVtYWlsLmNybDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH
+AwQwDQYJKoZIhvcNAQEFBQADggEBALFtYV2mGn98q0rkMPxTbyUkxsrt4jFcKw7u7mFVbwQ+zzne
+xRtJlOTrIEy05p5QLnLZjfWqo7NK2lYcYJeA3IKirUq9iiv/Cwm0xtcgBEXkzYABurorbs6q15L+
+5K/r9CYdFip/bDCVNy8zEqx/3cfREYxRmLLQo5HQrfafnoOTHh1CuEava2bwm3/q4wMC5QJRwarV
+NZ1yQAOJujEdxRBoUp7fooXFXAimeOZTT7Hot9MUnpOmw2TjrH5xzbyf6QMbzPvprDHBr3wVdAKZ
+w7JHpsIyYdfHb0gkUSeh1YdV8nuPmD0Wnu51tvjQjvLzxq4oW6fw8zYX/MMF08oDSlQ=
+-----END CERTIFICATE-----
+
+UTN USERFirst Hardware Root CA
+==============================
+-----BEGIN CERTIFICATE-----
+MIIEdDCCA1ygAwIBAgIQRL4Mi1AAJLQR0zYq/mUK/TANBgkqhkiG9w0BAQUFADCBlzELMAkGA1UE
+BhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEeMBwGA1UEChMVVGhl
+IFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHzAd
+BgNVBAMTFlVUTi1VU0VSRmlyc3QtSGFyZHdhcmUwHhcNOTkwNzA5MTgxMDQyWhcNMTkwNzA5MTgx
+OTIyWjCBlzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0
+eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8vd3d3LnVz
+ZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3QtSGFyZHdhcmUwggEiMA0GCSqGSIb3
+DQEBAQUAA4IBDwAwggEKAoIBAQCx98M4P7Sof885glFn0G2f0v9Y8+efK+wNiVSZuTiZFvfgIXlI
+wrthdBKWHTxqctU8EGc6Oe0rE81m65UJM6Rsl7HoxuzBdXmcRl6Nq9Bq/bkqVRcQVLMZ8Jr28bFd
+tqdt++BxF2uiiPsA3/4aMXcMmgF6sTLjKwEHOG7DpV4jvEWbe1DByTCP2+UretNb+zNAHqDVmBe8
+i4fDidNdoI6yqqr2jmmIBsX6iSHzCJ1pLgkzmykNRg+MzEk0sGlRvfkGzWitZky8PqxhvQqIDsjf
+Pe58BEydCl5rkdbux+0ojatNh4lz0G6k0B4WixThdkQDf2Os5M1JnMWS9KsyoUhbAgMBAAGjgbkw
+gbYwCwYDVR0PBAQDAgHGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFKFyXyYbKJhDlV0HN9WF
+lp1L0sNFMEQGA1UdHwQ9MDswOaA3oDWGM2h0dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9VVE4tVVNF
+UkZpcnN0LUhhcmR3YXJlLmNybDAxBgNVHSUEKjAoBggrBgEFBQcDAQYIKwYBBQUHAwUGCCsGAQUF
+BwMGBggrBgEFBQcDBzANBgkqhkiG9w0BAQUFAAOCAQEARxkP3nTGmZev/K0oXnWO6y1n7k57K9cM
+//bey1WiCuFMVGWTYGufEpytXoMs61quwOQt9ABjHbjAbPLPSbtNk28GpgoiskliCE7/yMgUsogW
+XecB5BKV5UU0s4tpvc+0hY91UZ59Ojg6FEgSxvunOxqNDYJAB+gECJChicsZUN/KHAG8HQQZexB2
+lzvukJDKxA4fFm517zP4029bHpbj4HR3dHuKom4t3XbWOTCC8KucUvIqx69JXn7HaOWCgchqJ/kn
+iCrVWFCVH/A7HFe7fRQ5YiuayZSSKqMiDP+JJn1fIytH1xUdqWqeUQ0qUZ6B+dQ7XnASfxAynB67
+nfhmqA==
+-----END CERTIFICATE-----
+
+UTN USERFirst Object Root CA
+============================
+-----BEGIN CERTIFICATE-----
+MIIEZjCCA06gAwIBAgIQRL4Mi1AAJLQR0zYt4LNfGzANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UE
+BhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEeMBwGA1UEChMVVGhl
+IFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHTAb
+BgNVBAMTFFVUTi1VU0VSRmlyc3QtT2JqZWN0MB4XDTk5MDcwOTE4MzEyMFoXDTE5MDcwOTE4NDAz
+NlowgZUxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJVVDEXMBUGA1UEBxMOU2FsdCBMYWtlIENpdHkx
+HjAcBgNVBAoTFVRoZSBVU0VSVFJVU1QgTmV0d29yazEhMB8GA1UECxMYaHR0cDovL3d3dy51c2Vy
+dHJ1c3QuY29tMR0wGwYDVQQDExRVVE4tVVNFUkZpcnN0LU9iamVjdDCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBAM6qgT+jo2F4qjEAVZURnicPHxzfOpuCaDDASmEd8S8O+r5596Uj71VR
+loTN2+O5bj4x2AogZ8f02b+U60cEPgLOKqJdhwQJ9jCdGIqXsqoc/EHSoTbL+z2RuufZcDX65OeQ
+w5ujm9M89RKZd7G3CeBo5hy485RjiGpq/gt2yb70IuRnuasaXnfBhQfdDWy/7gbHd2pBnqcP1/vu
+lBe3/IW+pKvEHDHd17bR5PDv3xaPslKT16HUiaEHLr/hARJCHhrh2JU022R5KP+6LhHC5ehbkkj7
+RwvCbNqtMoNB86XlQXD9ZZBt+vpRxPm9lisZBCzTbafc8H9vg2XiaquHhnUCAwEAAaOBrzCBrDAL
+BgNVHQ8EBAMCAcYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU2u1kdBScFDyr3ZmpvVsoTYs8
+ydgwQgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL2NybC51c2VydHJ1c3QuY29tL1VUTi1VU0VSRmly
+c3QtT2JqZWN0LmNybDApBgNVHSUEIjAgBggrBgEFBQcDAwYIKwYBBQUHAwgGCisGAQQBgjcKAwQw
+DQYJKoZIhvcNAQEFBQADggEBAAgfUrE3RHjb/c652pWWmKpVZIC1WkDdIaXFwfNfLEzIR1pp6ujw
+NTX00CXzyKakh0q9G7FzCL3Uw8q2NbtZhncxzaeAFK4T7/yxSPlrJSUtUbYsbUXBmMiKVl0+7kNO
+PmsnjtA6S4ULX9Ptaqd1y9Fahy85dRNacrACgZ++8A+EVCBibGnU4U3GDZlDAQ0Slox4nb9QorFE
+qmrPF3rPbw/U+CRVX/A0FklmPlBGyWNxODFiuGK581OtbLUrohKqGU8J2l7nk8aOFAj+8DCAGKCG
+hU3IfdeLA/5u1fedFqySLKAj5ZyRUh+U3xeUc8OzwcFxBSAAeL0TUh2oPs0AH8g=
+-----END CERTIFICATE-----
+
+Camerfirma Chambers of Commerce Root
+====================================
+-----BEGIN CERTIFICATE-----
+MIIEvTCCA6WgAwIBAgIBADANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJFVTEnMCUGA1UEChMe
+QUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQLExpodHRwOi8vd3d3LmNoYW1i
+ZXJzaWduLm9yZzEiMCAGA1UEAxMZQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdDAeFw0wMzA5MzAx
+NjEzNDNaFw0zNzA5MzAxNjEzNDRaMH8xCzAJBgNVBAYTAkVVMScwJQYDVQQKEx5BQyBDYW1lcmZp
+cm1hIFNBIENJRiBBODI3NDMyODcxIzAhBgNVBAsTGmh0dHA6Ly93d3cuY2hhbWJlcnNpZ24ub3Jn
+MSIwIAYDVQQDExlDaGFtYmVycyBvZiBDb21tZXJjZSBSb290MIIBIDANBgkqhkiG9w0BAQEFAAOC
+AQ0AMIIBCAKCAQEAtzZV5aVdGDDg2olUkfzIx1L4L1DZ77F1c2VHfRtbunXF/KGIJPov7coISjlU
+xFF6tdpg6jg8gbLL8bvZkSM/SAFwdakFKq0fcfPJVD0dBmpAPrMMhe5cG3nCYsS4No41XQEMIwRH
+NaqbYE6gZj3LJgqcQKH0XZi/caulAGgq7YN6D6IUtdQis4CwPAxaUWktWBiP7Zme8a7ileb2R6jW
+DA+wWFjbw2Y3npuRVDM30pQcakjJyfKl2qUMI/cjDpwyVV5xnIQFUZot/eZOKjRa3spAN2cMVCFV
+d9oKDMyXroDclDZK9D7ONhMeU+SsTjoF7Nuucpw4i9A5O4kKPnf+dQIBA6OCAUQwggFAMBIGA1Ud
+EwEB/wQIMAYBAf8CAQwwPAYDVR0fBDUwMzAxoC+gLYYraHR0cDovL2NybC5jaGFtYmVyc2lnbi5v
+cmcvY2hhbWJlcnNyb290LmNybDAdBgNVHQ4EFgQU45T1sU3p26EpW1eLTXYGduHRooowDgYDVR0P
+AQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzAnBgNVHREEIDAegRxjaGFtYmVyc3Jvb3RAY2hh
+bWJlcnNpZ24ub3JnMCcGA1UdEgQgMB6BHGNoYW1iZXJzcm9vdEBjaGFtYmVyc2lnbi5vcmcwWAYD
+VR0gBFEwTzBNBgsrBgEEAYGHLgoDATA+MDwGCCsGAQUFBwIBFjBodHRwOi8vY3BzLmNoYW1iZXJz
+aWduLm9yZy9jcHMvY2hhbWJlcnNyb290Lmh0bWwwDQYJKoZIhvcNAQEFBQADggEBAAxBl8IahsAi
+fJ/7kPMa0QOx7xP5IV8EnNrJpY0nbJaHkb5BkAFyk+cefV/2icZdp0AJPaxJRUXcLo0waLIJuvvD
+L8y6C98/d3tGfToSJI6WjzwFCm/SlCgdbQzALogi1djPHRPH8EjX1wWnz8dHnjs8NMiAT9QUu/wN
+UPf6s+xCX6ndbcj0dc97wXImsQEcXCz9ek60AcUFV7nnPKoF2YjpB0ZBzu9Bga5Y34OirsrXdx/n
+ADydb47kMgkdTXg0eDQ8lJsm7U9xxhl6vSAiSFr+S30Dt+dYvsYyTnQeaN2oaFuzPu5ifdmA6Ap1
+erfutGWaIZDgqtCYvDi1czyL+Nw=
+-----END CERTIFICATE-----
+
+Camerfirma Global Chambersign Root
+==================================
+-----BEGIN CERTIFICATE-----
+MIIExTCCA62gAwIBAgIBADANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJFVTEnMCUGA1UEChMe
+QUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQLExpodHRwOi8vd3d3LmNoYW1i
+ZXJzaWduLm9yZzEgMB4GA1UEAxMXR2xvYmFsIENoYW1iZXJzaWduIFJvb3QwHhcNMDMwOTMwMTYx
+NDE4WhcNMzcwOTMwMTYxNDE4WjB9MQswCQYDVQQGEwJFVTEnMCUGA1UEChMeQUMgQ2FtZXJmaXJt
+YSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQLExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEg
+MB4GA1UEAxMXR2xvYmFsIENoYW1iZXJzaWduIFJvb3QwggEgMA0GCSqGSIb3DQEBAQUAA4IBDQAw
+ggEIAoIBAQCicKLQn0KuWxfH2H3PFIP8T8mhtxOviteePgQKkotgVvq0Mi+ITaFgCPS3CU6gSS9J
+1tPfnZdan5QEcOw/Wdm3zGaLmFIoCQLfxS+EjXqXd7/sQJ0lcqu1PzKY+7e3/HKE5TWH+VX6ox8O
+by4o3Wmg2UIQxvi1RMLQQ3/bvOSiPGpVeAp3qdjqGTK3L/5cPxvusZjsyq16aUXjlg9V9ubtdepl
+6DJWk0aJqCWKZQbua795B9Dxt6/tLE2Su8CoX6dnfQTyFQhwrJLWfQTSM/tMtgsL+xrJxI0DqX5c
+8lCrEqWhz0hQpe/SyBoT+rB/sYIcd2oPX9wLlY/vQ37mRQklAgEDo4IBUDCCAUwwEgYDVR0TAQH/
+BAgwBgEB/wIBDDA/BgNVHR8EODA2MDSgMqAwhi5odHRwOi8vY3JsLmNoYW1iZXJzaWduLm9yZy9j
+aGFtYmVyc2lnbnJvb3QuY3JsMB0GA1UdDgQWBBRDnDafsJ4wTcbOX60Qq+UDpfqpFDAOBgNVHQ8B
+Af8EBAMCAQYwEQYJYIZIAYb4QgEBBAQDAgAHMCoGA1UdEQQjMCGBH2NoYW1iZXJzaWducm9vdEBj
+aGFtYmVyc2lnbi5vcmcwKgYDVR0SBCMwIYEfY2hhbWJlcnNpZ25yb290QGNoYW1iZXJzaWduLm9y
+ZzBbBgNVHSAEVDBSMFAGCysGAQQBgYcuCgEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly9jcHMuY2hh
+bWJlcnNpZ24ub3JnL2Nwcy9jaGFtYmVyc2lnbnJvb3QuaHRtbDANBgkqhkiG9w0BAQUFAAOCAQEA
+PDtwkfkEVCeR4e3t/mh/YV3lQWVPMvEYBZRqHN4fcNs+ezICNLUMbKGKfKX0j//U2K0X1S0E0T9Y
+gOKBWYi+wONGkyT+kL0mojAt6JcmVzWJdJYY9hXiryQZVgICsroPFOrGimbBhkVVi76SvpykBMdJ
+PJ7oKXqJ1/6v/2j1pReQvayZzKWGVwlnRtvWFsJG8eSpUPWP0ZIV018+xgBJOm5YstHRJw0lyDL4
+IBHNfTIzSJRUTN3cecQwn+uOuFW114hcxWokPbLTBQNRxgfvzBRydD1ucs4YKIxKoHflCStFREes
+t2d/AYoFWpO+ocH/+OcOZ6RHSXZddZAa9SaP8A==
+-----END CERTIFICATE-----
+
+NetLock Qualified (Class QA) Root
+=================================
+-----BEGIN CERTIFICATE-----
+MIIG0TCCBbmgAwIBAgIBezANBgkqhkiG9w0BAQUFADCByTELMAkGA1UEBhMCSFUxETAPBgNVBAcT
+CEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0b25zYWdpIEtmdC4xGjAYBgNV
+BAsTEVRhbnVzaXR2YW55a2lhZG9rMUIwQAYDVQQDEzlOZXRMb2NrIE1pbm9zaXRldHQgS296amVn
+eXpvaSAoQ2xhc3MgUUEpIFRhbnVzaXR2YW55a2lhZG8xHjAcBgkqhkiG9w0BCQEWD2luZm9AbmV0
+bG9jay5odTAeFw0wMzAzMzAwMTQ3MTFaFw0yMjEyMTUwMTQ3MTFaMIHJMQswCQYDVQQGEwJIVTER
+MA8GA1UEBxMIQnVkYXBlc3QxJzAlBgNVBAoTHk5ldExvY2sgSGFsb3phdGJpenRvbnNhZ2kgS2Z0
+LjEaMBgGA1UECxMRVGFudXNpdHZhbnlraWFkb2sxQjBABgNVBAMTOU5ldExvY2sgTWlub3NpdGV0
+dCBLb3pqZWd5em9pIChDbGFzcyBRQSkgVGFudXNpdHZhbnlraWFkbzEeMBwGCSqGSIb3DQEJARYP
+aW5mb0BuZXRsb2NrLmh1MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx1Ilstg91IRV
+CacbvWy5FPSKAtt2/GoqeKvld/Bu4IwjZ9ulZJm53QE+b+8tmjwi8F3JV6BVQX/yQ15YglMxZc4e
+8ia6AFQer7C8HORSjKAyr7c3sVNnaHRnUPYtLmTeriZ539+Zhqurf4XsoPuAzPS4DB6TRWO53Lhb
+m+1bOdRfYrCnjnxmOCyqsQhjF2d9zL2z8cM/z1A57dEZgxXbhxInlrfa6uWdvLrqOU+L73Sa58XQ
+0uqGURzk/mQIKAR5BevKxXEOC++r6uwSEaEYBTJp0QwsGj0lmT+1fMptsK6ZmfoIYOcZwvK9UdPM
+0wKswREMgM6r3JSda6M5UzrWhQIDAMV9o4ICwDCCArwwEgYDVR0TAQH/BAgwBgEB/wIBBDAOBgNV
+HQ8BAf8EBAMCAQYwggJ1BglghkgBhvhCAQ0EggJmFoICYkZJR1lFTEVNISBFemVuIHRhbnVzaXR2
+YW55IGEgTmV0TG9jayBLZnQuIE1pbm9zaXRldHQgU3pvbGdhbHRhdGFzaSBTemFiYWx5emF0YWJh
+biBsZWlydCBlbGphcmFzb2sgYWxhcGphbiBrZXN6dWx0LiBBIG1pbm9zaXRldHQgZWxla3Ryb25p
+a3VzIGFsYWlyYXMgam9naGF0YXMgZXJ2ZW55ZXN1bGVzZW5laywgdmFsYW1pbnQgZWxmb2dhZGFz
+YW5hayBmZWx0ZXRlbGUgYSBNaW5vc2l0ZXR0IFN6b2xnYWx0YXRhc2kgU3phYmFseXphdGJhbiwg
+YXogQWx0YWxhbm9zIFN6ZXJ6b2Rlc2kgRmVsdGV0ZWxla2JlbiBlbG9pcnQgZWxsZW5vcnplc2kg
+ZWxqYXJhcyBtZWd0ZXRlbGUuIEEgZG9rdW1lbnR1bW9rIG1lZ3RhbGFsaGF0b2sgYSBodHRwczov
+L3d3dy5uZXRsb2NrLmh1L2RvY3MvIGNpbWVuIHZhZ3kga2VyaGV0b2sgYXogaW5mb0BuZXRsb2Nr
+Lm5ldCBlLW1haWwgY2ltZW4uIFdBUk5JTkchIFRoZSBpc3N1YW5jZSBhbmQgdGhlIHVzZSBvZiB0
+aGlzIGNlcnRpZmljYXRlIGFyZSBzdWJqZWN0IHRvIHRoZSBOZXRMb2NrIFF1YWxpZmllZCBDUFMg
+YXZhaWxhYmxlIGF0IGh0dHBzOi8vd3d3Lm5ldGxvY2suaHUvZG9jcy8gb3IgYnkgZS1tYWlsIGF0
+IGluZm9AbmV0bG9jay5uZXQwHQYDVR0OBBYEFAlqYhaSsFq7VQ7LdTI6MuWyIckoMA0GCSqGSIb3
+DQEBBQUAA4IBAQCRalCc23iBmz+LQuM7/KbD7kPgz/PigDVJRXYC4uMvBcXxKufAQTPGtpvQMznN
+wNuhrWw3AkxYQTvyl5LGSKjN5Yo5iWH5Upfpvfb5lHTocQ68d4bDBsxafEp+NFAwLvt/MpqNPfMg
+W/hqyobzMUwsWYACff44yTB1HLdV47yfuqhthCgFdbOLDcCRVCHnpgu0mfVRQdzNo0ci2ccBgcTc
+R08m6h/t280NmPSjnLRzMkqWmf68f8glWPhY83ZmiVSkpj7EUFy6iRiCdUgh0k8T6GB+B3bbELVR
+5qq5aKrN9p2QdRLqOBrKROi3macqaJVmlaut74nLYKkGEsaUR+ko
+-----END CERTIFICATE-----
+
+NetLock Notary (Class A) Root
+=============================
+-----BEGIN CERTIFICATE-----
+MIIGfTCCBWWgAwIBAgICAQMwDQYJKoZIhvcNAQEEBQAwga8xCzAJBgNVBAYTAkhVMRAwDgYDVQQI
+EwdIdW5nYXJ5MREwDwYDVQQHEwhCdWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6
+dG9uc2FnaSBLZnQuMRowGAYDVQQLExFUYW51c2l0dmFueWtpYWRvazE2MDQGA1UEAxMtTmV0TG9j
+ayBLb3pqZWd5em9pIChDbGFzcyBBKSBUYW51c2l0dmFueWtpYWRvMB4XDTk5MDIyNDIzMTQ0N1oX
+DTE5MDIxOTIzMTQ0N1owga8xCzAJBgNVBAYTAkhVMRAwDgYDVQQIEwdIdW5nYXJ5MREwDwYDVQQH
+EwhCdWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6dG9uc2FnaSBLZnQuMRowGAYD
+VQQLExFUYW51c2l0dmFueWtpYWRvazE2MDQGA1UEAxMtTmV0TG9jayBLb3pqZWd5em9pIChDbGFz
+cyBBKSBUYW51c2l0dmFueWtpYWRvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvHSM
+D7tM9DceqQWC2ObhbHDqeLVu0ThEDaiDzl3S1tWBxdRL51uUcCbbO51qTGL3cfNk1mE7PetzozfZ
+z+qMkjvN9wfcZnSX9EUi3fRc4L9t875lM+QVOr/bmJBVOMTtplVjC7B4BPTjbsE/jvxReB+SnoPC
+/tmwqcm8WgD/qaiYdPv2LD4VOQ22BFWoDpggQrOxJa1+mm9dU7GrDPzr4PN6s6iz/0b2Y6LYOph7
+tqyF/7AlT3Rj5xMHpQqPBffAZG9+pyeAlt7ULoZgx2srXnN7F+eRP2QM2EsiNCubMvJIH5+hCoR6
+4sKtlz2O1cH5VqNQ6ca0+pii7pXmKgOM3wIDAQABo4ICnzCCApswDgYDVR0PAQH/BAQDAgAGMBIG
+A1UdEwEB/wQIMAYBAf8CAQQwEQYJYIZIAYb4QgEBBAQDAgAHMIICYAYJYIZIAYb4QgENBIICURaC
+Ak1GSUdZRUxFTSEgRXplbiB0YW51c2l0dmFueSBhIE5ldExvY2sgS2Z0LiBBbHRhbGFub3MgU3pv
+bGdhbHRhdGFzaSBGZWx0ZXRlbGVpYmVuIGxlaXJ0IGVsamFyYXNvayBhbGFwamFuIGtlc3p1bHQu
+IEEgaGl0ZWxlc2l0ZXMgZm9seWFtYXRhdCBhIE5ldExvY2sgS2Z0LiB0ZXJtZWtmZWxlbG9zc2Vn
+LWJpenRvc2l0YXNhIHZlZGkuIEEgZGlnaXRhbGlzIGFsYWlyYXMgZWxmb2dhZGFzYW5hayBmZWx0
+ZXRlbGUgYXogZWxvaXJ0IGVsbGVub3J6ZXNpIGVsamFyYXMgbWVndGV0ZWxlLiBBeiBlbGphcmFz
+IGxlaXJhc2EgbWVndGFsYWxoYXRvIGEgTmV0TG9jayBLZnQuIEludGVybmV0IGhvbmxhcGphbiBh
+IGh0dHBzOi8vd3d3Lm5ldGxvY2submV0L2RvY3MgY2ltZW4gdmFneSBrZXJoZXRvIGF6IGVsbGVu
+b3J6ZXNAbmV0bG9jay5uZXQgZS1tYWlsIGNpbWVuLiBJTVBPUlRBTlQhIFRoZSBpc3N1YW5jZSBh
+bmQgdGhlIHVzZSBvZiB0aGlzIGNlcnRpZmljYXRlIGlzIHN1YmplY3QgdG8gdGhlIE5ldExvY2sg
+Q1BTIGF2YWlsYWJsZSBhdCBodHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIG9yIGJ5IGUtbWFp
+bCBhdCBjcHNAbmV0bG9jay5uZXQuMA0GCSqGSIb3DQEBBAUAA4IBAQBIJEb3ulZv+sgoA0BO5TE5
+ayZrU3/b39/zcT0mwBQOxmd7I6gMc90Bu8bKbjc5VdXHjFYgDigKDtIqpLBJUsY4B/6+CgmM0ZjP
+ytoUMaFP0jn8DxEsQ8Pdq5PHVT5HfBgaANzze9jyf1JsIPQLX2lS9O74silg6+NJMSEN1rUQQeJB
+CWziGppWS3cC9qCbmieH6FUpccKQn0V4GuEVZD3QDtigdp+uxdAu6tYPVuxkf1qbFFgBJ34TUMdr
+KuZoPL9coAob4Q566eKAw+np9v1sEZ7Q5SgnK1QyQhSCdeZK8CtmdWOMovsEPoMOmzbwGOQmIMOM
+8CgHrTwXZoi1/baI
+-----END CERTIFICATE-----
+
+NetLock Business (Class B) Root
+===============================
+-----BEGIN CERTIFICATE-----
+MIIFSzCCBLSgAwIBAgIBaTANBgkqhkiG9w0BAQQFADCBmTELMAkGA1UEBhMCSFUxETAPBgNVBAcT
+CEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0b25zYWdpIEtmdC4xGjAYBgNV
+BAsTEVRhbnVzaXR2YW55a2lhZG9rMTIwMAYDVQQDEylOZXRMb2NrIFV6bGV0aSAoQ2xhc3MgQikg
+VGFudXNpdHZhbnlraWFkbzAeFw05OTAyMjUxNDEwMjJaFw0xOTAyMjAxNDEwMjJaMIGZMQswCQYD
+VQQGEwJIVTERMA8GA1UEBxMIQnVkYXBlc3QxJzAlBgNVBAoTHk5ldExvY2sgSGFsb3phdGJpenRv
+bnNhZ2kgS2Z0LjEaMBgGA1UECxMRVGFudXNpdHZhbnlraWFkb2sxMjAwBgNVBAMTKU5ldExvY2sg
+VXpsZXRpIChDbGFzcyBCKSBUYW51c2l0dmFueWtpYWRvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
+iQKBgQCx6gTsIKAjwo84YM/HRrPVG/77uZmeBNwcf4xKgZjupNTKihe5In+DCnVMm8Bp2GQ5o+2S
+o/1bXHQawEfKOml2mrriRBf8TKPV/riXiK+IA4kfpPIEPsgHC+b5sy96YhQJRhTKZPWLgLViqNhr
+1nGTLbO/CVRY7QbrqHvcQ7GhaQIDAQABo4ICnzCCApswEgYDVR0TAQH/BAgwBgEB/wIBBDAOBgNV
+HQ8BAf8EBAMCAAYwEQYJYIZIAYb4QgEBBAQDAgAHMIICYAYJYIZIAYb4QgENBIICURaCAk1GSUdZ
+RUxFTSEgRXplbiB0YW51c2l0dmFueSBhIE5ldExvY2sgS2Z0LiBBbHRhbGFub3MgU3pvbGdhbHRh
+dGFzaSBGZWx0ZXRlbGVpYmVuIGxlaXJ0IGVsamFyYXNvayBhbGFwamFuIGtlc3p1bHQuIEEgaGl0
+ZWxlc2l0ZXMgZm9seWFtYXRhdCBhIE5ldExvY2sgS2Z0LiB0ZXJtZWtmZWxlbG9zc2VnLWJpenRv
+c2l0YXNhIHZlZGkuIEEgZGlnaXRhbGlzIGFsYWlyYXMgZWxmb2dhZGFzYW5hayBmZWx0ZXRlbGUg
+YXogZWxvaXJ0IGVsbGVub3J6ZXNpIGVsamFyYXMgbWVndGV0ZWxlLiBBeiBlbGphcmFzIGxlaXJh
+c2EgbWVndGFsYWxoYXRvIGEgTmV0TG9jayBLZnQuIEludGVybmV0IGhvbmxhcGphbiBhIGh0dHBz
+Oi8vd3d3Lm5ldGxvY2submV0L2RvY3MgY2ltZW4gdmFneSBrZXJoZXRvIGF6IGVsbGVub3J6ZXNA
+bmV0bG9jay5uZXQgZS1tYWlsIGNpbWVuLiBJTVBPUlRBTlQhIFRoZSBpc3N1YW5jZSBhbmQgdGhl
+IHVzZSBvZiB0aGlzIGNlcnRpZmljYXRlIGlzIHN1YmplY3QgdG8gdGhlIE5ldExvY2sgQ1BTIGF2
+YWlsYWJsZSBhdCBodHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIG9yIGJ5IGUtbWFpbCBhdCBj
+cHNAbmV0bG9jay5uZXQuMA0GCSqGSIb3DQEBBAUAA4GBAATbrowXr/gOkDFOzT4JwG06sPgzTEdM
+43WIEJessDgVkcYplswhwG08pXTP2IKlOcNl40JwuyKQ433bNXbhoLXan3BukxowOR0w2y7jfLKR
+stE3Kfq51hdcR0/jHTjrn9V7lagonhVK0dHQKwCXoOKSNitjrFgBazMpUIaD8QFI
+-----END CERTIFICATE-----
+
+NetLock Express (Class C) Root
+==============================
+-----BEGIN CERTIFICATE-----
+MIIFTzCCBLigAwIBAgIBaDANBgkqhkiG9w0BAQQFADCBmzELMAkGA1UEBhMCSFUxETAPBgNVBAcT
+CEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0b25zYWdpIEtmdC4xGjAYBgNV
+BAsTEVRhbnVzaXR2YW55a2lhZG9rMTQwMgYDVQQDEytOZXRMb2NrIEV4cHJlc3N6IChDbGFzcyBD
+KSBUYW51c2l0dmFueWtpYWRvMB4XDTk5MDIyNTE0MDgxMVoXDTE5MDIyMDE0MDgxMVowgZsxCzAJ
+BgNVBAYTAkhVMREwDwYDVQQHEwhCdWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6
+dG9uc2FnaSBLZnQuMRowGAYDVQQLExFUYW51c2l0dmFueWtpYWRvazE0MDIGA1UEAxMrTmV0TG9j
+ayBFeHByZXNzeiAoQ2xhc3MgQykgVGFudXNpdHZhbnlraWFkbzCBnzANBgkqhkiG9w0BAQEFAAOB
+jQAwgYkCgYEA6+ywbGGKIyWvYCDj2Z/8kwvbXY2wobNAOoLO/XXgeDIDhlqGlZHtU/qdQPzm6N3Z
+W3oDvV3zOwzDUXmbrVWg6dADEK8KuhRC2VImESLH0iDMgqSaqf64gXadarfSNnU+sYYJ9m5tfk63
+euyucYT2BDMIJTLrdKwWRMbkQJMdf60CAwEAAaOCAp8wggKbMBIGA1UdEwEB/wQIMAYBAf8CAQQw
+DgYDVR0PAQH/BAQDAgAGMBEGCWCGSAGG+EIBAQQEAwIABzCCAmAGCWCGSAGG+EIBDQSCAlEWggJN
+RklHWUVMRU0hIEV6ZW4gdGFudXNpdHZhbnkgYSBOZXRMb2NrIEtmdC4gQWx0YWxhbm9zIFN6b2xn
+YWx0YXRhc2kgRmVsdGV0ZWxlaWJlbiBsZWlydCBlbGphcmFzb2sgYWxhcGphbiBrZXN6dWx0LiBB
+IGhpdGVsZXNpdGVzIGZvbHlhbWF0YXQgYSBOZXRMb2NrIEtmdC4gdGVybWVrZmVsZWxvc3NlZy1i
+aXp0b3NpdGFzYSB2ZWRpLiBBIGRpZ2l0YWxpcyBhbGFpcmFzIGVsZm9nYWRhc2FuYWsgZmVsdGV0
+ZWxlIGF6IGVsb2lydCBlbGxlbm9yemVzaSBlbGphcmFzIG1lZ3RldGVsZS4gQXogZWxqYXJhcyBs
+ZWlyYXNhIG1lZ3RhbGFsaGF0byBhIE5ldExvY2sgS2Z0LiBJbnRlcm5ldCBob25sYXBqYW4gYSBo
+dHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIGNpbWVuIHZhZ3kga2VyaGV0byBheiBlbGxlbm9y
+emVzQG5ldGxvY2submV0IGUtbWFpbCBjaW1lbi4gSU1QT1JUQU5UISBUaGUgaXNzdWFuY2UgYW5k
+IHRoZSB1c2Ugb2YgdGhpcyBjZXJ0aWZpY2F0ZSBpcyBzdWJqZWN0IHRvIHRoZSBOZXRMb2NrIENQ
+UyBhdmFpbGFibGUgYXQgaHR0cHM6Ly93d3cubmV0bG9jay5uZXQvZG9jcyBvciBieSBlLW1haWwg
+YXQgY3BzQG5ldGxvY2submV0LjANBgkqhkiG9w0BAQQFAAOBgQAQrX/XDDKACtiG8XmYta3UzbM2
+xJZIwVzNmtkFLp++UOv0JhQQLdRmF/iewSf98e3ke0ugbLWrmldwpu2gpO0u9f38vf5NNwgMvOOW
+gyL1SRt/Syu0VMGAfJlOHdCM7tCs5ZL6dVb+ZKATj7i4Fp1hBWeAyNDYpQcCNJgEjTME1A==
+-----END CERTIFICATE-----
+
+XRamp Global CA Root
+====================
+-----BEGIN CERTIFICATE-----
+MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCBgjELMAkGA1UE
+BhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2Vj
+dXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB
+dXRob3JpdHkwHhcNMDQxMTAxMTcxNDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMx
+HjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkg
+U2VydmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3Jp
+dHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS638eMpSe2OAtp87ZOqCwu
+IR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCPKZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMx
+foArtYzAQDsRhtDLooY2YKTVMIJt2W7QDxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FE
+zG+gSqmUsE3a56k0enI4qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqs
+AxcZZPRaJSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNViPvry
+xS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud
+EwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASsjVy16bYbMDYGA1UdHwQvMC0wK6Ap
+oCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMC
+AQEwDQYJKoZIhvcNAQEFBQADggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc
+/Kh4ZzXxHfARvbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt
+qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLaIR9NmXmd4c8n
+nxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSyi6mx5O+aGtA9aZnuqCij4Tyz
+8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQO+7ETPTsJ3xCwnR8gooJybQDJbw=
+-----END CERTIFICATE-----
+
+Go Daddy Class 2 CA
+===================
+-----BEGIN CERTIFICATE-----
+MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMY
+VGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRp
+ZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkG
+A1UEBhMCVVMxITAfBgNVBAoTGFRoZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28g
+RGFkZHkgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQAD
+ggENADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCAPVYYYwhv
+2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6wwdhFJ2+qN1j3hybX2C32
+qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXiEqITLdiOr18SPaAIBQi2XKVlOARFmR6j
+YGB0xUGlcmIbYsUfb18aQr4CUWWoriMYavx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmY
+vLEHZ6IVDd2gWMZEewo+YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0O
+BBYEFNLEsNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h/t2o
+atTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMu
+MTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwG
+A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wim
+PQoZ+YeAEW5p5JYXMP80kWNyOO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKt
+I3lpjbi2Tc7PTMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ
+HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mERdEr/VxqHD3VI
+Ls9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5CufReYNnyicsbkqWletNw+vHX/b
+vZ8=
+-----END CERTIFICATE-----
+
+Starfield Class 2 CA
+====================
+-----BEGIN CERTIFICATE-----
+MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzElMCMGA1UEChMc
+U3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZpZWxkIENsYXNzIDIg
+Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQwNjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBo
+MQswCQYDVQQGEwJVUzElMCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAG
+A1UECxMpU3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqG
+SIb3DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf8MOh2tTY
+bitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN+lq2cwQlZut3f+dZxkqZ
+JRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVm
+epsZGD3/cVE8MC5fvj13c7JdBmzDI1aaK4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSN
+F4Azbl5KXZnJHoe0nRrA1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HF
+MIHCMB0GA1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fRzt0f
+hvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNo
+bm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBDbGFzcyAyIENlcnRpZmljYXRpb24g
+QXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGs
+afPzWdqbAYcaT1epoXkJKtv3L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLM
+PUxA2IGvd56Deruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl
+xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynpVSJYACPq4xJD
+KVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEYWQPJIrSPnNVeKtelttQKbfi3
+QBFGmh95DmK/D5fs4C8fF5Q=
+-----END CERTIFICATE-----
+
+StartCom Certification Authority
+================================
+-----BEGIN CERTIFICATE-----
+MIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMN
+U3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmlu
+ZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0
+NjM2WhcNMzYwOTE3MTk0NjM2WjB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRk
+LjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMg
+U3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw
+ggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZkpMyONvg45iPwbm2xPN1y
+o4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rfOQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/
+Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/CJi/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/d
+eMotHweXMAEtcnn6RtYTKqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt
+2PZE4XNiHzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMMAv+Z
+6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w+2OqqGwaVLRcJXrJ
+osmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/
+untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVc
+UjyJthkqcwEKDwOzEmDyei+B26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT
+37uMdBNSSwIDAQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE
+FE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9jZXJ0LnN0YXJ0
+Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3JsLnN0YXJ0Y29tLm9yZy9zZnNj
+YS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFMBgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUH
+AgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0Y29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRw
+Oi8vY2VydC5zdGFydGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYg
+U3RhcnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlhYmlsaXR5
+LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2YgdGhlIFN0YXJ0Q29tIENl
+cnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFpbGFibGUgYXQgaHR0cDovL2NlcnQuc3Rh
+cnRjb20ub3JnL3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilT
+dGFydENvbSBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOC
+AgEAFmyZ9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8jhvh
+3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUWFjgKXlf2Ysd6AgXm
+vB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJzewT4F+irsfMuXGRuczE6Eri8sxHk
+fY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3
+fsNrarnDy0RLrHiQi+fHLB5LEUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZ
+EoalHmdkrQYuL6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq
+yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuCO3NJo2pXh5Tl
+1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6Vum0ABj6y6koQOdjQK/W/7HW/
+lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkyShNOsF/5oirpt9P/FlUQqmMGqz9IgcgA38coro
+g14=
+-----END CERTIFICATE-----
+
+Taiwan GRCA
+===========
+-----BEGIN CERTIFICATE-----
+MIIFcjCCA1qgAwIBAgIQH51ZWtcvwgZEpYAIaeNe9jANBgkqhkiG9w0BAQUFADA/MQswCQYDVQQG
+EwJUVzEwMC4GA1UECgwnR292ZXJubWVudCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4X
+DTAyMTIwNTEzMjMzM1oXDTMyMTIwNTEzMjMzM1owPzELMAkGA1UEBhMCVFcxMDAuBgNVBAoMJ0dv
+dmVybm1lbnQgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQAD
+ggIPADCCAgoCggIBAJoluOzMonWoe/fOW1mKydGGEghU7Jzy50b2iPN86aXfTEc2pBsBHH8eV4qN
+w8XRIePaJD9IK/ufLqGU5ywck9G/GwGHU5nOp/UKIXZ3/6m3xnOUT0b3EEk3+qhZSV1qgQdW8or5
+BtD3cCJNtLdBuTK4sfCxw5w/cP1T3YGq2GN49thTbqGsaoQkclSGxtKyyhwOeYHWtXBiCAEuTk8O
+1RGvqa/lmr/czIdtJuTJV6L7lvnM4T9TjGxMfptTCAtsF/tnyMKtsc2AtJfcdgEWFelq16TheEfO
+htX7MfP6Mb40qij7cEwdScevLJ1tZqa2jWR+tSBqnTuBto9AAGdLiYa4zGX+FVPpBMHWXx1E1wov
+J5pGfaENda1UhhXcSTvxls4Pm6Dso3pdvtUqdULle96ltqqvKKyskKw4t9VoNSZ63Pc78/1Fm9G7
+Q3hub/FCVGqY8A2tl+lSXunVanLeavcbYBT0peS2cWeqH+riTcFCQP5nRhc4L0c/cZyu5SHKYS1t
+B6iEfC3uUSXxY5Ce/eFXiGvviiNtsea9P63RPZYLhY3Naye7twWb7LuRqQoHEgKXTiCQ8P8NHuJB
+O9NAOueNXdpm5AKwB1KYXA6OM5zCppX7VRluTI6uSw+9wThNXo+EHWbNxWCWtFJaBYmOlXqYwZE8
+lSOyDvR5tMl8wUohAgMBAAGjajBoMB0GA1UdDgQWBBTMzO/MKWCkO7GStjz6MmKPrCUVOzAMBgNV
+HRMEBTADAQH/MDkGBGcqBwAEMTAvMC0CAQAwCQYFKw4DAhoFADAHBgVnKgMAAAQUA5vwIhP/lSg2
+09yewDL7MTqKUWUwDQYJKoZIhvcNAQEFBQADggIBAECASvomyc5eMN1PhnR2WPWus4MzeKR6dBcZ
+TulStbngCnRiqmjKeKBMmo4sIy7VahIkv9Ro04rQ2JyftB8M3jh+Vzj8jeJPXgyfqzvS/3WXy6Tj
+Zwj/5cAWtUgBfen5Cv8b5Wppv3ghqMKnI6mGq3ZW6A4M9hPdKmaKZEk9GhiHkASfQlK3T8v+R0F2
+Ne//AHY2RTKbxkaFXeIksB7jSJaYV0eUVXoPQbFEJPPB/hprv4j9wabak2BegUqZIJxIZhm1AHlU
+D7gsL0u8qV1bYH+Mh6XgUmMqvtg7hUAV/h62ZT/FS9p+tXo1KaMuephgIqP0fSdOLeq0dDzpD6Qz
+DxARvBMB1uUO07+1EqLhRSPAzAhuYbeJq4PjJB7mXQfnHyA+z2fI56wwbSdLaG5LKlwCCDTb+Hbk
+Z6MmnD+iMsJKxYEYMRBWqoTvLQr/uB930r+lWKBi5NdLkXWNiYCYfm3LU05er/ayl4WXudpVBrkk
+7tfGOB5jGxI7leFYrPLfhNVfmS8NVVvmONsuP3LpSIXLuykTjx44VbnzssQwmSNOXfJIoRIM3BKQ
+CZBUkQM8R+XVyWXgt0t97EfTsws+rZ7QdAAO671RrcDeLMDDav7v3Aun+kbfYNucpllQdSNpc5Oy
++fwC00fmcc4QAu4njIT/rEUNE1yDMuAlpYYsfPQS
+-----END CERTIFICATE-----
+
+Firmaprofesional Root CA
+========================
+-----BEGIN CERTIFICATE-----
+MIIEVzCCAz+gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBnTELMAkGA1UEBhMCRVMxIjAgBgNVBAcT
+GUMvIE11bnRhbmVyIDI0NCBCYXJjZWxvbmExQjBABgNVBAMTOUF1dG9yaWRhZCBkZSBDZXJ0aWZp
+Y2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODEmMCQGCSqGSIb3DQEJARYXY2FA
+ZmlybWFwcm9mZXNpb25hbC5jb20wHhcNMDExMDI0MjIwMDAwWhcNMTMxMDI0MjIwMDAwWjCBnTEL
+MAkGA1UEBhMCRVMxIjAgBgNVBAcTGUMvIE11bnRhbmVyIDI0NCBCYXJjZWxvbmExQjBABgNVBAMT
+OUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2
+ODEmMCQGCSqGSIb3DQEJARYXY2FAZmlybWFwcm9mZXNpb25hbC5jb20wggEiMA0GCSqGSIb3DQEB
+AQUAA4IBDwAwggEKAoIBAQDnIwNvbyOlXnjOlSztlB5uCp4Bx+ow0Syd3Tfom5h5VtP8c9/Qit5V
+j1H5WuretXDE7aTt/6MNbg9kUDGvASdYrv5sp0ovFy3Tc9UTHI9ZpTQsHVQERc1ouKDAA6XPhUJH
+lShbz++AbOCQl4oBPB3zhxAwJkh91/zpnZFx/0GaqUC1N5wpIE8fUuOgfRNtVLcK3ulqTgesrBlf
+3H5idPayBQC6haD9HThuy1q7hryUZzM1gywfI834yJFxzJeL764P3CkDG8A563DtwW4O2GcLiam8
+NeTvtjS0pbbELaW+0MOUJEjb35bTALVmGotmBQ/dPz/LP6pemkr4tErvlTcbAgMBAAGjgZ8wgZww
+KgYDVR0RBCMwIYYfaHR0cDovL3d3dy5maXJtYXByb2Zlc2lvbmFsLmNvbTASBgNVHRMBAf8ECDAG
+AQH/AgEBMCsGA1UdEAQkMCKADzIwMDExMDI0MjIwMDAwWoEPMjAxMzEwMjQyMjAwMDBaMA4GA1Ud
+DwEB/wQEAwIBBjAdBgNVHQ4EFgQUMwugZtHq2s7eYpMEKFK1FH84aLcwDQYJKoZIhvcNAQEFBQAD
+ggEBAEdz/o0nVPD11HecJ3lXV7cVVuzH2Fi3AQL0M+2TUIiefEaxvT8Ub/GzR0iLjJcG1+p+o1wq
+u00vR+L4OQbJnC4xGgN49Lw4xiKLMzHwFgQEffl25EvXwOaD7FnMP97/T2u3Z36mhoEyIwOdyPdf
+wUpgpZKpsaSgYMN4h7Mi8yrrW6ntBas3D7Hi05V2Y1Z0jFhyGzflZKG+TQyTmAyX9odtsz/ny4Cm
+7YjHX1BiAuiZdBbQ5rQ58SfLyEDW44YQqSMSkuBpQWOnryULwMWSyx6Yo1q6xTMPoJcB3X/ge9YG
+VM+h4k0460tQtcsm9MracEpqoeJ5quGnM/b9Sh/22WA=
+-----END CERTIFICATE-----
+
+Wells Fargo Root CA
+===================
+-----BEGIN CERTIFICATE-----
+MIID5TCCAs2gAwIBAgIEOeSXnjANBgkqhkiG9w0BAQUFADCBgjELMAkGA1UEBhMCVVMxFDASBgNV
+BAoTC1dlbGxzIEZhcmdvMSwwKgYDVQQLEyNXZWxscyBGYXJnbyBDZXJ0aWZpY2F0aW9uIEF1dGhv
+cml0eTEvMC0GA1UEAxMmV2VsbHMgRmFyZ28gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcN
+MDAxMDExMTY0MTI4WhcNMjEwMTE0MTY0MTI4WjCBgjELMAkGA1UEBhMCVVMxFDASBgNVBAoTC1dl
+bGxzIEZhcmdvMSwwKgYDVQQLEyNXZWxscyBGYXJnbyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEv
+MC0GA1UEAxMmV2VsbHMgRmFyZ28gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0GCSqG
+SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVqDM7Jvk0/82bfuUER84A4n135zHCLielTWi5MbqNQ1mX
+x3Oqfz1cQJ4F5aHiidlMuD+b+Qy0yGIZLEWukR5zcUHESxP9cMIlrCL1dQu3U+SlK93OvRw6esP3
+E48mVJwWa2uv+9iWsWCaSOAlIiR5NM4OJgALTqv9i86C1y8IcGjBqAr5dE8Hq6T54oN+J3N0Prj5
+OEL8pahbSCOz6+MlsoCultQKnMJ4msZoGK43YjdeUXWoWGPAUe5AeH6orxqg4bB4nVCMe+ez/I4j
+sNtlAHCEAQgAFG5Uhpq6zPk3EPbg3oQtnaSFN9OH4xXQwReQfhkhahKpdv0SAulPIV4XAgMBAAGj
+YTBfMA8GA1UdEwEB/wQFMAMBAf8wTAYDVR0gBEUwQzBBBgtghkgBhvt7hwcBCzAyMDAGCCsGAQUF
+BwIBFiRodHRwOi8vd3d3LndlbGxzZmFyZ28uY29tL2NlcnRwb2xpY3kwDQYJKoZIhvcNAQEFBQAD
+ggEBANIn3ZwKdyu7IvICtUpKkfnRLb7kuxpo7w6kAOnu5+/u9vnldKTC2FJYxHT7zmu1Oyl5GFrv
+m+0fazbuSCUlFLZWohDo7qd/0D+j0MNdJu4HzMPBJCGHHt8qElNvQRbn7a6U+oxy+hNH8Dx+rn0R
+OhPs7fpvcmR7nX1/Jv16+yWt6j4pf0zjAFcysLPp7VMX2YuyFA4w6OXVE8Zkr8QA1dhYJPz1j+zx
+x32l2w8n0cbyQIjmH/ZhqPRCyLk306m+LFZ4wnKbWV01QIroTmMatukgalHizqSQ33ZwmVxwQ023
+tqcZZE6St8WRPH9IFmV7Fv3L/PvZ1dZPIWU7Sn9Ho/s=
+-----END CERTIFICATE-----
+
+Swisscom Root CA 1
+==================
+-----BEGIN CERTIFICATE-----
+MIIF2TCCA8GgAwIBAgIQXAuFXAvnWUHfV8w/f52oNjANBgkqhkiG9w0BAQUFADBkMQswCQYDVQQG
+EwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0YWwgQ2VydGlmaWNhdGUgU2Vy
+dmljZXMxGzAZBgNVBAMTElN3aXNzY29tIFJvb3QgQ0EgMTAeFw0wNTA4MTgxMjA2MjBaFw0yNTA4
+MTgyMjA2MjBaMGQxCzAJBgNVBAYTAmNoMREwDwYDVQQKEwhTd2lzc2NvbTElMCMGA1UECxMcRGln
+aXRhbCBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczEbMBkGA1UEAxMSU3dpc3Njb20gUm9vdCBDQSAxMIIC
+IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0LmwqAzZuz8h+BvVM5OAFmUgdbI9m2BtRsiM
+MW8Xw/qabFbtPMWRV8PNq5ZJkCoZSx6jbVfd8StiKHVFXqrWW/oLJdihFvkcxC7mlSpnzNApbjyF
+NDhhSbEAn9Y6cV9Nbc5fuankiX9qUvrKm/LcqfmdmUc/TilftKaNXXsLmREDA/7n29uj/x2lzZAe
+AR81sH8A25Bvxn570e56eqeqDFdvpG3FEzuwpdntMhy0XmeLVNxzh+XTF3xmUHJd1BpYwdnP2IkC
+b6dJtDZd0KTeByy2dbcokdaXvij1mB7qWybJvbCXc9qukSbraMH5ORXWZ0sKbU/Lz7DkQnGMU3nn
+7uHbHaBuHYwadzVcFh4rUx80i9Fs/PJnB3r1re3WmquhsUvhzDdf/X/NTa64H5xD+SpYVUNFvJbN
+cA78yeNmuk6NO4HLFWR7uZToXTNShXEuT46iBhFRyePLoW4xCGQMwtI89Tbo19AOeCMgkckkKmUp
+WyL3Ic6DXqTz3kvTaI9GdVyDCW4pa8RwjPWd1yAv/0bSKzjCL3UcPX7ape8eYIVpQtPM+GP+HkM5
+haa2Y0EQs3MevNP6yn0WR+Kn1dCjigoIlmJWbjTb2QK5MHXjBNLnj8KwEUAKrNVxAmKLMb7dxiNY
+MUJDLXT5xp6mig/p/r+D5kNXJLrvRjSq1xIBOO0CAwEAAaOBhjCBgzAOBgNVHQ8BAf8EBAMCAYYw
+HQYDVR0hBBYwFDASBgdghXQBUwABBgdghXQBUwABMBIGA1UdEwEB/wQIMAYBAf8CAQcwHwYDVR0j
+BBgwFoAUAyUv3m+CATpcLNwroWm1Z9SM0/0wHQYDVR0OBBYEFAMlL95vggE6XCzcK6FptWfUjNP9
+MA0GCSqGSIb3DQEBBQUAA4ICAQA1EMvspgQNDQ/NwNurqPKIlwzfky9NfEBWMXrrpA9gzXrzvsMn
+jgM+pN0S734edAY8PzHyHHuRMSG08NBsl9Tpl7IkVh5WwzW9iAUPWxAaZOHHgjD5Mq2eUCzneAXQ
+MbFamIp1TpBcahQq4FJHgmDmHtqBsfsUC1rxn9KVuj7QG9YVHaO+htXbD8BJZLsuUBlL0iT43R4H
+VtA4oJVwIHaM190e3p9xxCPvgxNcoyQVTSlAPGrEqdi3pkSlDfTgnXceQHAm/NrZNuR55LU/vJtl
+vrsRls/bxig5OgjOR1tTWsWZ/l2p3e9M1MalrQLmjAcSHm8D0W+go/MpvRLHUKKwf4ipmXeascCl
+OS5cfGniLLDqN2qk4Vrh9VDlg++luyqI54zb/W1elxmofmZ1a3Hqv7HHb6D0jqTsNFFbjCYDcKF3
+1QESVwA12yPeDooomf2xEG9L/zgtYE4snOtnta1J7ksfrK/7DZBaZmBwXarNeNQk7shBoJMBkpxq
+nvy5JMWzFYJ+vq6VK+uxwNrjAWALXmmshFZhvnEX/h0TD/7Gh0Xp/jKgGg0TpJRVcaUWi7rKibCy
+x/yP2FS1k2Kdzs9Z+z0YzirLNRWCXf9UIltxUvu3yf5gmwBBZPCqKuy2QkPOiWaByIufOVQDJdMW
+NY6E0F/6MBr1mmz0DlP5OlvRHA==
+-----END CERTIFICATE-----
+
+DigiCert Assured ID Root CA
+===========================
+-----BEGIN CERTIFICATE-----
+MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQG
+EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw
+IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzEx
+MTEwMDAwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL
+ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0Ew
+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7cJpSIqvTO
+9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYPmDI2dsze3Tyoou9q+yHy
+UmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW
+/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpy
+oeb6pNnVFzF1roV9Iq4/AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whf
+GHdPAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRF
+66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzANBgkq
+hkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRCdWKuh+vy1dneVrOfzM4UKLkNl2Bc
+EkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTffwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38Fn
+SbNd67IJKusm7Xi+fT8r87cmNW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i
+8b5QZ7dsvfPxH2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe
++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g==
+-----END CERTIFICATE-----
+
+DigiCert Global Root CA
+=======================
+-----BEGIN CERTIFICATE-----
+MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQG
+EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw
+HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAw
+MDAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
+dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkq
+hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsBCSDMAZOn
+TjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97nh6Vfe63SKMI2tavegw5
+BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt43C/dxC//AH2hdmoRBBYMql1GNXRor5H
+4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7PT19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y
+7vrTC0LUq7dBMtoM1O/4gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQAB
+o2MwYTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbRTLtm
+8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUwDQYJKoZIhvcNAQEF
+BQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/EsrhMAtudXH/vTBH1jLuG2cenTnmCmr
+EbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIt
+tep3Sp+dWOIrWcBAI+0tKIJFPnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886
+UAb3LujEV0lsYSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk
+CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=
+-----END CERTIFICATE-----
+
+DigiCert High Assurance EV Root CA
+==================================
+-----BEGIN CERTIFICATE-----
+MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBsMQswCQYDVQQG
+EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSsw
+KQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5jZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAw
+MFoXDTMxMTExMDAwMDAwMFowbDELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ
+MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFu
+Y2UgRVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm+9S75S0t
+Mqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTWPNt0OKRKzE0lgvdKpVMS
+OO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEMxChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3
+MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFBIk5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQ
+NAQTXKFx01p8VdteZOE3hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUe
+h10aUAsgEsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMB
+Af8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaAFLE+w2kD+L9HAdSY
+JhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3NecnzyIZgYIVyHbIUf4KmeqvxgydkAQ
+V8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6zeM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFp
+myPInngiK3BD41VHMWEZ71jFhS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkK
+mNEVX58Svnw2Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe
+vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep+OkuE6N36B9K
+-----END CERTIFICATE-----
+
+Certplus Class 2 Primary CA
+===========================
+-----BEGIN CERTIFICATE-----
+MIIDkjCCAnqgAwIBAgIRAIW9S/PY2uNp9pTXX8OlRCMwDQYJKoZIhvcNAQEFBQAwPTELMAkGA1UE
+BhMCRlIxETAPBgNVBAoTCENlcnRwbHVzMRswGQYDVQQDExJDbGFzcyAyIFByaW1hcnkgQ0EwHhcN
+OTkwNzA3MTcwNTAwWhcNMTkwNzA2MjM1OTU5WjA9MQswCQYDVQQGEwJGUjERMA8GA1UEChMIQ2Vy
+dHBsdXMxGzAZBgNVBAMTEkNsYXNzIDIgUHJpbWFyeSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP
+ADCCAQoCggEBANxQltAS+DXSCHh6tlJw/W/uz7kRy1134ezpfgSN1sxvc0NXYKwzCkTsA18cgCSR
+5aiRVhKC9+Ar9NuuYS6JEI1rbLqzAr3VNsVINyPi8Fo3UjMXEuLRYE2+L0ER4/YXJQyLkcAbmXuZ
+Vg2v7tK8R1fjeUl7NIknJITesezpWE7+Tt9avkGtrAjFGA7v0lPubNCdEgETjdyAYveVqUSISnFO
+YFWe2yMZeVYHDD9jC1yw4r5+FfyUM1hBOHTE4Y+L3yasH7WLO7dDWWuwJKZtkIvEcupdM5i3y95e
+e++U8Rs+yskhwcWYAqqi9lt3m/V+llU0HGdpwPFC40es/CgcZlUCAwEAAaOBjDCBiTAPBgNVHRME
+CDAGAQH/AgEKMAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQU43Mt38sOKAze3bOkynm4jrvoMIkwEQYJ
+YIZIAYb4QgEBBAQDAgEGMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly93d3cuY2VydHBsdXMuY29t
+L0NSTC9jbGFzczIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCnVM+IRBnL39R/AN9WM2K191EBkOvD
+P9GIROkkXe/nFL0gt5o8AP5tn9uQ3Nf0YtaLcF3n5QRIqWh8yfFC82x/xXp8HVGIutIKPidd3i1R
+TtMTZGnkLuPT55sJmabglZvOGtd/vjzOUrMRFcEPF80Du5wlFbqidon8BvEY0JNLDnyCt6X09l/+
+7UCmnYR0ObncHoUW2ikbhiMAybuJfm6AiB4vFLQDJKgybwOaRywwvlbGp0ICcBvqQNi6BQNwB6SW
+//1IMwrh3KWBkJtN3X3n57LNXMhqlfil9o3EXXgIvnsG1knPGTZQIy4I5p4FTUcY1Rbpsda2ENW7
+l7+ijrRU
+-----END CERTIFICATE-----
+
+DST Root CA X3
+==============
+-----BEGIN CERTIFICATE-----
+MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/MSQwIgYDVQQK
+ExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4X
+DTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVowPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1
+cmUgVHJ1c3QgQ28uMRcwFQYDVQQDEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmT
+rE4Orz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEqOLl5CjH9
+UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9bxiqKqy69cK3FCxolkHRy
+xXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40d
+utolucbY38EVAjqr2m7xPi71XAicPNaDaeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0T
+AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQ
+MA0GCSqGSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69ikug
+dB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXrAvHRAosZy5Q6XkjE
+GB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZzR8srzJmwN0jP41ZL9c8PDHIyh8bw
+RLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubS
+fZGL+T0yjWW06XyxV3bqxbYoOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
+-----END CERTIFICATE-----
+
+DST ACES CA X6
+==============
+-----BEGIN CERTIFICATE-----
+MIIECTCCAvGgAwIBAgIQDV6ZCtadt3js2AdWO4YV2TANBgkqhkiG9w0BAQUFADBbMQswCQYDVQQG
+EwJVUzEgMB4GA1UEChMXRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QxETAPBgNVBAsTCERTVCBBQ0VT
+MRcwFQYDVQQDEw5EU1QgQUNFUyBDQSBYNjAeFw0wMzExMjAyMTE5NThaFw0xNzExMjAyMTE5NTha
+MFsxCzAJBgNVBAYTAlVTMSAwHgYDVQQKExdEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdDERMA8GA1UE
+CxMIRFNUIEFDRVMxFzAVBgNVBAMTDkRTVCBBQ0VTIENBIFg2MIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEAuT31LMmU3HWKlV1j6IR3dma5WZFcRt2SPp/5DgO0PWGSvSMmtWPuktKe1jzI
+DZBfZIGxqAgNTNj50wUoUrQBJcWVHAx+PhCEdc/BGZFjz+iokYi5Q1K7gLFViYsx+tC3dr5BPTCa
+pCIlF3PoHuLTrCq9Wzgh1SpL11V94zpVvddtawJXa+ZHfAjIgrrep4c9oW24MFbCswKBXy314pow
+GCi4ZtPLAZZv6opFVdbgnf9nKxcCpk4aahELfrd755jWjHZvwTvbUJN+5dCOHze4vbrGn2zpfDPy
+MjwmR/onJALJfh1biEITajV8fTXpLmaRcpPVMibEdPVTo7NdmvYJywIDAQABo4HIMIHFMA8GA1Ud
+EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgHGMB8GA1UdEQQYMBaBFHBraS1vcHNAdHJ1c3Rkc3Qu
+Y29tMGIGA1UdIARbMFkwVwYKYIZIAWUDAgEBATBJMEcGCCsGAQUFBwIBFjtodHRwOi8vd3d3LnRy
+dXN0ZHN0LmNvbS9jZXJ0aWZpY2F0ZXMvcG9saWN5L0FDRVMtaW5kZXguaHRtbDAdBgNVHQ4EFgQU
+CXIGThhDD+XWzMNqizF7eI+og7gwDQYJKoZIhvcNAQEFBQADggEBAKPYjtay284F5zLNAdMEA+V2
+5FYrnJmQ6AgwbN99Pe7lv7UkQIRJ4dEorsTCOlMwiPH1d25Ryvr/ma8kXxug/fKshMrfqfBfBC6t
+Fr8hlxCBPeP/h40y3JTlR4peahPJlJU90u7INJXQgNStMgiAVDzgvVJT11J8smk/f3rPanTK+gQq
+nExaBqXpIK1FZg9p8d2/6eMyi/rgwYZNcjwu2JN4Cir42NInPRmJX1p7ijvMDNpRrscL9yuwNwXs
+vFcj4jjSm2jzVhKIT0J8uDHEtdvkyCE06UgRNe76x5JXxZ805Mf29w4LTJxoeHtxMcfrHuBnQfO3
+oKfN5XozNmr6mis=
+-----END CERTIFICATE-----
+
+TURKTRUST Certificate Services Provider Root 1
+==============================================
+-----BEGIN CERTIFICATE-----
+MIID+zCCAuOgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBtzE/MD0GA1UEAww2VMOcUktUUlVTVCBF
+bGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxMQswCQYDVQQGDAJUUjEP
+MA0GA1UEBwwGQU5LQVJBMVYwVAYDVQQKDE0oYykgMjAwNSBUw5xSS1RSVVNUIEJpbGdpIMSwbGV0
+acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWeLjAeFw0wNTA1MTMx
+MDI3MTdaFw0xNTAzMjIxMDI3MTdaMIG3MT8wPQYDVQQDDDZUw5xSS1RSVVNUIEVsZWt0cm9uaWsg
+U2VydGlmaWthIEhpem1ldCBTYcSfbGF5xLFjxLFzxLExCzAJBgNVBAYMAlRSMQ8wDQYDVQQHDAZB
+TktBUkExVjBUBgNVBAoMTShjKSAyMDA1IFTDnFJLVFJVU1QgQmlsZ2kgxLBsZXRpxZ9pbSB2ZSBC
+aWxpxZ9pbSBHw7x2ZW5sacSfaSBIaXptZXRsZXJpIEEuxZ4uMIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEAylIF1mMD2Bxf3dJ7XfIMYGFbazt0K3gNfUW9InTojAPBxhEqPZW8qZSwu5GX
+yGl8hMW0kWxsE2qkVa2kheiVfrMArwDCBRj1cJ02i67L5BuBf5OI+2pVu32Fks66WJ/bMsW9Xe8i
+Si9BB35JYbOG7E6mQW6EvAPs9TscyB/C7qju6hJKjRTP8wrgUDn5CDX4EVmt5yLqS8oUBt5CurKZ
+8y1UiBAG6uEaPj1nH/vO+3yC6BFdSsG5FOpU2WabfIl9BJpiyelSPJ6c79L1JuTm5Rh8i27fbMx4
+W09ysstcP4wFjdFMjK2Sx+F4f2VsSQZQLJ4ywtdKxnWKWU51b0dewQIDAQABoxAwDjAMBgNVHRME
+BTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAV9VX/N5aAWSGk/KEVTCD21F/aAyT8z5Aa9CEKmu46
+sWrv7/hg0Uw2ZkUd82YCdAR7kjCo3gp2D++Vbr3JN+YaDayJSFvMgzbC9UZcWYJWtNX+I7TYVBxE
+q8Sn5RTOPEFhfEPmzcSBCYsk+1Ql1haolgxnB2+zUEfjHCQo3SqYpGH+2+oSN7wBGjSFvW5P55Fy
+B0SFHljKVETd96y5y4khctuPwGkplyqjrhgjlxxBKot8KsF8kOipKMDTkcatKIdAaLX/7KfS0zgY
+nNN9aV3wxqUeJBujR/xpB2jn5Jq07Q+hh4cCzofSSE7hvP/L8XKSRGQDJereW26fyfJOrN3H
+-----END CERTIFICATE-----
+
+TURKTRUST Certificate Services Provider Root 2
+==============================================
+-----BEGIN CERTIFICATE-----
+MIIEPDCCAySgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBvjE/MD0GA1UEAww2VMOcUktUUlVTVCBF
+bGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxMQswCQYDVQQGEwJUUjEP
+MA0GA1UEBwwGQW5rYXJhMV0wWwYDVQQKDFRUw5xSS1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUg
+QmlsacWfaW0gR8O8dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWeLiAoYykgS2FzxLFtIDIwMDUwHhcN
+MDUxMTA3MTAwNzU3WhcNMTUwOTE2MTAwNzU3WjCBvjE/MD0GA1UEAww2VMOcUktUUlVTVCBFbGVr
+dHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxMQswCQYDVQQGEwJUUjEPMA0G
+A1UEBwwGQW5rYXJhMV0wWwYDVQQKDFRUw5xSS1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmls
+acWfaW0gR8O8dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWeLiAoYykgS2FzxLFtIDIwMDUwggEiMA0G
+CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCpNn7DkUNMwxmYCMjHWHtPFoylzkkBH3MOrHUTpvqe
+LCDe2JAOCtFp0if7qnefJ1Il4std2NiDUBd9irWCPwSOtNXwSadktx4uXyCcUHVPr+G1QRT0mJKI
+x+XlZEdhR3n9wFHxwZnn3M5q+6+1ATDcRhzviuyV79z/rxAc653YsKpqhRgNF8k+v/Gb0AmJQv2g
+QrSdiVFVKc8bcLyEVK3BEx+Y9C52YItdP5qtygy/p1Zbj3e41Z55SZI/4PGXJHpsmxcPbe9TmJEr
+5A++WXkHeLuXlfSfadRYhwqp48y2WBmfJiGxxFmNskF1wK1pzpwACPI2/z7woQ8arBT9pmAPAgMB
+AAGjQzBBMB0GA1UdDgQWBBTZN7NOBf3Zz58SFq62iS/rJTqIHDAPBgNVHQ8BAf8EBQMDBwYAMA8G
+A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAHJglrfJ3NgpXiOFX7KzLXb7iNcX/ntt
+Rbj2hWyfIvwqECLsqrkw9qtY1jkQMZkpAL2JZkH7dN6RwRgLn7Vhy506vvWolKMiVW4XSf/SKfE4
+Jl3vpao6+XF75tpYHdN0wgH6PmlYX63LaL4ULptswLbcoCb6dxriJNoaN+BnrdFzgw2lGh1uEpJ+
+hGIAF728JRhX8tepb1mIvDS3LoV4nZbcFMMsilKbloxSZj2GFotHuFEJjOp9zYhys2AzsfAKRO8P
+9Qk3iCQOLGsgOqL6EfJANZxEaGM7rDNvY7wsu/LSy3Z9fYjYHcgFHW68lKlmjHdxx/qR+i9Rnuk5
+UrbnBEI=
+-----END CERTIFICATE-----
+
+SwissSign Platinum CA - G2
+==========================
+-----BEGIN CERTIFICATE-----
+MIIFwTCCA6mgAwIBAgIITrIAZwwDXU8wDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UEBhMCQ0gxFTAT
+BgNVBAoTDFN3aXNzU2lnbiBBRzEjMCEGA1UEAxMaU3dpc3NTaWduIFBsYXRpbnVtIENBIC0gRzIw
+HhcNMDYxMDI1MDgzNjAwWhcNMzYxMDI1MDgzNjAwWjBJMQswCQYDVQQGEwJDSDEVMBMGA1UEChMM
+U3dpc3NTaWduIEFHMSMwIQYDVQQDExpTd2lzc1NpZ24gUGxhdGludW0gQ0EgLSBHMjCCAiIwDQYJ
+KoZIhvcNAQEBBQADggIPADCCAgoCggIBAMrfogLi2vj8Bxax3mCq3pZcZB/HL37PZ/pEQtZ2Y5Wu
+669yIIpFR4ZieIbWIDkm9K6j/SPnpZy1IiEZtzeTIsBQnIJ71NUERFzLtMKfkr4k2HtnIuJpX+UF
+eNSH2XFwMyVTtIc7KZAoNppVRDBopIOXfw0enHb/FZ1glwCNioUD7IC+6ixuEFGSzH7VozPY1kne
+WCqv9hbrS3uQMpe5up1Y8fhXSQQeol0GcN1x2/ndi5objM89o03Oy3z2u5yg+gnOI2Ky6Q0f4nIo
+j5+saCB9bzuohTEJfwvH6GXp43gOCWcwizSC+13gzJ2BbWLuCB4ELE6b7P6pT1/9aXjvCR+htL/6
+8++QHkwFix7qepF6w9fl+zC8bBsQWJj3Gl/QKTIDE0ZNYWqFTFJ0LwYfexHihJfGmfNtf9dng34T
+aNhxKFrYzt3oEBSa/m0jh26OWnA81Y0JAKeqvLAxN23IhBQeW71FYyBrS3SMvds6DsHPWhaPpZjy
+domyExI7C3d3rLvlPClKknLKYRorXkzig3R3+jVIeoVNjZpTxN94ypeRSCtFKwH3HBqi7Ri6Cr2D
++m+8jVeTO9TUps4e8aCxzqv9KyiaTxvXw3LbpMS/XUz13XuWae5ogObnmLo2t/5u7Su9IPhlGdpV
+CX4l3P5hYnL5fhgC72O00Puv5TtjjGePAgMBAAGjgawwgakwDgYDVR0PAQH/BAQDAgEGMA8GA1Ud
+EwEB/wQFMAMBAf8wHQYDVR0OBBYEFFCvzAeHFUdvOMW0ZdHelarp35zMMB8GA1UdIwQYMBaAFFCv
+zAeHFUdvOMW0ZdHelarp35zMMEYGA1UdIAQ/MD0wOwYJYIV0AVkBAQEBMC4wLAYIKwYBBQUHAgEW
+IGh0dHA6Ly9yZXBvc2l0b3J5LnN3aXNzc2lnbi5jb20vMA0GCSqGSIb3DQEBBQUAA4ICAQAIhab1
+Fgz8RBrBY+D5VUYI/HAcQiiWjrfFwUF1TglxeeVtlspLpYhg0DB0uMoI3LQwnkAHFmtllXcBrqS3
+NQuB2nEVqXQXOHtYyvkv+8Bldo1bAbl93oI9ZLi+FHSjClTTLJUYFzX1UWs/j6KWYTl4a0vlpqD4
+U99REJNi54Av4tHgvI42Rncz7Lj7jposiU0xEQ8mngS7twSNC/K5/FqdOxa3L8iYq/6KUFkuozv8
+KV2LwUvJ4ooTHbG/u0IdUt1O2BReEMYxB+9xJ/cbOQncguqLs5WGXv312l0xpuAxtpTmREl0xRbl
+9x8DYSjFyMsSoEJL+WuICI20MhjzdZ/EfwBPBZWcoxcCw7NTm6ogOSkrZvqdr16zktK1puEa+S1B
+aYEUtLS17Yk9zvupnTVCRLEcFHOBzyoBNZox1S2PbYTfgE1X4z/FhHXaicYwu+uPyyIIoK6q8QNs
+OktNCaUOcsZWayFCTiMlFGiudgp8DAdwZPmaL/YFOSbGDI8Zf0NebvRbFS/bYV3mZy8/CJT5YLSY
+Mdp08YSTcU1f+2BY0fvEwW2JorsgH51xkcsymxM9Pn2SUjWskpSi0xjCfMfqr3YFFt1nJ8J+HAci
+IfNAChs0B0QTwoRqjt8ZWr9/6x3iGjjRXK9HkmuAtTClyY3YqzGBH9/CZjfTk6mFhnll0g==
+-----END CERTIFICATE-----
+
+SwissSign Gold CA - G2
+======================
+-----BEGIN CERTIFICATE-----
+MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNVBAYTAkNIMRUw
+EwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2lnbiBHb2xkIENBIC0gRzIwHhcN
+MDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBFMQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dp
+c3NTaWduIEFHMR8wHQYDVQQDExZTd2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0B
+AQEFAAOCAg8AMIICCgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUq
+t2/876LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+bbqBHH5C
+jCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c6bM8K8vzARO/Ws/BtQpg
+vd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqEemA8atufK+ze3gE/bk3lUIbLtK/tREDF
+ylqM2tIrfKjuvqblCqoOpd8FUrdVxyJdMmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvR
+AiTysybUa9oEVeXBCsdtMDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuend
+jIj3o02yMszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69yFGkO
+peUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPiaG59je883WX0XaxR
+7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxMgI93e2CaHt+28kgeDrpOVG2Y4OGi
+GqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw
+AwEB/zAdBgNVHQ4EFgQUWyV7lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64
+OfPAeGZe6Drn8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov
+L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe645R88a7A3hfm
+5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczOUYrHUDFu4Up+GC9pWbY9ZIEr
+44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOf
+Mke6UiI0HTJ6CVanfCU2qT1L2sCCbwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6m
+Gu6uLftIdxf+u+yvGPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxp
+mo/a77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCChdiDyyJk
+vC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid392qgQmwLOM7XdVAyksLf
+KzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEppLd6leNcG2mqeSz53OiATIgHQv2ieY2Br
+NU0LbbqhPcCT4H8js1WtciVORvnSFu+wZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6Lqj
+viOvrv1vA+ACOzB2+httQc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ
+-----END CERTIFICATE-----
+
+SwissSign Silver CA - G2
+========================
+-----BEGIN CERTIFICATE-----
+MIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCQ0gxFTAT
+BgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMB4X
+DTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0NlowRzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3
+aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG
+9w0BAQEFAAOCAg8AMIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644
+N0MvFz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7brYT7QbNHm
++/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieFnbAVlDLaYQ1HTWBCrpJH
+6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH6ATK72oxh9TAtvmUcXtnZLi2kUpCe2Uu
+MGoM9ZDulebyzYLs2aFK7PayS+VFheZteJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5h
+qAaEuSh6XzjZG6k4sIN/c8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5
+FZGkECwJMoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRHHTBs
+ROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTfjNFusB3hB48IHpmc
+celM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb65i/4z3GcRm25xBWNOHkDRUjvxF3X
+CO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/
+BAUwAwEB/zAdBgNVHQ4EFgQUF6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRB
+tjpbO8tFnb0cwpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0
+cDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBAHPGgeAn0i0P
+4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShpWJHckRE1qTodvBqlYJ7YH39F
+kWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9xCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L
+3XWgwF15kIwb4FDm3jH+mHtwX6WQ2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx
+/uNncqCxv1yL5PqZIseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFa
+DGi8aRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2Xem1ZqSqP
+e97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQRdAtq/gsD/KNVV4n+Ssuu
+WxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/OMpXEA29MC/HpeZBoNquBYeaoKRlbEwJ
+DIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ub
+DgEj8Z+7fNzcbBGXJbLytGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u
+-----END CERTIFICATE-----
+
+GeoTrust Primary Certification Authority
+========================================
+-----BEGIN CERTIFICATE-----
+MIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQG
+EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMoR2VvVHJ1c3QgUHJpbWFyeSBD
+ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjExMjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgx
+CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQ
+cmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9AWbK7hWN
+b6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjAZIVcFU2Ix7e64HXprQU9
+nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE07e9GceBrAqg1cmuXm2bgyxx5X9gaBGge
+RwLmnWDiNpcB3841kt++Z8dtd1k7j53WkBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGt
+tm/81w7a4DSwDRp35+MImO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD
+AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJKoZI
+hvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ16CePbJC/kRYkRj5K
+Ts4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl4b7UVXGYNTq+k+qurUKykG/g/CFN
+NWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6KoKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHa
+Floxt/m0cYASSJlyc1pZU8FjUjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG
+1riR/aYNKxoUAT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk=
+-----END CERTIFICATE-----
+
+thawte Primary Root CA
+======================
+-----BEGIN CERTIFICATE-----
+MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCBqTELMAkGA1UE
+BhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2
+aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhv
+cml6ZWQgdXNlIG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3
+MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwg
+SW5jLjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMv
+KGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNVBAMT
+FnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCs
+oPD7gFnUnMekz52hWXMJEEUMDSxuaPFsW0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ
+1CRfBsDMRJSUjQJib+ta3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGc
+q/gcfomk6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6Sk/K
+aAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94JNqR32HuHUETVPm4p
+afs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD
+VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XPr87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUF
+AAOCAQEAeRHAS7ORtvzw6WfUDW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeE
+uzLlQRHAd9mzYJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX
+xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2/qxAeeWsEG89
+jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/LHbTY5xZ3Y+m4Q6gLkH3LpVH
+z7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7jVaMaA==
+-----END CERTIFICATE-----
+
+VeriSign Class 3 Public Primary Certification Authority - G5
+============================================================
+-----BEGIN CERTIFICATE-----
+MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCByjELMAkGA1UE
+BhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBO
+ZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVk
+IHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRp
+ZmljYXRpb24gQXV0aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCB
+yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2ln
+biBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2lnbiwgSW5jLiAtIEZvciBh
+dXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmlt
+YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
+ggEKAoIBAQCvJAgIKXo1nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKz
+j/i5Vbext0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIzSdhD
+Y2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQGBO+QueQA5N06tRn/
+Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+rCpSx4/VBEnkjWNHiDxpg8v+R70r
+fk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/
+BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2Uv
+Z2lmMCEwHzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy
+aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKvMzEzMA0GCSqG
+SIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzEp6B4Eq1iDkVwZMXnl2YtmAl+
+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKE
+KQsTb47bDN0lAtukixlE0kF6BWlKWE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiC
+Km0oHw0LxOXnGiYZ4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vE
+ZV8NhnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq
+-----END CERTIFICATE-----
+
+SecureTrust CA
+==============
+-----BEGIN CERTIFICATE-----
+MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBIMQswCQYDVQQG
+EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xFzAVBgNVBAMTDlNlY3VyZVRy
+dXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIzMTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAe
+BgNVBAoTF1NlY3VyZVRydXN0IENvcnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCC
+ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQX
+OZEzZum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO0gMdA+9t
+DWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIaowW8xQmxSPmjL8xk037uH
+GFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b
+01k/unK8RCSc43Oz969XL0Imnal0ugBS8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmH
+ursCAwEAAaOBnTCBmjATBgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/
+BAUwAwEB/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCegJYYj
+aHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQAwDQYJ
+KoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt36Z3q059c4EVlew3KW+JwULKUBRSu
+SceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHf
+mbx8IVQr5Fiiu1cprp6poxkmD5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZ
+nMUFdAvnZyPSCPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR
+3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE=
+-----END CERTIFICATE-----
+
+Secure Global CA
+================
+-----BEGIN CERTIFICATE-----
+MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQG
+EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBH
+bG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkxMjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEg
+MB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwg
+Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jx
+YDiJiQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa/FHtaMbQ
+bqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJjnIFHovdRIWCQtBJwB1g
+8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnIHmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYV
+HDGA76oYa8J719rO+TMg1fW9ajMtgQT7sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi
+0XPnj3pDAgMBAAGjgZ0wgZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud
+EwEB/wQFMAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCswKaAn
+oCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsGAQQBgjcVAQQDAgEA
+MA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0LURYD7xh8yOOvaliTFGCRsoTciE6+
+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXOH0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cn
+CDpOGR86p1hcF895P4vkp9MmI50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/5
+3CYNv6ZHdAbYiNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc
+f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW
+-----END CERTIFICATE-----
+
+COMODO Certification Authority
+==============================
+-----BEGIN CERTIFICATE-----
+MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCBgTELMAkGA1UE
+BhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG
+A1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNVBAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1
+dGhvcml0eTAeFw0wNjEyMDEwMDAwMDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEb
+MBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFD
+T01PRE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0aG9yaXR5
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3UcEbVASY06m/weaKXTuH
++7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI2GqGd0S7WWaXUF601CxwRM/aN5VCaTww
+xHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV
+4EajcNxo2f8ESIl33rXp+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA
+1KGzqSX+DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5OnKVI
+rLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW/zAOBgNVHQ8BAf8E
+BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6gPKA6hjhodHRwOi8vY3JsLmNvbW9k
+b2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOC
+AQEAPpiem/Yb6dc5t3iuHXIYSdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CP
+OGEIqB6BCsAvIC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/
+RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4zJVSk/BwJVmc
+IGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5ddBA6+C4OmF4O5MBKgxTMVBbkN
++8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IBZQ==
+-----END CERTIFICATE-----
+
+Network Solutions Certificate Authority
+=======================================
+-----BEGIN CERTIFICATE-----
+MIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQG
+EwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydOZXR3b3Jr
+IFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMx
+MjM1OTU5WjBiMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu
+MTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0G
+CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwzc7MEL7xx
+jOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPPOCwGJgl6cvf6UDL4wpPT
+aaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rlmGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXT
+crA/vGp97Eh/jcOrqnErU2lBUzS1sLnFBgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc
+/Qzpf14Dl847ABSHJ3A4qY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMB
+AAGjgZcwgZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIBBjAP
+BgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwubmV0c29sc3NsLmNv
+bS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3JpdHkuY3JsMA0GCSqGSIb3DQEBBQUA
+A4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc86fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q
+4LqILPxFzBiwmZVRDuwduIj/h1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/
+GGUsyfJj4akH/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv
+wKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHNpGxlaKFJdlxD
+ydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey
+-----END CERTIFICATE-----
+
+WellsSecure Public Root Certificate Authority
+=============================================
+-----BEGIN CERTIFICATE-----
+MIIEvTCCA6WgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBhTELMAkGA1UEBhMCVVMxIDAeBgNVBAoM
+F1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJlMRwwGgYDVQQLDBNXZWxscyBGYXJnbyBCYW5rIE5BMTYw
+NAYDVQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcN
+MDcxMjEzMTcwNzU0WhcNMjIxMjE0MDAwNzU0WjCBhTELMAkGA1UEBhMCVVMxIDAeBgNVBAoMF1dl
+bGxzIEZhcmdvIFdlbGxzU2VjdXJlMRwwGgYDVQQLDBNXZWxscyBGYXJnbyBCYW5rIE5BMTYwNAYD
+VQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0G
+CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDub7S9eeKPCCGeOARBJe+rWxxTkqxtnt3CxC5FlAM1
+iGd0V+PfjLindo8796jE2yljDpFoNoqXjopxaAkH5OjUDk/41itMpBb570OYj7OeUt9tkTmPOL13
+i0Nj67eT/DBMHAGTthP796EfvyXhdDcsHqRePGj4S78NuR4uNuip5Kf4D8uCdXw1LSLWwr8L87T8
+bJVhHlfXBIEyg1J55oNjz7fLY4sR4r1e6/aN7ZVyKLSsEmLpSjPmgzKuBXWVvYSV2ypcm44uDLiB
+K0HmOFafSZtsdvqKXfcBeYF8wYNABf5x/Qw/zE5gCQ5lRxAvAcAFP4/4s0HvWkJ+We/SlwxlAgMB
+AAGjggE0MIIBMDAPBgNVHRMBAf8EBTADAQH/MDkGA1UdHwQyMDAwLqAsoCqGKGh0dHA6Ly9jcmwu
+cGtpLndlbGxzZmFyZ28uY29tL3dzcHJjYS5jcmwwDgYDVR0PAQH/BAQDAgHGMB0GA1UdDgQWBBQm
+lRkQ2eihl5H/3BnZtQQ+0nMKajCBsgYDVR0jBIGqMIGngBQmlRkQ2eihl5H/3BnZtQQ+0nMKaqGB
+i6SBiDCBhTELMAkGA1UEBhMCVVMxIDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJlMRww
+GgYDVQQLDBNXZWxscyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMg
+Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHmCAQEwDQYJKoZIhvcNAQEFBQADggEBALkVsUSRzCPI
+K0134/iaeycNzXK7mQDKfGYZUMbVmO2rvwNa5U3lHshPcZeG1eMd/ZDJPHV3V3p9+N701NX3leZ0
+bh08rnyd2wIDBSxxSyU+B+NemvVmFymIGjifz6pBA4SXa5M4esowRBskRDPQ5NHcKDj0E0M1NSlj
+qHyita04pO2t/caaH/+Xc/77szWnk4bGdpEA5qxRFsQnMlzbc9qlk1eOPm01JghZ1edE13YgY+es
+E2fDbbFwRnzVlhE9iW9dqKHrjQrawx0zbKPqZxmamX9LPYNRKh3KL4YMon4QLSvUFpULB6ouFJJJ
+tylv2G0xffX8oRAHh84vWdw+WNs=
+-----END CERTIFICATE-----
+
+COMODO ECC Certification Authority
+==================================
+-----BEGIN CERTIFICATE-----
+MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTELMAkGA1UEBhMC
+R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE
+ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBB
+dXRob3JpdHkwHhcNMDgwMzA2MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0Ix
+GzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR
+Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRo
+b3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSRFtSrYpn1PlILBs5BAH+X
+4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0JcfRK9ChQtP6IHG4/bC8vCVlbpVsLM5ni
+wz2J+Wos77LTBumjQjBAMB0GA1UdDgQWBBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8E
+BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VG
+FAkK+qDmfQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdvGDeA
+U/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY=
+-----END CERTIFICATE-----
+
+IGC/A
+=====
+-----BEGIN CERTIFICATE-----
+MIIEAjCCAuqgAwIBAgIFORFFEJQwDQYJKoZIhvcNAQEFBQAwgYUxCzAJBgNVBAYTAkZSMQ8wDQYD
+VQQIEwZGcmFuY2UxDjAMBgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQTS9TR0ROMQ4wDAYDVQQLEwVE
+Q1NTSTEOMAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2LmZy
+MB4XDTAyMTIxMzE0MjkyM1oXDTIwMTAxNzE0MjkyMlowgYUxCzAJBgNVBAYTAkZSMQ8wDQYDVQQI
+EwZGcmFuY2UxDjAMBgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQTS9TR0ROMQ4wDAYDVQQLEwVEQ1NT
+STEOMAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2LmZyMIIB
+IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsh/R0GLFMzvABIaIs9z4iPf930Pfeo2aSVz2
+TqrMHLmh6yeJ8kbpO0px1R2OLc/mratjUMdUC24SyZA2xtgv2pGqaMVy/hcKshd+ebUyiHDKcMCW
+So7kVc0dJ5S/znIq7Fz5cyD+vfcuiWe4u0dzEvfRNWk68gq5rv9GQkaiv6GFGvm/5P9JhfejcIYy
+HF2fYPepraX/z9E0+X1bF8bc1g4oa8Ld8fUzaJ1O/Id8NhLWo4DoQw1VYZTqZDdH6nfK0LJYBcNd
+frGoRpAxVs5wKpayMLh35nnAvSk7/ZR3TL0gzUEl4C7HG7vupARB0l2tEmqKm0f7yd1GQOGdPDPQ
+tQIDAQABo3cwdTAPBgNVHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBRjAVBgNVHSAEDjAMMAoGCCqB
+egF5AQEBMB0GA1UdDgQWBBSjBS8YYFDCiQrdKyFP/45OqDAxNjAfBgNVHSMEGDAWgBSjBS8YYFDC
+iQrdKyFP/45OqDAxNjANBgkqhkiG9w0BAQUFAAOCAQEABdwm2Pp3FURo/C9mOnTgXeQp/wYHE4RK
+q89toB9RlPhJy3Q2FLwV3duJL92PoF189RLrn544pEfMs5bZvpwlqwN+Mw+VgQ39FuCIvjfwbF3Q
+MZsyK10XZZOYYLxuj7GoPB7ZHPOpJkL5ZB3C55L29B5aqhlSXa/oovdgoPaN8In1buAKBQGVyYsg
+Crpa/JosPL3Dt8ldeCUFP1YUmwza+zpI/pdpXsoQhvdOlgQITeywvl3cO45Pwf2aNjSaTFR+FwNI
+lQgRHAdvhQh+XU3Endv7rs6y0bO4g2wdsrN58dhwmX7wEwLOXt1R0982gaEbeC9xs/FZTEYYKKuF
+0mBWWg==
+-----END CERTIFICATE-----
+
+Security Communication EV RootCA1
+=================================
+-----BEGIN CERTIFICATE-----
+MIIDfTCCAmWgAwIBAgIBADANBgkqhkiG9w0BAQUFADBgMQswCQYDVQQGEwJKUDElMCMGA1UEChMc
+U0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEqMCgGA1UECxMhU2VjdXJpdHkgQ29tbXVuaWNh
+dGlvbiBFViBSb290Q0ExMB4XDTA3MDYwNjAyMTIzMloXDTM3MDYwNjAyMTIzMlowYDELMAkGA1UE
+BhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKjAoBgNVBAsTIVNl
+Y3VyaXR5IENvbW11bmljYXRpb24gRVYgUm9vdENBMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
+AQoCggEBALx/7FebJOD+nLpCeamIivqA4PUHKUPqjgo0No0c+qe1OXj/l3X3L+SqawSERMqm4miO
+/VVQYg+kcQ7OBzgtQoVQrTyWb4vVog7P3kmJPdZkLjjlHmy1V4qe70gOzXppFodEtZDkBp2uoQSX
+WHnvIEqCa4wiv+wfD+mEce3xDuS4GBPMVjZd0ZoeUWs5bmB2iDQL87PRsJ3KYeJkHcFGB7hj3R4z
+ZbOOCVVSPbW9/wfrrWFVGCypaZhKqkDFMxRldAD5kd6vA0jFQFTcD4SQaCDFkpbcLuUCRarAX1T4
+bepJz11sS6/vmsJWXMY1VkJqMF/Cq/biPT+zyRGPMUzXn0kCAwEAAaNCMEAwHQYDVR0OBBYEFDVK
+9U2vP9eCOKyrcWUXdYydVZPmMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqG
+SIb3DQEBBQUAA4IBAQCoh+ns+EBnXcPBZsdAS5f8hxOQWsTvoMpfi7ent/HWtWS3irO4G8za+6xm
+iEHO6Pzk2x6Ipu0nUBsCMCRGef4Eh3CXQHPRwMFXGZpppSeZq51ihPZRwSzJIxXYKLerJRO1RuGG
+Av8mjMSIkh1W/hln8lXkgKNrnKt34VFxDSDbEJrbvXZ5B3eZKK2aXtqxT0QsNY6llsf9g/BYxnnW
+mHyojf6GPgcWkuF75x3sM3Z+Qi5KhfmRiWiEA4Glm5q+4zfFVKtWOxgtQaQM+ELbmaDgcm+7XeEW
+T1MKZPlO9L9OVL14bIjqv5wTJMJwaaJ/D8g8rQjJsJhAoyrniIPtd490
+-----END CERTIFICATE-----
+
+OISTE WISeKey Global Root GA CA
+===============================
+-----BEGIN CERTIFICATE-----
+MIID8TCCAtmgAwIBAgIQQT1yx/RrH4FDffHSKFTfmjANBgkqhkiG9w0BAQUFADCBijELMAkGA1UE
+BhMCQ0gxEDAOBgNVBAoTB1dJU2VLZXkxGzAZBgNVBAsTEkNvcHlyaWdodCAoYykgMjAwNTEiMCAG
+A1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBH
+bG9iYWwgUm9vdCBHQSBDQTAeFw0wNTEyMTExNjAzNDRaFw0zNzEyMTExNjA5NTFaMIGKMQswCQYD
+VQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEbMBkGA1UECxMSQ29weXJpZ2h0IChjKSAyMDA1MSIw
+IAYDVQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5
+IEdsb2JhbCBSb290IEdBIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy0+zAJs9
+Nt350UlqaxBJH+zYK7LG+DKBKUOVTJoZIyEVRd7jyBxRVVuuk+g3/ytr6dTqvirdqFEr12bDYVxg
+Asj1znJ7O7jyTmUIms2kahnBAbtzptf2w93NvKSLtZlhuAGio9RN1AU9ka34tAhxZK9w8RxrfvbD
+d50kc3vkDIzh2TbhmYsFmQvtRTEJysIA2/dyoJaqlYfQjse2YXMNdmaM3Bu0Y6Kff5MTMPGhJ9vZ
+/yxViJGg4E8HsChWjBgbl0SOid3gF27nKu+POQoxhILYQBRJLnpB5Kf+42TMwVlxSywhp1t94B3R
+LoGbw9ho972WG6xwsRYUC9tguSYBBQIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw
+AwEB/zAdBgNVHQ4EFgQUswN+rja8sHnR3JQmthG+IbJphpQwEAYJKwYBBAGCNxUBBAMCAQAwDQYJ
+KoZIhvcNAQEFBQADggEBAEuh/wuHbrP5wUOxSPMowB0uyQlB+pQAHKSkq0lPjz0e701vvbyk9vIm
+MMkQyh2I+3QZH4VFvbBsUfk2ftv1TDI6QU9bR8/oCy22xBmddMVHxjtqD6wU2zz0c5ypBd8A3HR4
++vg1YFkCExh8vPtNsCBtQ7tgMHpnM1zFmdH4LTlSc/uMqpclXHLZCB6rTjzjgTGfA6b7wP4piFXa
+hNVQA7bihKOmNqoROgHhGEvWRGizPflTdISzRpFGlgC3gCy24eMQ4tui5yiPAZZiFj4A4xylNoEY
+okxSdsARo27mHbrjWr42U8U+dY+GaSlYU7Wcu2+fXMUY7N0v4ZjJ/L7fCg0=
+-----END CERTIFICATE-----
+
+S-TRUST Authentication and Encryption Root CA 2005 PN
+=====================================================
+-----BEGIN CERTIFICATE-----
+MIIEezCCA2OgAwIBAgIQNxkY5lNUfBq1uMtZWts1tzANBgkqhkiG9w0BAQUFADCBrjELMAkGA1UE
+BhMCREUxIDAeBgNVBAgTF0JhZGVuLVd1ZXJ0dGVtYmVyZyAoQlcpMRIwEAYDVQQHEwlTdHV0dGdh
+cnQxKTAnBgNVBAoTIERldXRzY2hlciBTcGFya2Fzc2VuIFZlcmxhZyBHbWJIMT4wPAYDVQQDEzVT
+LVRSVVNUIEF1dGhlbnRpY2F0aW9uIGFuZCBFbmNyeXB0aW9uIFJvb3QgQ0EgMjAwNTpQTjAeFw0w
+NTA2MjIwMDAwMDBaFw0zMDA2MjEyMzU5NTlaMIGuMQswCQYDVQQGEwJERTEgMB4GA1UECBMXQmFk
+ZW4tV3VlcnR0ZW1iZXJnIChCVykxEjAQBgNVBAcTCVN0dXR0Z2FydDEpMCcGA1UEChMgRGV1dHNj
+aGVyIFNwYXJrYXNzZW4gVmVybGFnIEdtYkgxPjA8BgNVBAMTNVMtVFJVU1QgQXV0aGVudGljYXRp
+b24gYW5kIEVuY3J5cHRpb24gUm9vdCBDQSAyMDA1OlBOMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEA2bVKwdMz6tNGs9HiTNL1toPQb9UY6ZOvJ44TzbUlNlA0EmQpoVXhOmCTnijJ4/Ob
+4QSwI7+Vio5bG0F/WsPoTUzVJBY+h0jUJ67m91MduwwA7z5hca2/OnpYH5Q9XIHV1W/fuJvS9eXL
+g3KSwlOyggLrra1fFi2SU3bxibYs9cEv4KdKb6AwajLrmnQDaHgTncovmwsdvs91DSaXm8f1Xgqf
+eN+zvOyauu9VjxuapgdjKRdZYgkqeQd3peDRF2npW932kKvimAoA0SVtnteFhy+S8dF2g08LOlk3
+KC8zpxdQ1iALCvQm+Z845y2kuJuJja2tyWp9iRe79n+Ag3rm7QIDAQABo4GSMIGPMBIGA1UdEwEB
+/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMCkGA1UdEQQiMCCkHjAcMRowGAYDVQQDExFTVFJv
+bmxpbmUxLTIwNDgtNTAdBgNVHQ4EFgQUD8oeXHngovMpttKFswtKtWXsa1IwHwYDVR0jBBgwFoAU
+D8oeXHngovMpttKFswtKtWXsa1IwDQYJKoZIhvcNAQEFBQADggEBAK8B8O0ZPCjoTVy7pWMciDMD
+pwCHpB8gq9Yc4wYfl35UvbfRssnV2oDsF9eK9XvCAPbpEW+EoFolMeKJ+aQAPzFoLtU96G7m1R08
+P7K9n3frndOMusDXtk3sU5wPBG7qNWdX4wple5A64U8+wwCSersFiXOMy6ZNwPv2AtawB6MDwidA
+nwzkhYItr5pCHdDHjfhA7p0GVxzZotiAFP7hYy0yh9WUUpY6RsZxlj33mA6ykaqP2vROJAA5Veit
+F7nTNCtKqUDMFypVZUF0Qn71wK/Ik63yGFs9iQzbRzkk+OBM8h+wPQrKBU6JIRrjKpms/H+h8Q8b
+Hz2eBIPdltkdOpQ=
+-----END CERTIFICATE-----
+
+Microsec e-Szigno Root CA
+=========================
+-----BEGIN CERTIFICATE-----
+MIIHqDCCBpCgAwIBAgIRAMy4579OKRr9otxmpRwsDxEwDQYJKoZIhvcNAQEFBQAwcjELMAkGA1UE
+BhMCSFUxETAPBgNVBAcTCEJ1ZGFwZXN0MRYwFAYDVQQKEw1NaWNyb3NlYyBMdGQuMRQwEgYDVQQL
+EwtlLVN6aWdubyBDQTEiMCAGA1UEAxMZTWljcm9zZWMgZS1Temlnbm8gUm9vdCBDQTAeFw0wNTA0
+MDYxMjI4NDRaFw0xNzA0MDYxMjI4NDRaMHIxCzAJBgNVBAYTAkhVMREwDwYDVQQHEwhCdWRhcGVz
+dDEWMBQGA1UEChMNTWljcm9zZWMgTHRkLjEUMBIGA1UECxMLZS1Temlnbm8gQ0ExIjAgBgNVBAMT
+GU1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
+AQDtyADVgXvNOABHzNuEwSFpLHSQDCHZU4ftPkNEU6+r+ICbPHiN1I2uuO/TEdyB5s87lozWbxXG
+d36hL+BfkrYn13aaHUM86tnsL+4582pnS4uCzyL4ZVX+LMsvfUh6PXX5qqAnu3jCBspRwn5mS6/N
+oqdNAoI/gqyFxuEPkEeZlApxcpMqyabAvjxWTHOSJ/FrtfX9/DAFYJLG65Z+AZHCabEeHXtTRbjc
+QR/Ji3HWVBTji1R4P770Yjtb9aPs1ZJ04nQw7wHb4dSrmZsqa/i9phyGI0Jf7Enemotb9HI6QMVJ
+PqW+jqpx62z69Rrkav17fVVA71hu5tnVvCSrwe+3AgMBAAGjggQ3MIIEMzBnBggrBgEFBQcBAQRb
+MFkwKAYIKwYBBQUHMAGGHGh0dHBzOi8vcmNhLmUtc3ppZ25vLmh1L29jc3AwLQYIKwYBBQUHMAKG
+IWh0dHA6Ly93d3cuZS1zemlnbm8uaHUvUm9vdENBLmNydDAPBgNVHRMBAf8EBTADAQH/MIIBcwYD
+VR0gBIIBajCCAWYwggFiBgwrBgEEAYGoGAIBAQEwggFQMCgGCCsGAQUFBwIBFhxodHRwOi8vd3d3
+LmUtc3ppZ25vLmh1L1NaU1ovMIIBIgYIKwYBBQUHAgIwggEUHoIBEABBACAAdABhAG4A+gBzAO0A
+dAB2AOEAbgB5ACAA6QByAHQAZQBsAG0AZQB6AOkAcwDpAGgAZQB6ACAA6QBzACAAZQBsAGYAbwBn
+AGEAZADhAHMA4QBoAG8AegAgAGEAIABTAHoAbwBsAGcA4QBsAHQAYQB0APMAIABTAHoAbwBsAGcA
+4QBsAHQAYQB0AOEAcwBpACAAUwB6AGEAYgDhAGwAeQB6AGEAdABhACAAcwB6AGUAcgBpAG4AdAAg
+AGsAZQBsAGwAIABlAGwAagDhAHIAbgBpADoAIABoAHQAdABwADoALwAvAHcAdwB3AC4AZQAtAHMA
+egBpAGcAbgBvAC4AaAB1AC8AUwBaAFMAWgAvMIHIBgNVHR8EgcAwgb0wgbqggbeggbSGIWh0dHA6
+Ly93d3cuZS1zemlnbm8uaHUvUm9vdENBLmNybIaBjmxkYXA6Ly9sZGFwLmUtc3ppZ25vLmh1L0NO
+PU1pY3Jvc2VjJTIwZS1Temlnbm8lMjBSb290JTIwQ0EsT1U9ZS1Temlnbm8lMjBDQSxPPU1pY3Jv
+c2VjJTIwTHRkLixMPUJ1ZGFwZXN0LEM9SFU/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDtiaW5h
+cnkwDgYDVR0PAQH/BAQDAgEGMIGWBgNVHREEgY4wgYuBEGluZm9AZS1zemlnbm8uaHWkdzB1MSMw
+IQYDVQQDDBpNaWNyb3NlYyBlLVN6aWduw7MgUm9vdCBDQTEWMBQGA1UECwwNZS1TemlnbsOzIEhT
+WjEWMBQGA1UEChMNTWljcm9zZWMgS2Z0LjERMA8GA1UEBxMIQnVkYXBlc3QxCzAJBgNVBAYTAkhV
+MIGsBgNVHSMEgaQwgaGAFMegSXUWYYTbMUuE0vE3QJDvTtz3oXakdDByMQswCQYDVQQGEwJIVTER
+MA8GA1UEBxMIQnVkYXBlc3QxFjAUBgNVBAoTDU1pY3Jvc2VjIEx0ZC4xFDASBgNVBAsTC2UtU3pp
+Z25vIENBMSIwIAYDVQQDExlNaWNyb3NlYyBlLVN6aWdubyBSb290IENBghEAzLjnv04pGv2i3Gal
+HCwPETAdBgNVHQ4EFgQUx6BJdRZhhNsxS4TS8TdAkO9O3PcwDQYJKoZIhvcNAQEFBQADggEBANMT
+nGZjWS7KXHAM/IO8VbH0jgdsZifOwTsgqRy7RlRw7lrMoHfqaEQn6/Ip3Xep1fvj1KcExJW4C+FE
+aGAHQzAxQmHl7tnlJNUb3+FKG6qfx1/4ehHqE5MAyopYse7tDk2016g2JnzgOsHVV4Lxdbb9iV/a
+86g4nzUGCM4ilb7N1fy+W955a9x6qWVmvrElWl/tftOsRm1M9DKHtCAE4Gx4sHfRhUZLphK3dehK
+yVZs15KrnfVJONJPU+NVkBHbmJbGSfI+9J8b4PeI3CVimUTYc78/MPMMNz7UwiiAc7EBt51alhQB
+S6kRnSlqLtBdgcDPsiBDxwPgN05dCtxZICU=
+-----END CERTIFICATE-----
+
+Certigna
+========
+-----BEGIN CERTIFICATE-----
+MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNVBAYTAkZSMRIw
+EAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4XDTA3MDYyOTE1MTMwNVoXDTI3
+MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwI
+Q2VydGlnbmEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7q
+XOEm7RFHYeGifBZ4QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyH
+GxnygQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbwzBfsV1/p
+ogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q130yGLMLLGq/jj8UEYkg
+DncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKf
+Irjxwo1p3Po6WAbfAgMBAAGjgbwwgbkwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQ
+tCRZvgHyUtVF9lo53BEwZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJ
+BgNVBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzjAQ/J
+SP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG9w0BAQUFAAOCAQEA
+hQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8hbV6lUmPOEvjvKtpv6zf+EwLHyzs+
+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFncfca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1klu
+PBS1xp81HlDQwY9qcEQCYsuuHWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY
+1gkIl2PlwS6wt0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw
+WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg==
+-----END CERTIFICATE-----
+
+AC Ra\xC3\xADz Certic\xC3\xA1mara S.A.
+======================================
+-----BEGIN CERTIFICATE-----
+MIIGZjCCBE6gAwIBAgIPB35Sk3vgFeNX8GmMy+wMMA0GCSqGSIb3DQEBBQUAMHsxCzAJBgNVBAYT
+AkNPMUcwRQYDVQQKDD5Tb2NpZWRhZCBDYW1lcmFsIGRlIENlcnRpZmljYWNpw7NuIERpZ2l0YWwg
+LSBDZXJ0aWPDoW1hcmEgUy5BLjEjMCEGA1UEAwwaQUMgUmHDrXogQ2VydGljw6FtYXJhIFMuQS4w
+HhcNMDYxMTI3MjA0NjI5WhcNMzAwNDAyMjE0MjAyWjB7MQswCQYDVQQGEwJDTzFHMEUGA1UECgw+
+U29jaWVkYWQgQ2FtZXJhbCBkZSBDZXJ0aWZpY2FjacOzbiBEaWdpdGFsIC0gQ2VydGljw6FtYXJh
+IFMuQS4xIzAhBgNVBAMMGkFDIFJhw616IENlcnRpY8OhbWFyYSBTLkEuMIICIjANBgkqhkiG9w0B
+AQEFAAOCAg8AMIICCgKCAgEAq2uJo1PMSCMI+8PPUZYILrgIem08kBeGqentLhM0R7LQcNzJPNCN
+yu5LF6vQhbCnIwTLqKL85XXbQMpiiY9QngE9JlsYhBzLfDe3fezTf3MZsGqy2IiKLUV0qPezuMDU
+2s0iiXRNWhU5cxh0T7XrmafBHoi0wpOQY5fzp6cSsgkiBzPZkc0OnB8OIMfuuzONj8LSWKdf/WU3
+4ojC2I+GdV75LaeHM/J4Ny+LvB2GNzmxlPLYvEqcgxhaBvzz1NS6jBUJJfD5to0EfhcSM2tXSExP
+2yYe68yQ54v5aHxwD6Mq0Do43zeX4lvegGHTgNiRg0JaTASJaBE8rF9ogEHMYELODVoqDA+bMMCm
+8Ibbq0nXl21Ii/kDwFJnmxL3wvIumGVC2daa49AZMQyth9VXAnow6IYm+48jilSH5L887uvDdUhf
+HjlvgWJsxS3EF1QZtzeNnDeRyPYL1epjb4OsOMLzP96a++EjYfDIJss2yKHzMI+ko6Kh3VOz3vCa
+Mh+DkXkwwakfU5tTohVTP92dsxA7SH2JD/ztA/X7JWR1DhcZDY8AFmd5ekD8LVkH2ZD6mq093ICK
+5lw1omdMEWux+IBkAC1vImHFrEsm5VoQgpukg3s0956JkSCXjrdCx2bD0Omk1vUgjcTDlaxECp1b
+czwmPS9KvqfJpxAe+59QafMCAwEAAaOB5jCB4zAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE
+AwIBBjAdBgNVHQ4EFgQU0QnQ6dfOeXRU+Tows/RtLAMDG2gwgaAGA1UdIASBmDCBlTCBkgYEVR0g
+ADCBiTArBggrBgEFBQcCARYfaHR0cDovL3d3dy5jZXJ0aWNhbWFyYS5jb20vZHBjLzBaBggrBgEF
+BQcCAjBOGkxMaW1pdGFjaW9uZXMgZGUgZ2FyYW507WFzIGRlIGVzdGUgY2VydGlmaWNhZG8gc2Ug
+cHVlZGVuIGVuY29udHJhciBlbiBsYSBEUEMuMA0GCSqGSIb3DQEBBQUAA4ICAQBclLW4RZFNjmEf
+AygPU3zmpFmps4p6xbD/CHwso3EcIRNnoZUSQDWDg4902zNc8El2CoFS3UnUmjIz75uny3XlesuX
+EpBcunvFm9+7OSPI/5jOCk0iAUgHforA1SBClETvv3eiiWdIG0ADBaGJ7M9i4z0ldma/Jre7Ir5v
+/zlXdLp6yQGVwZVR6Kss+LGGIOk/yzVb0hfpKv6DExdA7ohiZVvVO2Dpezy4ydV/NgIlqmjCMRW3
+MGXrfx1IebHPOeJCgBbT9ZMj/EyXyVo3bHwi2ErN0o42gzmRkBDI8ck1fj+404HGIGQatlDCIaR4
+3NAvO2STdPCWkPHv+wlaNECW8DYSwaN0jJN+Qd53i+yG2dIPPy3RzECiiWZIHiCznCNZc6lEc7wk
+eZBWN7PGKX6jD/EpOe9+XCgycDWs2rjIdWb8m0w5R44bb5tNAlQiM+9hup4phO9OSzNHdpdqy35f
+/RWmnkJDW2ZaiogN9xa5P1FlK2Zqi9E4UqLWRhH6/JocdJ6PlwsCT2TG9WjTSy3/pDceiz+/RL5h
+RqGEPQgnTIEgd4kI6mdAXmwIUV80WoyWaM3X94nCHNMyAK9Sy9NgWyo6R35rMDOhYil/SrnhLecU
+Iw4OGEfhefwVVdCx/CVxY3UzHCMrr1zZ7Ud3YA47Dx7SwNxkBYn8eNZcLCZDqQ==
+-----END CERTIFICATE-----
+
+TC TrustCenter Class 2 CA II
+============================
+-----BEGIN CERTIFICATE-----
+MIIEqjCCA5KgAwIBAgIOLmoAAQACH9dSISwRXDswDQYJKoZIhvcNAQEFBQAwdjELMAkGA1UEBhMC
+REUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNVBAsTGVRDIFRydXN0Q2VudGVy
+IENsYXNzIDIgQ0ExJTAjBgNVBAMTHFRDIFRydXN0Q2VudGVyIENsYXNzIDIgQ0EgSUkwHhcNMDYw
+MTEyMTQzODQzWhcNMjUxMjMxMjI1OTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1
+c3RDZW50ZXIgR21iSDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMiBDQTElMCMGA1UE
+AxMcVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMiBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
+AQoCggEBAKuAh5uO8MN8h9foJIIRszzdQ2Lu+MNF2ujhoF/RKrLqk2jftMjWQ+nEdVl//OEd+DFw
+IxuInie5e/060smp6RQvkL4DUsFJzfb95AhmC1eKokKguNV/aVyQMrKXDcpK3EY+AlWJU+MaWss2
+xgdW94zPEfRMuzBwBJWl9jmM/XOBCH2JXjIeIqkiRUuwZi4wzJ9l/fzLganx4Duvo4bRierERXlQ
+Xa7pIXSSTYtZgo+U4+lK8edJsBTj9WLL1XK9H7nSn6DNqPoByNkN39r8R52zyFTfSUrxIan+GE7u
+SNQZu+995OKdy1u2bv/jzVrndIIFuoAlOMvkaZ6vQaoahPUCAwEAAaOCATQwggEwMA8GA1UdEwEB
+/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTjq1RMgKHbVkO3kUrL84J6E1wIqzCB
+7QYDVR0fBIHlMIHiMIHfoIHcoIHZhjVodHRwOi8vd3d3LnRydXN0Y2VudGVyLmRlL2NybC92Mi90
+Y19jbGFzc18yX2NhX0lJLmNybIaBn2xkYXA6Ly93d3cudHJ1c3RjZW50ZXIuZGUvQ049VEMlMjBU
+cnVzdENlbnRlciUyMENsYXNzJTIwMiUyMENBJTIwSUksTz1UQyUyMFRydXN0Q2VudGVyJTIwR21i
+SCxPVT1yb290Y2VydHMsREM9dHJ1c3RjZW50ZXIsREM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9u
+TGlzdD9iYXNlPzANBgkqhkiG9w0BAQUFAAOCAQEAjNfffu4bgBCzg/XbEeprS6iSGNn3Bzn1LL4G
+dXpoUxUc6krtXvwjshOg0wn/9vYua0Fxec3ibf2uWWuFHbhOIprtZjluS5TmVfwLG4t3wVMTZonZ
+KNaL80VKY7f9ewthXbhtvsPcW3nS7Yblok2+XnR8au0WOB9/WIFaGusyiC2y8zl3gK9etmF1Kdsj
+TYjKUCjLhdLTEKJZbtOTVAB6okaVhgWcqRmY5TFyDADiZ9lA4CQze28suVyrZZ0srHbqNZn1l7kP
+JOzHdiEoZa5X6AeIdUpWoNIFOqTmjZKILPPy4cHGYdtBxceb9w4aUUXCYWvcZCcXjFq32nQozZfk
+vQ==
+-----END CERTIFICATE-----
+
+TC TrustCenter Class 3 CA II
+============================
+-----BEGIN CERTIFICATE-----
+MIIEqjCCA5KgAwIBAgIOSkcAAQAC5aBd1j8AUb8wDQYJKoZIhvcNAQEFBQAwdjELMAkGA1UEBhMC
+REUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNVBAsTGVRDIFRydXN0Q2VudGVy
+IENsYXNzIDMgQ0ExJTAjBgNVBAMTHFRDIFRydXN0Q2VudGVyIENsYXNzIDMgQ0EgSUkwHhcNMDYw
+MTEyMTQ0MTU3WhcNMjUxMjMxMjI1OTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1
+c3RDZW50ZXIgR21iSDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQTElMCMGA1UE
+AxMcVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
+AQoCggEBALTgu1G7OVyLBMVMeRwjhjEQY0NVJz/GRcekPewJDRoeIMJWHt4bNwcwIi9v8Qbxq63W
+yKthoy9DxLCyLfzDlml7forkzMA5EpBCYMnMNWju2l+QVl/NHE1bWEnrDgFPZPosPIlY2C8u4rBo
+6SI7dYnWRBpl8huXJh0obazovVkdKyT21oQDZogkAHhg8fir/gKya/si+zXmFtGt9i4S5Po1auUZ
+uV3bOx4a+9P/FRQI2AlqukWdFHlgfa9Aigdzs5OW03Q0jTo3Kd5c7PXuLjHCINy+8U9/I1LZW+Jk
+2ZyqBwi1Rb3R0DHBq1SfqdLDYmAD8bs5SpJKPQq5ncWg/jcCAwEAAaOCATQwggEwMA8GA1UdEwEB
+/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTUovyfs8PYA9NXXAek0CSnwPIA1DCB
+7QYDVR0fBIHlMIHiMIHfoIHcoIHZhjVodHRwOi8vd3d3LnRydXN0Y2VudGVyLmRlL2NybC92Mi90
+Y19jbGFzc18zX2NhX0lJLmNybIaBn2xkYXA6Ly93d3cudHJ1c3RjZW50ZXIuZGUvQ049VEMlMjBU
+cnVzdENlbnRlciUyMENsYXNzJTIwMyUyMENBJTIwSUksTz1UQyUyMFRydXN0Q2VudGVyJTIwR21i
+SCxPVT1yb290Y2VydHMsREM9dHJ1c3RjZW50ZXIsREM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9u
+TGlzdD9iYXNlPzANBgkqhkiG9w0BAQUFAAOCAQEANmDkcPcGIEPZIxpC8vijsrlNirTzwppVMXzE
+O2eatN9NDoqTSheLG43KieHPOh6sHfGcMrSOWXaiQYUlN6AT0PV8TtXqluJucsG7Kv5sbviRmEb8
+yRtXW+rIGjs/sFGYPAfaLFkB2otE6OF0/ado3VS6g0bsyEa1+K+XwDsJHI/OcpY9M1ZwvJbL2NV9
+IJqDnxrcOfHFcqMRA/07QlIp2+gB95tejNaNhk4Z+rwcvsUhpYeeeC422wlxo3I0+GzjBgnyXlal
+092Y+tTmBvTwtiBjS+opvaqCZh77gaqnN60TGOaSw4HBM7uIHqHn4rS9MWwOUT1v+5ZWgOI2F9Hc
+5A==
+-----END CERTIFICATE-----
+
+TC TrustCenter Universal CA I
+=============================
+-----BEGIN CERTIFICATE-----
+MIID3TCCAsWgAwIBAgIOHaIAAQAC7LdggHiNtgYwDQYJKoZIhvcNAQEFBQAweTELMAkGA1UEBhMC
+REUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxJDAiBgNVBAsTG1RDIFRydXN0Q2VudGVy
+IFVuaXZlcnNhbCBDQTEmMCQGA1UEAxMdVEMgVHJ1c3RDZW50ZXIgVW5pdmVyc2FsIENBIEkwHhcN
+MDYwMzIyMTU1NDI4WhcNMjUxMjMxMjI1OTU5WjB5MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMg
+VHJ1c3RDZW50ZXIgR21iSDEkMCIGA1UECxMbVEMgVHJ1c3RDZW50ZXIgVW5pdmVyc2FsIENBMSYw
+JAYDVQQDEx1UQyBUcnVzdENlbnRlciBVbml2ZXJzYWwgQ0EgSTCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBAKR3I5ZEr5D0MacQ9CaHnPM42Q9e3s9B6DGtxnSRJJZ4Hgmgm5qVSkr1YnwC
+qMqs+1oEdjneX/H5s7/zA1hV0qq34wQi0fiU2iIIAI3TfCZdzHd55yx4Oagmcw6iXSVphU9VDprv
+xrlE4Vc93x9UIuVvZaozhDrzznq+VZeujRIPFDPiUHDDSYcTvFHe15gSWu86gzOSBnWLknwSaHtw
+ag+1m7Z3W0hZneTvWq3zwZ7U10VOylY0Ibw+F1tvdwxIAUMpsN0/lm7mlaoMwCC2/T42J5zjXM9O
+gdwZu5GQfezmlwQek8wiSdeXhrYTCjxDI3d+8NzmzSQfO4ObNDqDNOMCAwEAAaNjMGEwHwYDVR0j
+BBgwFoAUkqR1LKSevoFE63n8isWVpesQdXMwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
+AYYwHQYDVR0OBBYEFJKkdSyknr6BROt5/IrFlaXrEHVzMA0GCSqGSIb3DQEBBQUAA4IBAQAo0uCG
+1eb4e/CX3CJrO5UUVg8RMKWaTzqwOuAGy2X17caXJ/4l8lfmXpWMPmRgFVp/Lw0BxbFg/UU1z/Cy
+vwbZ71q+s2IhtNerNXxTPqYn8aEt2hojnczd7Dwtnic0XQ/CNnm8yUpiLe1r2X1BQ3y2qsrtYbE3
+ghUJGooWMNjsydZHcnhLEEYUjl8Or+zHL6sQ17bxbuyGssLoDZJz3KL0Dzq/YSMQiZxIQG5wALPT
+ujdEWBF6AmqI8Dc08BnprNRlc/ZpjGSUOnmFKbAWKwyCPwacx/0QK54PLLae4xW/2TYcuiUaUj0a
+7CIMHOCkoj3w6DnPgcB77V0fb8XQC9eY
+-----END CERTIFICATE-----
+
+Deutsche Telekom Root CA 2
+==========================
+-----BEGIN CERTIFICATE-----
+MIIDnzCCAoegAwIBAgIBJjANBgkqhkiG9w0BAQUFADBxMQswCQYDVQQGEwJERTEcMBoGA1UEChMT
+RGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxlU2VjIFRydXN0IENlbnRlcjEjMCEG
+A1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290IENBIDIwHhcNOTkwNzA5MTIxMTAwWhcNMTkwNzA5
+MjM1OTAwWjBxMQswCQYDVQQGEwJERTEcMBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0G
+A1UECxMWVC1UZWxlU2VjIFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBS
+b290IENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrC6M14IspFLEUha88EOQ5
+bzVdSq7d6mGNlUn0b2SjGmBmpKlAIoTZ1KXleJMOaAGtuU1cOs7TuKhCQN/Po7qCWWqSG6wcmtoI
+KyUn+WkjR/Hg6yx6m/UTAtB+NHzCnjwAWav12gz1MjwrrFDa1sPeg5TKqAyZMg4ISFZbavva4VhY
+AUlfckE8FQYBjl2tqriTtM2e66foai1SNNs671x1Udrb8zH57nGYMsRUFUQM+ZtV7a3fGAigo4aK
+Se5TBY8ZTNXeWHmb0mocQqvF1afPaA+W5OFhmHZhyJF81j4A4pFQh+GdCuatl9Idxjp9y7zaAzTV
+jlsB9WoHtxa2bkp/AgMBAAGjQjBAMB0GA1UdDgQWBBQxw3kbuvVT1xfgiXotF2wKsyudMzAPBgNV
+HRMECDAGAQH/AgEFMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAlGRZrTlk5ynr
+E/5aw4sTV8gEJPB0d8Bg42f76Ymmg7+Wgnxu1MM9756AbrsptJh6sTtU6zkXR34ajgv8HzFZMQSy
+zhfzLMdiNlXiItiJVbSYSKpk+tYcNthEeFpaIzpXl/V6ME+un2pMSyuOoAPjPuCp1NJ70rOo4nI8
+rZ7/gFnkm0W09juwzTkZmDLl6iFhkOQxIY40sfcvNUqFENrnijchvllj4PKFiDFT1FQUhXB59C4G
+dyd1Lx+4ivn+xbrYNuSD7Odlt79jWvNGr4GUN9RBjNYj1h7P9WgbRGOiWrqnNVmh5XAFmw4jV5mU
+Cm26OWMohpLzGITY+9HPBVZkVw==
+-----END CERTIFICATE-----
+
+ComSign CA
+==========
+-----BEGIN CERTIFICATE-----
+MIIDkzCCAnugAwIBAgIQFBOWgxRVjOp7Y+X8NId3RDANBgkqhkiG9w0BAQUFADA0MRMwEQYDVQQD
+EwpDb21TaWduIENBMRAwDgYDVQQKEwdDb21TaWduMQswCQYDVQQGEwJJTDAeFw0wNDAzMjQxMTMy
+MThaFw0yOTAzMTkxNTAyMThaMDQxEzARBgNVBAMTCkNvbVNpZ24gQ0ExEDAOBgNVBAoTB0NvbVNp
+Z24xCzAJBgNVBAYTAklMMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8ORUaSvTx49q
+ROR+WCf4C9DklBKK8Rs4OC8fMZwG1Cyn3gsqrhqg455qv588x26i+YtkbDqthVVRVKU4VbirgwTy
+P2Q298CNQ0NqZtH3FyrV7zb6MBBC11PN+fozc0yz6YQgitZBJzXkOPqUm7h65HkfM/sb2CEJKHxN
+GGleZIp6GZPKfuzzcuc3B1hZKKxC+cX/zT/npfo4sdAMx9lSGlPWgcxCejVb7Us6eva1jsz/D3zk
+YDaHL63woSV9/9JLEYhwVKZBqGdTUkJe5DSe5L6j7KpiXd3DTKaCQeQzC6zJMw9kglcq/QytNuEM
+rkvF7zuZ2SOzW120V+x0cAwqTwIDAQABo4GgMIGdMAwGA1UdEwQFMAMBAf8wPQYDVR0fBDYwNDAy
+oDCgLoYsaHR0cDovL2ZlZGlyLmNvbXNpZ24uY28uaWwvY3JsL0NvbVNpZ25DQS5jcmwwDgYDVR0P
+AQH/BAQDAgGGMB8GA1UdIwQYMBaAFEsBmz5WGmU2dst7l6qSBe4y5ygxMB0GA1UdDgQWBBRLAZs+
+VhplNnbLe5eqkgXuMucoMTANBgkqhkiG9w0BAQUFAAOCAQEA0Nmlfv4pYEWdfoPPbrxHbvUanlR2
+QnG0PFg/LUAlQvaBnPGJEMgOqnhPOAlXsDzACPw1jvFIUY0McXS6hMTXcpuEfDhOZAYnKuGntewI
+mbQKDdSFc8gS4TXt8QUxHXOZDOuWyt3T5oWq8Ir7dcHyCTxlZWTzTNity4hp8+SDtwy9F1qWF8pb
+/627HOkthIDYIb6FUtnUdLlphbpN7Sgy6/lhSuTENh4Z3G+EER+V9YMoGKgzkkMn3V0TBEVPh9VG
+zT2ouvDzuFYkRes3x+F2T3I5GN9+dHLHcy056mDmrRGiVod7w2ia/viMcKjfZTL0pECMocJEAw6U
+AGegcQCCSA==
+-----END CERTIFICATE-----
+
+ComSign Secured CA
+==================
+-----BEGIN CERTIFICATE-----
+MIIDqzCCApOgAwIBAgIRAMcoRwmzuGxFjB36JPU2TukwDQYJKoZIhvcNAQEFBQAwPDEbMBkGA1UE
+AxMSQ29tU2lnbiBTZWN1cmVkIENBMRAwDgYDVQQKEwdDb21TaWduMQswCQYDVQQGEwJJTDAeFw0w
+NDAzMjQxMTM3MjBaFw0yOTAzMTYxNTA0NTZaMDwxGzAZBgNVBAMTEkNvbVNpZ24gU2VjdXJlZCBD
+QTEQMA4GA1UEChMHQ29tU2lnbjELMAkGA1UEBhMCSUwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
+ggEKAoIBAQDGtWhfHZQVw6QIVS3joFd67+l0Kru5fFdJGhFeTymHDEjWaueP1H5XJLkGieQcPOqs
+49ohgHMhCu95mGwfCP+hUH3ymBvJVG8+pSjsIQQPRbsHPaHA+iqYHU4Gk/v1iDurX8sWv+bznkqH
+7Rnqwp9D5PGBpX8QTz7RSmKtUxvLg/8HZaWSLWapW7ha9B20IZFKF3ueMv5WJDmyVIRD9YTC2LxB
+kMyd1mja6YJQqTtoz7VdApRgFrFD2UNd3V2Hbuq7s8lr9gOUCXDeFhF6K+h2j0kQmHe5Y1yLM5d1
+9guMsqtb3nQgJT/j8xH5h2iGNXHDHYwt6+UarA9z1YJZQIDTAgMBAAGjgacwgaQwDAYDVR0TBAUw
+AwEB/zBEBgNVHR8EPTA7MDmgN6A1hjNodHRwOi8vZmVkaXIuY29tc2lnbi5jby5pbC9jcmwvQ29t
+U2lnblNlY3VyZWRDQS5jcmwwDgYDVR0PAQH/BAQDAgGGMB8GA1UdIwQYMBaAFMFL7XC29z58ADsA
+j8c+DkWfHl3sMB0GA1UdDgQWBBTBS+1wtvc+fAA7AI/HPg5Fnx5d7DANBgkqhkiG9w0BAQUFAAOC
+AQEAFs/ukhNQq3sUnjO2QiBq1BW9Cav8cujvR3qQrFHBZE7piL1DRYHjZiM/EoZNGeQFsOY3wo3a
+BijJD4mkU6l1P7CW+6tMM1X5eCZGbxs2mPtCdsGCuY7e+0X5YxtiOzkGynd6qDwJz2w2PQ8KRUtp
+FhpFfTMDZflScZAmlaxMDPWLkz/MdXSFmLr/YnpNH4n+rr2UAJm/EaXc4HnFFgt9AmEd6oX5AhVP
+51qJThRv4zdLhfXBPGHg/QVBspJ/wx2g0K5SZGBrGMYmnNj1ZOQ2GmKfig8+/21OGVZOIJFsnzQz
+OjRXUDpvgV4GxvU+fE6OK85lBi5d0ipTdF7Tbieejw==
+-----END CERTIFICATE-----
+
+Cybertrust Global Root
+======================
+-----BEGIN CERTIFICATE-----
+MIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYGA1UEChMPQ3li
+ZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBSb290MB4XDTA2MTIxNTA4
+MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQD
+ExZDeWJlcnRydXN0IEdsb2JhbCBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
++Mi8vRRQZhP/8NN57CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW
+0ozSJ8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2yHLtgwEZL
+AfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iPt3sMpTjr3kfb1V05/Iin
+89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNzFtApD0mpSPCzqrdsxacwOUBdrsTiXSZT
+8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAYXSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAP
+BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2
+MDSgMqAwhi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3JsMB8G
+A1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUAA4IBAQBW7wojoFRO
+lZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMjWqd8BfP9IjsO0QbE2zZMcwSO5bAi
+5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUxXOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2
+hO0j9n0Hq0V+09+zv+mKts2oomcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+T
+X3EJIrduPuocA06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW
+WL1WMRJOEcgh4LMRkWXbtKaIOM5V
+-----END CERTIFICATE-----
+
+ePKI Root Certification Authority
+=================================
+-----BEGIN CERTIFICATE-----
+MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBeMQswCQYDVQQG
+EwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0ZC4xKjAoBgNVBAsMIWVQS0kg
+Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMx
+MjdaMF4xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEq
+MCgGA1UECwwhZVBLSSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0B
+AQEFAAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAHSyZbCUNs
+IZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAhijHyl3SJCRImHJ7K2RKi
+lTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3XDZoTM1PRYfl61dd4s5oz9wCGzh1NlDiv
+qOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX
+12ruOzjjK9SXDrkb5wdJfzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0O
+WQqraffAsgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uUWH1+
+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLSnT0IFaUQAS2zMnao
+lQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pHdmX2Os+PYhcZewoozRrSgx4hxyy/
+vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJipNiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXi
+Zo1jDiVN1Rmy5nk3pyKdVDECAwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/Qkqi
+MAwGA1UdEwQFMAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH
+ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGBuvl2ICO1J2B0
+1GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6YlPwZpVnPDimZI+ymBV3QGypzq
+KOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkPJXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdV
+xrsStZf0X4OFunHB2WyBEXYKCrC/gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEP
+NXubrjlpC2JgQCA2j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+r
+GNm65ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUBo2M3IUxE
+xJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS/jQ6fbjpKdx2qcgw+BRx
+gMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2zGp1iro2C6pSe3VkQw63d4k3jMdXH7Ojy
+sP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTEW9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmOD
+BCEIZ43ygknQW/2xzQ+DhNQ+IIX3Sj0rnP0qCglN6oH4EZw=
+-----END CERTIFICATE-----
+
+T\xc3\x9c\x42\xC4\xB0TAK UEKAE K\xC3\xB6k Sertifika Hizmet Sa\xC4\x9Flay\xc4\xb1\x63\xc4\xb1s\xc4\xb1 - S\xC3\xBCr\xC3\xBCm 3
+=============================================================================================================================
+-----BEGIN CERTIFICATE-----
+MIIFFzCCA/+gAwIBAgIBETANBgkqhkiG9w0BAQUFADCCASsxCzAJBgNVBAYTAlRSMRgwFgYDVQQH
+DA9HZWJ6ZSAtIEtvY2FlbGkxRzBFBgNVBAoMPlTDvHJraXllIEJpbGltc2VsIHZlIFRla25vbG9q
+aWsgQXJhxZ90xLFybWEgS3VydW11IC0gVMOcQsSwVEFLMUgwRgYDVQQLDD9VbHVzYWwgRWxla3Ry
+b25payB2ZSBLcmlwdG9sb2ppIEFyYcWfdMSxcm1hIEVuc3RpdMO8c8O8IC0gVUVLQUUxIzAhBgNV
+BAsMGkthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppMUowSAYDVQQDDEFUw5xCxLBUQUsgVUVLQUUg
+S8O2ayBTZXJ0aWZpa2EgSGl6bWV0IFNhxJ9sYXnEsWPEsXPEsSAtIFPDvHLDvG0gMzAeFw0wNzA4
+MjQxMTM3MDdaFw0xNzA4MjExMTM3MDdaMIIBKzELMAkGA1UEBhMCVFIxGDAWBgNVBAcMD0dlYnpl
+IC0gS29jYWVsaTFHMEUGA1UECgw+VMO8cmtpeWUgQmlsaW1zZWwgdmUgVGVrbm9sb2ppayBBcmHF
+n3TEsXJtYSBLdXJ1bXUgLSBUw5xCxLBUQUsxSDBGBgNVBAsMP1VsdXNhbCBFbGVrdHJvbmlrIHZl
+IEtyaXB0b2xvamkgQXJhxZ90xLFybWEgRW5zdGl0w7xzw7wgLSBVRUtBRTEjMCEGA1UECwwaS2Ft
+dSBTZXJ0aWZpa2FzeW9uIE1lcmtlemkxSjBIBgNVBAMMQVTDnELEsFRBSyBVRUtBRSBLw7ZrIFNl
+cnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxIC0gU8O8csO8bSAzMIIBIjANBgkqhkiG9w0B
+AQEFAAOCAQ8AMIIBCgKCAQEAim1L/xCIOsP2fpTo6iBkcK4hgb46ezzb8R1Sf1n68yJMlaCQvEhO
+Eav7t7WNeoMojCZG2E6VQIdhn8WebYGHV2yKO7Rm6sxA/OOqbLLLAdsyv9Lrhc+hDVXDWzhXcLh1
+xnnRFDDtG1hba+818qEhTsXOfJlfbLm4IpNQp81McGq+agV/E5wrHur+R84EpW+sky58K5+eeROR
+6Oqeyjh1jmKwlZMq5d/pXpduIF9fhHpEORlAHLpVK/swsoHvhOPc7Jg4OQOFCKlUAwUp8MmPi+oL
+hmUZEdPpCSPeaJMDyTYcIW7OjGbxmTDY17PDHfiBLqi9ggtm/oLL4eAagsNAgQIDAQABo0IwQDAd
+BgNVHQ4EFgQUvYiHyY/2pAoLquvF/pEjnatKijIwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF
+MAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAB18+kmPNOm3JpIWmgV050vQbTlswyb2zrgxvMTfvCr4
+N5EY3ATIZJkrGG2AA1nJrvhY0D7twyOfaTyGOBye79oneNGEN3GKPEs5z35FBtYt2IpNeBLWrcLT
+y9LQQfMmNkqblWwM7uXRQydmwYj3erMgbOqwaSvHIOgMA8RBBZniP+Rr+KCGgceExh/VS4ESshYh
+LBOhgLJeDEoTniDYYkCrkOpkSi+sDQESeUWoL4cZaMjihccwsnX5OD+ywJO0a+IDRM5noN+J1q2M
+dqMTw5RhK2vZbMEHCiIHhWyFJEapvj+LeISCfiQMnf2BN+MlqO02TpUsyZyQ2uypQjyttgI=
+-----END CERTIFICATE-----
+
+Buypass Class 2 CA 1
+====================
+-----BEGIN CERTIFICATE-----
+MIIDUzCCAjugAwIBAgIBATANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU
+QnV5cGFzcyBBUy05ODMxNjMzMjcxHTAbBgNVBAMMFEJ1eXBhc3MgQ2xhc3MgMiBDQSAxMB4XDTA2
+MTAxMzEwMjUwOVoXDTE2MTAxMzEwMjUwOVowSzELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBh
+c3MgQVMtOTgzMTYzMzI3MR0wGwYDVQQDDBRCdXlwYXNzIENsYXNzIDIgQ0EgMTCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBAIs8B0XY9t/mx8q6jUPFR42wWsE425KEHK8T1A9vNkYgxC7M
+cXA0ojTTNy7Y3Tp3L8DrKehc0rWpkTSHIln+zNvnma+WwajHQN2lFYxuyHyXA8vmIPLXl18xoS83
+0r7uvqmtqEyeIWZDO6i88wmjONVZJMHCR3axiFyCO7srpgTXjAePzdVBHfCuuCkslFJgNJQ72uA4
+0Z0zPhX0kzLFANq1KWYOOngPIVJfAuWSeyXTkh4vFZ2B5J2O6O+JzhRMVB0cgRJNcKi+EAUXfh/R
+uFdV7c27UsKwHnjCTTZoy1YmwVLBvXb3WNVyfh9EdrsAiR0WnVE1703CVu9r4Iw7DekCAwEAAaNC
+MEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUP42aWYv8e3uco684sDntkHGA1sgwDgYDVR0P
+AQH/BAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQAVGn4TirnoB6NLJzKyQJHyIdFkhb5jatLPgcIV
+1Xp+DCmsNx4cfHZSldq1fyOhKXdlyTKdqC5Wq2B2zha0jX94wNWZUYN/Xtm+DKhQ7SLHrQVMdvvt
+7h5HZPb3J31cKA9FxVxiXqaakZG3Uxcu3K1gnZZkOb1naLKuBctN518fV4bVIJwo+28TOPX2EZL2
+fZleHwzoq0QkKXJAPTZSr4xYkHPB7GEseaHsh7U/2k3ZIQAw3pDaDtMaSKk+hQsUi4y8QZ5q9w5w
+wDX3OaJdZtB7WZ+oRxKaJyOkLY4ng5IgodcVf/EuGO70SH8vf/GhGLWhC5SgYiAynB321O+/TIho
+-----END CERTIFICATE-----
+
+Buypass Class 3 CA 1
+====================
+-----BEGIN CERTIFICATE-----
+MIIDUzCCAjugAwIBAgIBAjANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU
+QnV5cGFzcyBBUy05ODMxNjMzMjcxHTAbBgNVBAMMFEJ1eXBhc3MgQ2xhc3MgMyBDQSAxMB4XDTA1
+MDUwOTE0MTMwM1oXDTE1MDUwOTE0MTMwM1owSzELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBh
+c3MgQVMtOTgzMTYzMzI3MR0wGwYDVQQDDBRCdXlwYXNzIENsYXNzIDMgQ0EgMTCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBAKSO13TZKWTeXx+HgJHqTjnmGcZEC4DVC69TB4sSveZn8AKx
+ifZgisRbsELRwCGoy+Gb72RRtqfPFfV0gGgEkKBYouZ0plNTVUhjP5JW3SROjvi6K//zNIqeKNc0
+n6wv1g/xpC+9UrJJhW05NfBEMJNGJPO251P7vGGvqaMU+8IXF4Rs4HyI+MkcVyzwPX6UvCWThOia
+AJpFBUJXgPROztmuOfbIUxAMZTpHe2DC1vqRycZxbL2RhzyRhkmr8w+gbCZ2Xhysm3HljbybIR6c
+1jh+JIAVMYKWsUnTYjdbiAwKYjT+p0h+mbEwi5A3lRyoH6UsjfRVyNvdWQrCrXig9IsCAwEAAaNC
+MEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUOBTmyPCppAP0Tj4io1vy1uCtQHQwDgYDVR0P
+AQH/BAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQABZ6OMySU9E2NdFm/soT4JXJEVKirZgCFPBdy7
+pYmrEzMqnji3jG8CcmPHc3ceCQa6Oyh7pEfJYWsICCD8igWKH7y6xsL+z27sEzNxZy5p+qksP2bA
+EllNC1QCkoS72xLvg3BweMhT+t/Gxv/ciC8HwEmdMldg0/L2mSlf56oBzKwzqBwKu5HEA6BvtjT5
+htOzdlSY9EqBs1OdTUDs5XcTRa9bqh/YL0yCe/4qxFi7T/ye/QNlGioOw6UgFpRreaaiErS7GqQj
+el/wroQk5PMr+4okoyeYZdowdXb8GZHo2+ubPzK/QJcHJrrM85SFSnonk8+QQtS4Wxam58tAA915
+-----END CERTIFICATE-----
+
+EBG Elektronik Sertifika Hizmet Sa\xC4\x9Flay\xc4\xb1\x63\xc4\xb1s\xc4\xb1
+==========================================================================
+-----BEGIN CERTIFICATE-----
+MIIF5zCCA8+gAwIBAgIITK9zQhyOdAIwDQYJKoZIhvcNAQEFBQAwgYAxODA2BgNVBAMML0VCRyBF
+bGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxMTcwNQYDVQQKDC5FQkcg
+QmlsacWfaW0gVGVrbm9sb2ppbGVyaSB2ZSBIaXptZXRsZXJpIEEuxZ4uMQswCQYDVQQGEwJUUjAe
+Fw0wNjA4MTcwMDIxMDlaFw0xNjA4MTQwMDMxMDlaMIGAMTgwNgYDVQQDDC9FQkcgRWxla3Ryb25p
+ayBTZXJ0aWZpa2EgSGl6bWV0IFNhxJ9sYXnEsWPEsXPEsTE3MDUGA1UECgwuRUJHIEJpbGnFn2lt
+IFRla25vbG9qaWxlcmkgdmUgSGl6bWV0bGVyaSBBLsWeLjELMAkGA1UEBhMCVFIwggIiMA0GCSqG
+SIb3DQEBAQUAA4ICDwAwggIKAoICAQDuoIRh0DpqZhAy2DE4f6en5f2h4fuXd7hxlugTlkaDT7by
+X3JWbhNgpQGR4lvFzVcfd2NR/y8927k/qqk153nQ9dAktiHq6yOU/im/+4mRDGSaBUorzAzu8T2b
+gmmkTPiab+ci2hC6X5L8GCcKqKpE+i4stPtGmggDg3KriORqcsnlZR9uKg+ds+g75AxuetpX/dfr
+eYteIAbTdgtsApWjluTLdlHRKJ2hGvxEok3MenaoDT2/F08iiFD9rrbskFBKW5+VQarKD7JK/oCZ
+TqNGFav4c0JqwmZ2sQomFd2TkuzbqV9UIlKRcF0T6kjsbgNs2d1s/OsNA/+mgxKb8amTD8UmTDGy
+Y5lhcucqZJnSuOl14nypqZoaqsNW2xCaPINStnuWt6yHd6i58mcLlEOzrz5z+kI2sSXFCjEmN1Zn
+uqMLfdb3ic1nobc6HmZP9qBVFCVMLDMNpkGMvQQxahByCp0OLna9XvNRiYuoP1Vzv9s6xiQFlpJI
+qkuNKgPlV5EQ9GooFW5Hd4RcUXSfGenmHmMWOeMRFeNYGkS9y8RsZteEBt8w9DeiQyJ50hBs37vm
+ExH8nYQKE3vwO9D8owrXieqWfo1IhR5kX9tUoqzVegJ5a9KK8GfaZXINFHDk6Y54jzJ0fFfy1tb0
+Nokb+Clsi7n2l9GkLqq+CxnCRelwXQIDAJ3Zo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB
+/wQEAwIBBjAdBgNVHQ4EFgQU587GT/wWZ5b6SqMHwQSny2re2kcwHwYDVR0jBBgwFoAU587GT/wW
+Z5b6SqMHwQSny2re2kcwDQYJKoZIhvcNAQEFBQADggIBAJuYml2+8ygjdsZs93/mQJ7ANtyVDR2t
+FcU22NU57/IeIl6zgrRdu0waypIN30ckHrMk2pGI6YNw3ZPX6bqz3xZaPt7gyPvT/Wwp+BVGoGgm
+zJNSroIBk5DKd8pNSe/iWtkqvTDOTLKBtjDOWU/aWR1qeqRFsIImgYZ29fUQALjuswnoT4cCB64k
+XPBfrAowzIpAoHMEwfuJJPaaHFy3PApnNgUIMbOv2AFoKuB4j3TeuFGkjGwgPaL7s9QJ/XvCgKqT
+bCmYIai7FvOpEl90tYeY8pUm3zTvilORiF0alKM/fCL414i6poyWqD1SNGKfAB5UVUJnxk1Gj7sU
+RT0KlhaOEKGXmdXTMIXM3rRyt7yKPBgpaP3ccQfuJDlq+u2lrDgv+R4QDgZxGhBM/nV+/x5XOULK
+1+EVoVZVWRvRo68R2E7DpSvvkL/A7IITW43WciyTTo9qKd+FPNMN4KIYEsxVL0e3p5sC/kH2iExt
+2qkBR4NkJ2IQgtYSe14DHzSpyZH+r11thie3I6p1GMog57AP14kOpmciY/SDQSsGS7tY1dHXt7kQ
+Y9iJSrSq3RZj9W6+YKH47ejWkE8axsWgKdOnIaj1Wjz3x0miIZpKlVIglnKaZsv30oZDfCK+lvm9
+AahH3eU7QPl1K5srRmSGjR70j/sHd9DqSaIcjVIUpgqT
+-----END CERTIFICATE-----
+
+certSIGN ROOT CA
+================
+-----BEGIN CERTIFICATE-----
+MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYTAlJPMREwDwYD
+VQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTAeFw0wNjA3MDQxNzIwMDRa
+Fw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UE
+CxMQY2VydFNJR04gUk9PVCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7I
+JUqOtdu0KBuqV5Do0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHH
+rfAQUySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5dRdY4zTW2
+ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQOA7+j0xbm0bqQfWwCHTD
+0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwvJoIQ4uNllAoEwF73XVv4EOLQunpL+943
+AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B
+Af8EBAMCAcYwHQYDVR0OBBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IB
+AQA+0hyJLjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecYMnQ8
+SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ44gx+FkagQnIl6Z0
+x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6IJd1hJyMctTEHBDa0GpC9oHRxUIlt
+vBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNwi/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7Nz
+TogVZ96edhBiIL5VaZVDADlN9u6wWk5JRFRYX0KD
+-----END CERTIFICATE-----
+
+CNNIC ROOT
+==========
+-----BEGIN CERTIFICATE-----
+MIIDVTCCAj2gAwIBAgIESTMAATANBgkqhkiG9w0BAQUFADAyMQswCQYDVQQGEwJDTjEOMAwGA1UE
+ChMFQ05OSUMxEzARBgNVBAMTCkNOTklDIFJPT1QwHhcNMDcwNDE2MDcwOTE0WhcNMjcwNDE2MDcw
+OTE0WjAyMQswCQYDVQQGEwJDTjEOMAwGA1UEChMFQ05OSUMxEzARBgNVBAMTCkNOTklDIFJPT1Qw
+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDTNfc/c3et6FtzF8LRb+1VvG7q6KR5smzD
+o+/hn7E7SIX1mlwhIhAsxYLO2uOabjfhhyzcuQxauohV3/2q2x8x6gHx3zkBwRP9SFIhxFXf2tiz
+VHa6dLG3fdfA6PZZxU3Iva0fFNrfWEQlMhkqx35+jq44sDB7R3IJMfAw28Mbdim7aXZOV/kbZKKT
+VrdvmW7bCgScEeOAH8tjlBAKqeFkgjH5jCftppkA9nCTGPihNIaj3XrCGHn2emU1z5DrvTOTn1Or
+czvmmzQgLx3vqR1jGqCA2wMv+SYahtKNu6m+UjqHZ0gNv7Sg2Ca+I19zN38m5pIEo3/PIKe38zrK
+y5nLAgMBAAGjczBxMBEGCWCGSAGG+EIBAQQEAwIABzAfBgNVHSMEGDAWgBRl8jGtKvf33VKWCscC
+wQ7vptU7ETAPBgNVHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIB/jAdBgNVHQ4EFgQUZfIxrSr3991S
+lgrHAsEO76bVOxEwDQYJKoZIhvcNAQEFBQADggEBAEs17szkrr/Dbq2flTtLP1se31cpolnKOOK5
+Gv+e5m4y3R6u6jW39ZORTtpC4cMXYFDy0VwmuYK36m3knITnA3kXr5g9lNvHugDnuL8BV8F3RTIM
+O/G0HAiw/VGgod2aHRM2mm23xzy54cXZF/qD1T0VoDy7HgviyJA/qIYM/PmLXoXLT1tLYhFHxUV8
+BS9BsZ4QaRuZluBVeftOhpm4lNqGOGqTo+fLbuXf6iFViZx9fX+Y9QCJ7uOEwFyWtcVG6kbghVW2
+G8kS1sHNzYDzAgE8yGnLRUhj2JTQ7IUOO04RZfSCjKY9ri4ilAnIXOo8gV0WKgOXFlUJ24pBgp5m
+mxE=
+-----END CERTIFICATE-----
+
+ApplicationCA - Japanese Government
+===================================
+-----BEGIN CERTIFICATE-----
+MIIDoDCCAoigAwIBAgIBMTANBgkqhkiG9w0BAQUFADBDMQswCQYDVQQGEwJKUDEcMBoGA1UEChMT
+SmFwYW5lc2UgR292ZXJubWVudDEWMBQGA1UECxMNQXBwbGljYXRpb25DQTAeFw0wNzEyMTIxNTAw
+MDBaFw0xNzEyMTIxNTAwMDBaMEMxCzAJBgNVBAYTAkpQMRwwGgYDVQQKExNKYXBhbmVzZSBHb3Zl
+cm5tZW50MRYwFAYDVQQLEw1BcHBsaWNhdGlvbkNBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEAp23gdE6Hj6UG3mii24aZS2QNcfAKBZuOquHMLtJqO8F6tJdhjYq+xpqcBrSGUeQ3DnR4
+fl+Kf5Sk10cI/VBaVuRorChzoHvpfxiSQE8tnfWuREhzNgaeZCw7NCPbXCbkcXmP1G55IrmTwcrN
+wVbtiGrXoDkhBFcsovW8R0FPXjQilbUfKW1eSvNNcr5BViCH/OlQR9cwFO5cjFW6WY2H/CPek9AE
+jP3vbb3QesmlOmpyM8ZKDQUXKi17safY1vC+9D/qDihtQWEjdnjDuGWk81quzMKq2edY3rZ+nYVu
+nyoKb58DKTCXKB28t89UKU5RMfkntigm/qJj5kEW8DOYRwIDAQABo4GeMIGbMB0GA1UdDgQWBBRU
+WssmP3HMlEYNllPqa0jQk/5CdTAOBgNVHQ8BAf8EBAMCAQYwWQYDVR0RBFIwUKROMEwxCzAJBgNV
+BAYTAkpQMRgwFgYDVQQKDA/ml6XmnKzlm73mlL/lupwxIzAhBgNVBAsMGuOCouODl+ODquOCseOD
+vOOCt+ODp+ODs0NBMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBADlqRHZ3ODrs
+o2dGD/mLBqj7apAxzn7s2tGJfHrrLgy9mTLnsCTWw//1sogJhyzjVOGjprIIC8CFqMjSnHH2HZ9g
+/DgzE+Ge3Atf2hZQKXsvcJEPmbo0NI2VdMV+eKlmXb3KIXdCEKxmJj3ekav9FfBv7WxfEPjzFvYD
+io+nEhEMy/0/ecGc/WLuo89UDNErXxc+4z6/wCs+CZv+iKZ+tJIX/COUgb1up8WMwusRRdv4QcmW
+dupwX3kSa+SjB1oF7ydJzyGfikwJcGapJsErEU4z0g781mzSDjJkaP+tBXhfAx2o45CsJOAPQKdL
+rosot4LKGAfmt1t06SAZf7IbiVQ=
+-----END CERTIFICATE-----
+
+GeoTrust Primary Certification Authority - G3
+=============================================
+-----BEGIN CERTIFICATE-----
+MIID/jCCAuagAwIBAgIQFaxulBmyeUtB9iepwxgPHzANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UE
+BhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChjKSAyMDA4IEdlb1RydXN0
+IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFy
+eSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEczMB4XDTA4MDQwMjAwMDAwMFoXDTM3MTIwMTIz
+NTk1OVowgZgxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAo
+YykgMjAwOCBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNVBAMT
+LUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMzCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBANziXmJYHTNXOTIz+uvLh4yn1ErdBojqZI4xmKU4kB6Yzy5j
+K/BGvESyiaHAKAxJcCGVn2TAppMSAmUmhsalifD614SgcK9PGpc/BkTVyetyEH3kMSj7HGHmKAdE
+c5IiaacDiGydY8hS2pgn5whMcD60yRLBxWeDXTPzAxHsatBT4tG6NmCUgLthY2xbF37fQJQeqw3C
+IShwiP/WJmxsYAQlTlV+fe+/lEjetx3dcI0FX4ilm/LC7urRQEFtYjgdVgbFA0dRIBn8exALDmKu
+dlW/X3e+PkkBUz2YJQN2JFodtNuJ6nnltrM7P7pMKEF/BqxqjsHQ9gUdfeZChuOl1UcCAwEAAaNC
+MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMR5yo6hTgMdHNxr
+2zFblD4/MH8tMA0GCSqGSIb3DQEBCwUAA4IBAQAtxRPPVoB7eni9n64smefv2t+UXglpp+duaIy9
+cr5HqQ6XErhK8WTTOd8lNNTBzU6B8A8ExCSzNJbGpqow32hhc9f5joWJ7w5elShKKiePEI4ufIbE
+Ap7aDHdlDkQNkv39sxY2+hENHYwOB4lqKVb3cvTdFZx3NWZXqxNT2I7BQMXXExZacse3aQHEerGD
+AWh9jUGhlBjBJVz88P6DAod8DQ3PLghcSkANPuyBYeYk28rgDi0Hsj5W3I31QYUHSJsMC8tJP33s
+t/3LjWeJGqvtux6jAAgIFyqCXDFdRootD4abdNlF+9RAsXqqaC2Gspki4cErx5z481+oghLrGREt
+-----END CERTIFICATE-----
+
+thawte Primary Root CA - G2
+===========================
+-----BEGIN CERTIFICATE-----
+MIICiDCCAg2gAwIBAgIQNfwmXNmET8k9Jj1Xm67XVjAKBggqhkjOPQQDAzCBhDELMAkGA1UEBhMC
+VVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjE4MDYGA1UECxMvKGMpIDIwMDcgdGhhd3RlLCBJbmMu
+IC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAiBgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3Qg
+Q0EgLSBHMjAeFw0wNzExMDUwMDAwMDBaFw0zODAxMTgyMzU5NTlaMIGEMQswCQYDVQQGEwJVUzEV
+MBMGA1UEChMMdGhhd3RlLCBJbmMuMTgwNgYDVQQLEy8oYykgMjAwNyB0aGF3dGUsIEluYy4gLSBG
+b3IgYXV0aG9yaXplZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAt
+IEcyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEotWcgnuVnfFSeIf+iha/BebfowJPDQfGAFG6DAJS
+LSKkQjnE/o/qycG+1E3/n3qe4rF8mq2nhglzh9HnmuN6papu+7qzcMBniKI11KOasf2twu8x+qi5
+8/sIxpHR+ymVo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU
+mtgAMADna3+FGO6Lts6KDPgR4bswCgYIKoZIzj0EAwMDaQAwZgIxAN344FdHW6fmCsO99YCKlzUN
+G4k8VIZ3KMqh9HneteY4sPBlcIx/AlTCv//YoT7ZzwIxAMSNlPzcU9LcnXgWHxUzI1NS41oxXZ3K
+rr0TKUQNJ1uo52icEvdYPy5yAlejj6EULg==
+-----END CERTIFICATE-----
+
+thawte Primary Root CA - G3
+===========================
+-----BEGIN CERTIFICATE-----
+MIIEKjCCAxKgAwIBAgIQYAGXt0an6rS0mtZLL/eQ+zANBgkqhkiG9w0BAQsFADCBrjELMAkGA1UE
+BhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2
+aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIwMDggdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhv
+cml6ZWQgdXNlIG9ubHkxJDAiBgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMzAeFw0w
+ODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIGuMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhh
+d3RlLCBJbmMuMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9uIFNlcnZpY2VzIERpdmlzaW9uMTgwNgYD
+VQQLEy8oYykgMjAwOCB0aGF3dGUsIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTEkMCIG
+A1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAtIEczMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEAsr8nLPvb2FvdeHsbnndmgcs+vHyu86YnmjSjaDFxODNi5PNxZnmxqWWjpYvVj2At
+P0LMqmsywCPLLEHd5N/8YZzic7IilRFDGF/Eth9XbAoFWCLINkw6fKXRz4aviKdEAhN0cXMKQlkC
++BsUa0Lfb1+6a4KinVvnSr0eAXLbS3ToO39/fR8EtCab4LRarEc9VbjXsCZSKAExQGbY2SS99irY
+7CFJXJv2eul/VTV+lmuNk5Mny5K76qxAwJ/C+IDPXfRa3M50hqY+bAtTyr2SzhkGcuYMXDhpxwTW
+vGzOW/b3aJzcJRVIiKHpqfiYnODz1TEoYRFsZ5aNOZnLwkUkOQIDAQABo0IwQDAPBgNVHRMBAf8E
+BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUrWyqlGCc7eT/+j4KdCtjA/e2Wb8wDQYJ
+KoZIhvcNAQELBQADggEBABpA2JVlrAmSicY59BDlqQ5mU1143vokkbvnRFHfxhY0Cu9qRFHqKweK
+A3rD6z8KLFIWoCtDuSWQP3CpMyVtRRooOyfPqsMpQhvfO0zAMzRbQYi/aytlryjvsvXDqmbOe1bu
+t8jLZ8HJnBoYuMTDSQPxYA5QzUbF83d597YV4Djbxy8ooAw/dyZ02SUS2jHaGh7cKUGRIjxpp7sC
+8rZcJwOJ9Abqm+RyguOhCcHpABnTPtRwa7pxpqpYrvS76Wy274fMm7v/OeZWYdMKp8RcTGB7BXcm
+er/YB1IsYvdwY9k5vG8cwnncdimvzsUsZAReiDZuMdRAGmI0Nj81Aa6sY6A=
+-----END CERTIFICATE-----
+
+GeoTrust Primary Certification Authority - G2
+=============================================
+-----BEGIN CERTIFICATE-----
+MIICrjCCAjWgAwIBAgIQPLL0SAoA4v7rJDteYD7DazAKBggqhkjOPQQDAzCBmDELMAkGA1UEBhMC
+VVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChjKSAyMDA3IEdlb1RydXN0IElu
+Yy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBD
+ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMB4XDTA3MTEwNTAwMDAwMFoXDTM4MDExODIzNTk1
+OVowgZgxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykg
+MjAwNyBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNVBAMTLUdl
+b1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMjB2MBAGByqGSM49AgEG
+BSuBBAAiA2IABBWx6P0DFUPlrOuHNxFi79KDNlJ9RVcLSo17VDs6bl8VAsBQps8lL33KSLjHUGMc
+KiEIfJo22Av+0SbFWDEwKCXzXV2juLaltJLtbCyf691DiaI8S0iRHVDsJt/WYC69IaNCMEAwDwYD
+VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBVfNVdRVfslsq0DafwBo/q+
+EVXVMAoGCCqGSM49BAMDA2cAMGQCMGSWWaboCd6LuvpaiIjwH5HTRqjySkwCY/tsXzjbLkGTqQ7m
+ndwxHLKgpxgceeHHNgIwOlavmnRs9vuD4DPTCF+hnMJbn0bWtsuRBmOiBuczrD6ogRLQy7rQkgu2
+npaqBA+K
+-----END CERTIFICATE-----
+
+VeriSign Universal Root Certification Authority
+===============================================
+-----BEGIN CERTIFICATE-----
+MIIEuTCCA6GgAwIBAgIQQBrEZCGzEyEDDrvkEhrFHTANBgkqhkiG9w0BAQsFADCBvTELMAkGA1UE
+BhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBO
+ZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwOCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVk
+IHVzZSBvbmx5MTgwNgYDVQQDEy9WZXJpU2lnbiBVbml2ZXJzYWwgUm9vdCBDZXJ0aWZpY2F0aW9u
+IEF1dGhvcml0eTAeFw0wODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIG9MQswCQYDVQQGEwJV
+UzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0IE5ldHdv
+cmsxOjA4BgNVBAsTMShjKSAyMDA4IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl
+IG9ubHkxODA2BgNVBAMTL1ZlcmlTaWduIFVuaXZlcnNhbCBSb290IENlcnRpZmljYXRpb24gQXV0
+aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx2E3XrEBNNti1xWb/1hajCMj
+1mCOkdeQmIN65lgZOIzF9uVkhbSicfvtvbnazU0AtMgtc6XHaXGVHzk8skQHnOgO+k1KxCHfKWGP
+MiJhgsWHH26MfF8WIFFE0XBPV+rjHOPMee5Y2A7Cs0WTwCznmhcrewA3ekEzeOEz4vMQGn+HLL72
+9fdC4uW/h2KJXwBL38Xd5HVEMkE6HnFuacsLdUYI0crSK5XQz/u5QGtkjFdN/BMReYTtXlT2NJ8I
+AfMQJQYXStrxHXpma5hgZqTZ79IugvHw7wnqRMkVauIDbjPTrJ9VAMf2CGqUuV/c4DPxhGD5WycR
+tPwW8rtWaoAljQIDAQABo4GyMIGvMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMG0G
+CCsGAQUFBwEMBGEwX6FdoFswWTBXMFUWCWltYWdlL2dpZjAhMB8wBwYFKw4DAhoEFI/l0xqGrI2O
+a8PPgGrUSBgsexkuMCUWI2h0dHA6Ly9sb2dvLnZlcmlzaWduLmNvbS92c2xvZ28uZ2lmMB0GA1Ud
+DgQWBBS2d/ppSEefUxLVwuoHMnYH0ZcHGTANBgkqhkiG9w0BAQsFAAOCAQEASvj4sAPmLGd75JR3
+Y8xuTPl9Dg3cyLk1uXBPY/ok+myDjEedO2Pzmvl2MpWRsXe8rJq+seQxIcaBlVZaDrHC1LGmWazx
+Y8u4TB1ZkErvkBYoH1quEPuBUDgMbMzxPcP1Y+Oz4yHJJDnp/RVmRvQbEdBNc6N9Rvk97ahfYtTx
+P/jgdFcrGJ2BtMQo2pSXpXDrrB2+BxHw1dvd5Yzw1TKwg+ZX4o+/vqGqvz0dtdQ46tewXDpPaj+P
+wGZsY6rp2aQW9IHRlRQOfc2VNNnSj3BzgXucfr2YYdhFh5iQxeuGMMY1v/D/w1WIg0vvBZIGcfK4
+mJO37M2CYfE45k+XmCpajQ==
+-----END CERTIFICATE-----
+
+VeriSign Class 3 Public Primary Certification Authority - G4
+============================================================
+-----BEGIN CERTIFICATE-----
+MIIDhDCCAwqgAwIBAgIQL4D+I4wOIg9IZxIokYesszAKBggqhkjOPQQDAzCByjELMAkGA1UEBhMC
+VVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3
+b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVz
+ZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmlj
+YXRpb24gQXV0aG9yaXR5IC0gRzQwHhcNMDcxMTA1MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCByjEL
+MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBU
+cnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRo
+b3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5
+IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASnVnp8
+Utpkmw4tXNherJI9/gHmGUo9FANL+mAnINmDiWn6VMaaGF5VKmTeBvaNSjutEDxlPZCIBIngMGGz
+rl0Bp3vefLK+ymVhAIau2o970ImtTR1ZmkGxvEeA3J5iw/mjgbIwga8wDwYDVR0TAQH/BAUwAwEB
+/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEw
+HzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVyaXNpZ24u
+Y29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFLMWkf3upm7ktS5Jj4d4gYDs5bG1MAoGCCqGSM49BAMD
+A2gAMGUCMGYhDBgmYFo4e1ZC4Kf8NoRRkSAsdk1DPcQdhCPQrNZ8NQbOzWm9kA3bbEhCHQ6qQgIx
+AJw9SDkjOVgaFRJZap7v1VmyHVIsmXHNxynfGyphe3HR3vPA5Q06Sqotp9iGKt0uEA==
+-----END CERTIFICATE-----
+
+NetLock Arany (Class Gold) Főtanúsítvány
+============================================
+-----BEGIN CERTIFICATE-----
+MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQGEwJIVTERMA8G
+A1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3MDUGA1UECwwuVGFuw7pzw610
+dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBB
+cmFueSAoQ2xhc3MgR29sZCkgRsWRdGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgx
+MjA2MTUwODIxWjCBpzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxO
+ZXRMb2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlmaWNhdGlv
+biBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNzIEdvbGQpIEbFkXRhbsO6
+c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxCRec75LbRTDofTjl5Bu
+0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrTlF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw
+/HpYzY6b7cNGbIRwXdrzAZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAk
+H3B5r9s5VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRGILdw
+fzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2BJtr+UBdADTHLpl1
+neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIB
+BjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2MU9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwW
+qZw8UQCgwBEIBaeZ5m8BiFRhbvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTta
+YtOUZcTh5m2C+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC
+bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2FuLjbvrW5Kfna
+NwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2XjG4Kvte9nHfRCaexOYNkbQu
+dZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E=
+-----END CERTIFICATE-----
+
+Staat der Nederlanden Root CA - G2
+==================================
+-----BEGIN CERTIFICATE-----
+MIIFyjCCA7KgAwIBAgIEAJiWjDANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJOTDEeMBwGA1UE
+CgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFhdCBkZXIgTmVkZXJsYW5kZW4g
+Um9vdCBDQSAtIEcyMB4XDTA4MDMyNjExMTgxN1oXDTIwMDMyNTExMDMxMFowWjELMAkGA1UEBhMC
+TkwxHjAcBgNVBAoMFVN0YWF0IGRlciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5l
+ZGVybGFuZGVuIFJvb3QgQ0EgLSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMVZ
+5291qj5LnLW4rJ4L5PnZyqtdj7U5EILXr1HgO+EASGrP2uEGQxGZqhQlEq0i6ABtQ8SpuOUfiUtn
+vWFI7/3S4GCI5bkYYCjDdyutsDeqN95kWSpGV+RLufg3fNU254DBtvPUZ5uW6M7XxgpT0GtJlvOj
+CwV3SPcl5XCsMBQgJeN/dVrlSPhOewMHBPqCYYdu8DvEpMfQ9XQ+pV0aCPKbJdL2rAQmPlU6Yiil
+e7Iwr/g3wtG61jj99O9JMDeZJiFIhQGp5Rbn3JBV3w/oOM2ZNyFPXfUib2rFEhZgF1XyZWampzCR
+OME4HYYEhLoaJXhena/MUGDWE4dS7WMfbWV9whUYdMrhfmQpjHLYFhN9C0lK8SgbIHRrxT3dsKpI
+CT0ugpTNGmXZK4iambwYfp/ufWZ8Pr2UuIHOzZgweMFvZ9C+X+Bo7d7iscksWXiSqt8rYGPy5V65
+48r6f1CGPqI0GAwJaCgRHOThuVw+R7oyPxjMW4T182t0xHJ04eOLoEq9jWYv6q012iDTiIJh8BIi
+trzQ1aTsr1SIJSQ8p22xcik/Plemf1WvbibG/ufMQFxRRIEKeN5KzlW/HdXZt1bv8Hb/C3m1r737
+qWmRRpdogBQ2HbN/uymYNqUg+oJgYjOk7Na6B6duxc8UpufWkjTYgfX8HV2qXB72o007uPc5AgMB
+AAGjgZcwgZQwDwYDVR0TAQH/BAUwAwEB/zBSBgNVHSAESzBJMEcGBFUdIAAwPzA9BggrBgEFBQcC
+ARYxaHR0cDovL3d3dy5wa2lvdmVyaGVpZC5ubC9wb2xpY2llcy9yb290LXBvbGljeS1HMjAOBgNV
+HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJFoMocVHYnitfGsNig0jQt8YojrMA0GCSqGSIb3DQEBCwUA
+A4ICAQCoQUpnKpKBglBu4dfYszk78wIVCVBR7y29JHuIhjv5tLySCZa59sCrI2AGeYwRTlHSeYAz
++51IvuxBQ4EffkdAHOV6CMqqi3WtFMTC6GY8ggen5ieCWxjmD27ZUD6KQhgpxrRW/FYQoAUXvQwj
+f/ST7ZwaUb7dRUG/kSS0H4zpX897IZmflZ85OkYcbPnNe5yQzSipx6lVu6xiNGI1E0sUOlWDuYaN
+kqbG9AclVMwWVxJKgnjIFNkXgiYtXSAfea7+1HAWFpWD2DU5/1JddRwWxRNVz0fMdWVSSt7wsKfk
+CpYL+63C4iWEst3kvX5ZbJvw8NjnyvLplzh+ib7M+zkXYT9y2zqR2GUBGR2tUKRXCnxLvJxxcypF
+URmFzI79R6d0lR2o0a9OF7FpJsKqeFdbxU2n5Z4FF5TKsl+gSRiNNOkmbEgeqmiSBeGCc1qb3Adb
+CG19ndeNIdn8FCCqwkXfP+cAslHkwvgFuXkajDTznlvkN1trSt8sV4pAWja63XVECDdCcAz+3F4h
+oKOKwJCcaNpQ5kUQR3i2TtJlycM33+FCY7BXN0Ute4qcvwXqZVUz9zkQxSgqIXobisQk+T8VyJoV
+IPVVYpbtbZNQvOSqeK3Zywplh6ZmwcSBo3c6WB4L7oOLnR7SUqTMHW+wmG2UMbX4cQrcufx9MmDm
+66+KAQ==
+-----END CERTIFICATE-----
+
+CA Disig
+========
+-----BEGIN CERTIFICATE-----
+MIIEDzCCAvegAwIBAgIBATANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQGEwJTSzETMBEGA1UEBxMK
+QnJhdGlzbGF2YTETMBEGA1UEChMKRGlzaWcgYS5zLjERMA8GA1UEAxMIQ0EgRGlzaWcwHhcNMDYw
+MzIyMDEzOTM0WhcNMTYwMzIyMDEzOTM0WjBKMQswCQYDVQQGEwJTSzETMBEGA1UEBxMKQnJhdGlz
+bGF2YTETMBEGA1UEChMKRGlzaWcgYS5zLjERMA8GA1UEAxMIQ0EgRGlzaWcwggEiMA0GCSqGSIb3
+DQEBAQUAA4IBDwAwggEKAoIBAQCS9jHBfYj9mQGp2HvycXXxMcbzdWb6UShGhJd4NLxs/LxFWYgm
+GErENx+hSkS943EE9UQX4j/8SFhvXJ56CbpRNyIjZkMhsDxkovhqFQ4/61HhVKndBpnXmjxUizkD
+Pw/Fzsbrg3ICqB9x8y34dQjbYkzo+s7552oftms1grrijxaSfQUMbEYDXcDtab86wYqg6I7ZuUUo
+hwjstMoVvoLdtUSLLa2GDGhibYVW8qwUYzrG0ZmsNHhWS8+2rT+MitcE5eN4TPWGqvWP+j1scaMt
+ymfraHtuM6kMgiioTGohQBUgDCZbg8KpFhXAJIJdKxatymP2dACw30PEEGBWZ2NFAgMBAAGjgf8w
+gfwwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUjbJJaJ1yCCW5wCf1UJNWSEZx+Y8wDgYDVR0P
+AQH/BAQDAgEGMDYGA1UdEQQvMC2BE2Nhb3BlcmF0b3JAZGlzaWcuc2uGFmh0dHA6Ly93d3cuZGlz
+aWcuc2svY2EwZgYDVR0fBF8wXTAtoCugKYYnaHR0cDovL3d3dy5kaXNpZy5zay9jYS9jcmwvY2Ff
+ZGlzaWcuY3JsMCygKqAohiZodHRwOi8vY2EuZGlzaWcuc2svY2EvY3JsL2NhX2Rpc2lnLmNybDAa
+BgNVHSAEEzARMA8GDSuBHpGT5goAAAABAQEwDQYJKoZIhvcNAQEFBQADggEBAF00dGFMrzvY/59t
+WDYcPQuBDRIrRhCA/ec8J9B6yKm2fnQwM6M6int0wHl5QpNt/7EpFIKrIYwvF/k/Ji/1WcbvgAa3
+mkkp7M5+cTxqEEHA9tOasnxakZzArFvITV734VP/Q3f8nktnbNfzg9Gg4H8l37iYC5oyOGwwoPP/
+CBUz91BKez6jPiCp3C9WgArtQVCwyfTssuMmRAAOb54GvCKWU3BlxFAKRmukLyeBEicTXxChds6K
+ezfqwzlhA5WYOudsiCUI/HloDYd9Yvi0X/vF2Ey9WLw/Q1vUHgFNPGO+I++MzVpQuGhU+QqZMxEA
+4Z7CRneC9VkGjCFMhwnN5ag=
+-----END CERTIFICATE-----
+
+Juur-SK
+=======
+-----BEGIN CERTIFICATE-----
+MIIE5jCCA86gAwIBAgIEO45L/DANBgkqhkiG9w0BAQUFADBdMRgwFgYJKoZIhvcNAQkBFglwa2lA
+c2suZWUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKExlBUyBTZXJ0aWZpdHNlZXJpbWlza2Vza3VzMRAw
+DgYDVQQDEwdKdXVyLVNLMB4XDTAxMDgzMDE0MjMwMVoXDTE2MDgyNjE0MjMwMVowXTEYMBYGCSqG
+SIb3DQEJARYJcGtpQHNrLmVlMQswCQYDVQQGEwJFRTEiMCAGA1UEChMZQVMgU2VydGlmaXRzZWVy
+aW1pc2tlc2t1czEQMA4GA1UEAxMHSnV1ci1TSzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
+ggEBAIFxNj4zB9bjMI0TfncyRsvPGbJgMUaXhvSYRqTCZUXP00B841oiqBB4M8yIsdOBSvZiF3tf
+TQou0M+LI+5PAk676w7KvRhj6IAcjeEcjT3g/1tf6mTll+g/mX8MCgkzABpTpyHhOEvWgxutr2TC
++Rx6jGZITWYfGAriPrsfB2WThbkasLnE+w0R9vXW+RvHLCu3GFH+4Hv2qEivbDtPL+/40UceJlfw
+UR0zlv/vWT3aTdEVNMfqPxZIe5EcgEMPPbgFPtGzlc3Yyg/CQ2fbt5PgIoIuvvVoKIO5wTtpeyDa
+Tpxt4brNj3pssAki14sL2xzVWiZbDcDq5WDQn/413z8CAwEAAaOCAawwggGoMA8GA1UdEwEB/wQF
+MAMBAf8wggEWBgNVHSAEggENMIIBCTCCAQUGCisGAQQBzh8BAQEwgfYwgdAGCCsGAQUFBwICMIHD
+HoHAAFMAZQBlACAAcwBlAHIAdABpAGYAaQBrAGEAYQB0ACAAbwBuACAAdgDkAGwAagBhAHMAdABh
+AHQAdQBkACAAQQBTAC0AaQBzACAAUwBlAHIAdABpAGYAaQB0AHMAZQBlAHIAaQBtAGkAcwBrAGUA
+cwBrAHUAcwAgAGEAbABhAG0ALQBTAEsAIABzAGUAcgB0AGkAZgBpAGsAYQBhAHQAaQBkAGUAIABr
+AGkAbgBuAGkAdABhAG0AaQBzAGUAawBzMCEGCCsGAQUFBwIBFhVodHRwOi8vd3d3LnNrLmVlL2Nw
+cy8wKwYDVR0fBCQwIjAgoB6gHIYaaHR0cDovL3d3dy5zay5lZS9qdXVyL2NybC8wHQYDVR0OBBYE
+FASqekej5ImvGs8KQKcYP2/v6X2+MB8GA1UdIwQYMBaAFASqekej5ImvGs8KQKcYP2/v6X2+MA4G
+A1UdDwEB/wQEAwIB5jANBgkqhkiG9w0BAQUFAAOCAQEAe8EYlFOiCfP+JmeaUOTDBS8rNXiRTHyo
+ERF5TElZrMj3hWVcRrs7EKACr81Ptcw2Kuxd/u+gkcm2k298gFTsxwhwDY77guwqYHhpNjbRxZyL
+abVAyJRld/JXIWY7zoVAtjNjGr95HvxcHdMdkxuLDF2FvZkwMhgJkVLpfKG6/2SSmuz+Ne6ML678
+IIbsSt4beDI3poHSna9aEhbKmVv8b20OxaAehsmR0FyYgl9jDIpaq9iVpszLita/ZEuOyoqysOkh
+Mp6qqIWYNIE5ITuoOlIyPfZrN4YGWhWY3PARZv40ILcD9EEQfTmEeZZyY7aWAuVrua0ZTbvGRNs2
+yyqcjg==
+-----END CERTIFICATE-----
+
+Hongkong Post Root CA 1
+=======================
+-----BEGIN CERTIFICATE-----
+MIIDMDCCAhigAwIBAgICA+gwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCSEsxFjAUBgNVBAoT
+DUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3QgUm9vdCBDQSAxMB4XDTAzMDUx
+NTA1MTMxNFoXDTIzMDUxNTA0NTIyOVowRzELMAkGA1UEBhMCSEsxFjAUBgNVBAoTDUhvbmdrb25n
+IFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3QgUm9vdCBDQSAxMIIBIjANBgkqhkiG9w0BAQEF
+AAOCAQ8AMIIBCgKCAQEArP84tulmAknjorThkPlAj3n54r15/gK97iSSHSL22oVyaf7XPwnU3ZG1
+ApzQjVrhVcNQhrkpJsLj2aDxaQMoIIBFIi1WpztUlVYiWR8o3x8gPW2iNr4joLFutbEnPzlTCeqr
+auh0ssJlXI6/fMN4hM2eFvz1Lk8gKgifd/PFHsSaUmYeSF7jEAaPIpjhZY4bXSNmO7ilMlHIhqqh
+qZ5/dpTCpmy3QfDVyAY45tQM4vM7TG1QjMSDJ8EThFk9nnV0ttgCXjqQesBCNnLsak3c78QA3xMY
+V18meMjWCnl3v/evt3a5pQuEF10Q6m/hq5URX208o1xNg1vysxmKgIsLhwIDAQABoyYwJDASBgNV
+HRMBAf8ECDAGAQH/AgEDMA4GA1UdDwEB/wQEAwIBxjANBgkqhkiG9w0BAQUFAAOCAQEADkbVPK7i
+h9legYsCmEEIjEy82tvuJxuC52pF7BaLT4Wg87JwvVqWuspube5Gi27nKi6Wsxkz67SfqLI37pio
+l7Yutmcn1KZJ/RyTZXaeQi/cImyaT/JaFTmxcdcrUehtHJjA2Sr0oYJ71clBoiMBdDhViw+5Lmei
+IAQ32pwL0xch4I+XeTRvhEgCIDMb5jREn5Fw9IBehEPCKdJsEhTkYY2sEJCehFC78JZvRZ+K88ps
+T/oROhUVRsPNH4NbLUES7VBnQRM9IauUiqpOfMGx+6fWtScvl6tu4B3i0RwsH0Ti/L6RoZz71ilT
+c4afU9hDDl3WY4JxHYB0yvbiAmvZWg==
+-----END CERTIFICATE-----
+
+SecureSign RootCA11
+===================
+-----BEGIN CERTIFICATE-----
+MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDErMCkGA1UEChMi
+SmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoGA1UEAxMTU2VjdXJlU2lnbiBS
+b290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0MDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSsw
+KQYDVQQKEyJKYXBhbiBDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1
+cmVTaWduIFJvb3RDQTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvL
+TJszi1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8h9uuywGO
+wvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOVMdrAG/LuYpmGYz+/3ZMq
+g6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9UK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rP
+O7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitA
+bpSACW22s293bzUIUPsCh8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZX
+t94wDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAKCh
+OBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xmKbabfSVSSUOrTC4r
+bnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQX5Ucv+2rIrVls4W6ng+4reV6G4pQ
+Oh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWrQbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01
+y8hSyn+B/tlr0/cR7SXf+Of5pPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061
+lgeLKBObjBmNQSdJQO7e5iNEOdyhIta6A/I=
+-----END CERTIFICATE-----
+
+ACEDICOM Root
+=============
+-----BEGIN CERTIFICATE-----
+MIIFtTCCA52gAwIBAgIIYY3HhjsBggUwDQYJKoZIhvcNAQEFBQAwRDEWMBQGA1UEAwwNQUNFRElD
+T00gUm9vdDEMMAoGA1UECwwDUEtJMQ8wDQYDVQQKDAZFRElDT00xCzAJBgNVBAYTAkVTMB4XDTA4
+MDQxODE2MjQyMloXDTI4MDQxMzE2MjQyMlowRDEWMBQGA1UEAwwNQUNFRElDT00gUm9vdDEMMAoG
+A1UECwwDUEtJMQ8wDQYDVQQKDAZFRElDT00xCzAJBgNVBAYTAkVTMIICIjANBgkqhkiG9w0BAQEF
+AAOCAg8AMIICCgKCAgEA/5KV4WgGdrQsyFhIyv2AVClVYyT/kGWbEHV7w2rbYgIB8hiGtXxaOLHk
+WLn709gtn70yN78sFW2+tfQh0hOR2QetAQXW8713zl9CgQr5auODAKgrLlUTY4HKRxx7XBZXehuD
+YAQ6PmXDzQHe3qTWDLqO3tkE7hdWIpuPY/1NFgu3e3eM+SW10W2ZEi5PGrjm6gSSrj0RuVFCPYew
+MYWveVqc/udOXpJPQ/yrOq2lEiZmueIM15jO1FillUAKt0SdE3QrwqXrIhWYENiLxQSfHY9g5QYb
+m8+5eaA9oiM/Qj9r+hwDezCNzmzAv+YbX79nuIQZ1RXve8uQNjFiybwCq0Zfm/4aaJQ0PZCOrfbk
+HQl/Sog4P75n/TSW9R28MHTLOO7VbKvU/PQAtwBbhTIWdjPp2KOZnQUAqhbm84F9b32qhm2tFXTT
+xKJxqvQUfecyuB+81fFOvW8XAjnXDpVCOscAPukmYxHqC9FK/xidstd7LzrZlvvoHpKuE1XI2Sf2
+3EgbsCTBheN3nZqk8wwRHQ3ItBTutYJXCb8gWH8vIiPYcMt5bMlL8qkqyPyHK9caUPgn6C9D4zq9
+2Fdx/c6mUlv53U3t5fZvie27k5x2IXXwkkwp9y+cAS7+UEaeZAwUswdbxcJzbPEHXEUkFDWug/Fq
+TYl6+rPYLWbwNof1K1MCAwEAAaOBqjCBpzAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKaz
+4SsrSbbXc6GqlPUB53NlTKxQMA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUprPhKytJttdzoaqU
+9QHnc2VMrFAwRAYDVR0gBD0wOzA5BgRVHSAAMDEwLwYIKwYBBQUHAgEWI2h0dHA6Ly9hY2VkaWNv
+bS5lZGljb21ncm91cC5jb20vZG9jMA0GCSqGSIb3DQEBBQUAA4ICAQDOLAtSUWImfQwng4/F9tqg
+aHtPkl7qpHMyEVNEskTLnewPeUKzEKbHDZ3Ltvo/Onzqv4hTGzz3gvoFNTPhNahXwOf9jU8/kzJP
+eGYDdwdY6ZXIfj7QeQCM8htRM5u8lOk6e25SLTKeI6RF+7YuE7CLGLHdztUdp0J/Vb77W7tH1Pwk
+zQSulgUV1qzOMPPKC8W64iLgpq0i5ALudBF/TP94HTXa5gI06xgSYXcGCRZj6hitoocf8seACQl1
+ThCojz2GuHURwCRiipZ7SkXp7FnFvmuD5uHorLUwHv4FB4D54SMNUI8FmP8sX+g7tq3PgbUhh8oI
+KiMnMCArz+2UW6yyetLHKKGKC5tNSixthT8Jcjxn4tncB7rrZXtaAWPWkFtPF2Y9fwsZo5NjEFIq
+nxQWWOLcpfShFosOkYuByptZ+thrkQdlVV9SH686+5DdaaVbnG0OLLb6zqylfDJKZ0DcMDQj3dcE
+I2bw/FWAp/tmGYI1Z2JwOV5vx+qQQEQIHriy1tvuWacNGHk0vFQYXlPKNFHtRQrmjseCNj6nOGOp
+MCwXEGCSn1WHElkQwg9naRHMTh5+Spqtr0CodaxWkHS4oJyleW/c6RrIaQXpuvoDs3zk4E7Czp3o
+tkYNbn5XOmeUwssfnHdKZ05phkOTOPu220+DkdRgfks+KzgHVZhepA==
+-----END CERTIFICATE-----
+
+Verisign Class 1 Public Primary Certification Authority
+=======================================================
+-----BEGIN CERTIFICATE-----
+MIICPDCCAaUCED9pHoGc8JpK83P/uUii5N0wDQYJKoZIhvcNAQEFBQAwXzELMAkGA1UEBhMCVVMx
+FzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAxIFB1YmxpYyBQcmltYXJ5
+IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2MDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVow
+XzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAx
+IFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUA
+A4GNADCBiQKBgQDlGb9to1ZhLZlIcfZn3rmN67eehoAKkQ76OCWvRoiC5XOooJskXQ0fzGVuDLDQ
+VoQYh5oGmxChc9+0WDlrbsH2FdWoqD+qEgaNMax/sDTXjzRniAnNFBHiTkVWaR94AoDa3EeRKbs2
+yWNcxeDXLYd7obcysHswuiovMaruo2fa2wIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAFgVKTk8d6Pa
+XCUDfGD67gmZPCcQcMgMCeazh88K4hiWNWLMv5sneYlfycQJ9M61Hd8qveXbhpxoJeUwfLaJFf5n
+0a3hUKw8fGJLj7qE1xIVGx/KXQ/BUpQqEZnae88MNhPVNdwQGVnqlMEAv3WP2fr9dgTbYruQagPZ
+RjXZ+Hxb
+-----END CERTIFICATE-----
+
+Verisign Class 3 Public Primary Certification Authority
+=======================================================
+-----BEGIN CERTIFICATE-----
+MIICPDCCAaUCEDyRMcsf9tAbDpq40ES/Er4wDQYJKoZIhvcNAQEFBQAwXzELMAkGA1UEBhMCVVMx
+FzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmltYXJ5
+IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2MDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVow
+XzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAz
+IFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUA
+A4GNADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhEBarsAx94
+f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/isI19wKTakyYbnsZogy1Ol
+hec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBABByUqkFFBky
+CEHwxWsKzH4PIRnN5GfcX6kb5sroc50i2JhucwNhkcV8sEVAbkSdjbCxlnRhLQ2pRdKkkirWmnWX
+bj9T/UWZYB2oK0z5XqcJ2HUw19JlYD1n1khVdWk/kfVIC0dpImmClr7JyDiGSnoscxlIaU5rfGW/
+D/xwzoiQ
+-----END CERTIFICATE-----
+
+Microsec e-Szigno Root CA 2009
+==============================
+-----BEGIN CERTIFICATE-----
+MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYDVQQGEwJIVTER
+MA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jv
+c2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o
+dTAeFw0wOTA2MTYxMTMwMThaFw0yOTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UE
+BwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUt
+U3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTCCASIw
+DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvPkd6mJviZpWNwrZuuyjNA
+fW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tccbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG
+0IMZfcChEhyVbUr02MelTTMuhTlAdX4UfIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKA
+pxn1ntxVUwOXewdI/5n7N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm
+1HxdrtbCxkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1+rUC
+AwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTLD8bf
+QkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAbBgNVHREE
+FDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqGSIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0o
+lZMEyL/azXm4Q5DwpL7v8u8hmLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfX
+I/OMn74dseGkddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775
+tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c2Pm2G2JwCz02
+yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5tHMN1Rq41Bab2XD0h7lbwyYIi
+LXpUq3DDfSJlgnCW
+-----END CERTIFICATE-----
+
+E-Guven Kok Elektronik Sertifika Hizmet Saglayicisi
+===================================================
+-----BEGIN CERTIFICATE-----
+MIIDtjCCAp6gAwIBAgIQRJmNPMADJ72cdpW56tustTANBgkqhkiG9w0BAQUFADB1MQswCQYDVQQG
+EwJUUjEoMCYGA1UEChMfRWxla3Ryb25payBCaWxnaSBHdXZlbmxpZ2kgQS5TLjE8MDoGA1UEAxMz
+ZS1HdXZlbiBLb2sgRWxla3Ryb25payBTZXJ0aWZpa2EgSGl6bWV0IFNhZ2xheWljaXNpMB4XDTA3
+MDEwNDExMzI0OFoXDTE3MDEwNDExMzI0OFowdTELMAkGA1UEBhMCVFIxKDAmBgNVBAoTH0VsZWt0
+cm9uaWsgQmlsZ2kgR3V2ZW5saWdpIEEuUy4xPDA6BgNVBAMTM2UtR3V2ZW4gS29rIEVsZWt0cm9u
+aWsgU2VydGlmaWthIEhpem1ldCBTYWdsYXlpY2lzaTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
+AQoCggEBAMMSIJ6wXgBljU5Gu4Bc6SwGl9XzcslwuedLZYDBS75+PNdUMZTe1RK6UxYC6lhj71vY
+8+0qGqpxSKPcEC1fX+tcS5yWCEIlKBHMilpiAVDV6wlTL/jDj/6z/P2douNffb7tC+Bg62nsM+3Y
+jfsSSYMAyYuXjDtzKjKzEve5TfL0TW3H5tYmNwjy2f1rXKPlSFxYvEK+A1qBuhw1DADT9SN+cTAI
+JjjcJRFHLfO6IxClv7wC90Nex/6wN1CZew+TzuZDLMN+DfIcQ2Zgy2ExR4ejT669VmxMvLz4Bcpk
+9Ok0oSy1c+HCPujIyTQlCFzz7abHlJ+tiEMl1+E5YP6sOVkCAwEAAaNCMEAwDgYDVR0PAQH/BAQD
+AgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJ/uRLOU1fqRTy7ZVZoEVtstxNulMA0GCSqG
+SIb3DQEBBQUAA4IBAQB/X7lTW2M9dTLn+sR0GstG30ZpHFLPqk/CaOv/gKlR6D1id4k9CnU58W5d
+F4dvaAXBlGzZXd/aslnLpRCKysw5zZ/rTt5S/wzw9JKp8mxTq5vSR6AfdPebmvEvFZ96ZDAYBzwq
+D2fK/A+JYZ1lpTzlvBNbCNvj/+27BrtqBrF6T2XGgv0enIu1De5Iu7i9qgi0+6N8y5/NkHZchpZ4
+Vwpm+Vganf2XKWDeEaaQHBkc7gGWIjQ0LpH5t8Qn0Xvmv/uARFoW5evg1Ao4vOSR49XrXMGs3xtq
+fJ7lddK2l4fbzIcrQzqECK+rPNv3PGYxhrCdU3nt+CPeQuMtgvEP5fqX
+-----END CERTIFICATE-----
+
+GlobalSign Root CA - R3
+=======================
+-----BEGIN CERTIFICATE-----
+MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4GA1UECxMXR2xv
+YmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2Jh
+bFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxT
+aWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2ln
+bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWt
+iHL8RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsTgHeMCOFJ
+0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmmKPZpO/bLyCiR5Z2KYVc3
+rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zdQQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjl
+OCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZXriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2
+xmmFghcCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE
+FI/wS3+oLkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZURUm7
+lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMpjjM5RcOO5LlXbKr8
+EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK6fBdRoyV3XpYKBovHd7NADdBj+1E
+bddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQXmcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18
+YIvDQVETI53O9zJrlAGomecsMx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7r
+kpeDMdmztcpHWD9f
+-----END CERTIFICATE-----
+
+TC TrustCenter Universal CA III
+===============================
+-----BEGIN CERTIFICATE-----
+MIID4TCCAsmgAwIBAgIOYyUAAQACFI0zFQLkbPQwDQYJKoZIhvcNAQEFBQAwezELMAkGA1UEBhMC
+REUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxJDAiBgNVBAsTG1RDIFRydXN0Q2VudGVy
+IFVuaXZlcnNhbCBDQTEoMCYGA1UEAxMfVEMgVHJ1c3RDZW50ZXIgVW5pdmVyc2FsIENBIElJSTAe
+Fw0wOTA5MDkwODE1MjdaFw0yOTEyMzEyMzU5NTlaMHsxCzAJBgNVBAYTAkRFMRwwGgYDVQQKExNU
+QyBUcnVzdENlbnRlciBHbWJIMSQwIgYDVQQLExtUQyBUcnVzdENlbnRlciBVbml2ZXJzYWwgQ0Ex
+KDAmBgNVBAMTH1RDIFRydXN0Q2VudGVyIFVuaXZlcnNhbCBDQSBJSUkwggEiMA0GCSqGSIb3DQEB
+AQUAA4IBDwAwggEKAoIBAQDC2pxisLlxErALyBpXsq6DFJmzNEubkKLF5+cvAqBNLaT6hdqbJYUt
+QCggbergvbFIgyIpRJ9Og+41URNzdNW88jBmlFPAQDYvDIRlzg9uwliT6CwLOunBjvvya8o84pxO
+juT5fdMnnxvVZ3iHLX8LR7PH6MlIfK8vzArZQe+f/prhsq75U7Xl6UafYOPfjdN/+5Z+s7Vy+Eut
+CHnNaYlAJ/Uqwa1D7KRTyGG299J5KmcYdkhtWyUB0SbFt1dpIxVbYYqt8Bst2a9c8SaQaanVDED1
+M4BDj5yjdipFtK+/fz6HP3bFzSreIMUWWMv5G/UPyw0RUmS40nZid4PxWJ//AgMBAAGjYzBhMB8G
+A1UdIwQYMBaAFFbn4VslQ4Dg9ozhcbyO5YAvxEjiMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/
+BAQDAgEGMB0GA1UdDgQWBBRW5+FbJUOA4PaM4XG8juWAL8RI4jANBgkqhkiG9w0BAQUFAAOCAQEA
+g8ev6n9NCjw5sWi+e22JLumzCecYV42FmhfzdkJQEw/HkG8zrcVJYCtsSVgZ1OK+t7+rSbyUyKu+
+KGwWaODIl0YgoGhnYIg5IFHYaAERzqf2EQf27OysGh+yZm5WZ2B6dF7AbZc2rrUNXWZzwCUyRdhK
+BgePxLcHsU0GDeGl6/R1yrqc0L2z0zIkTO5+4nYES0lT2PLpVDP85XEfPRRclkvxOvIAu2y0+pZV
+CIgJwcyRGSmwIC3/yzikQOEXvnlhgP8HA4ZMTnsGnxGGjYnuJ8Tb4rwZjgvDwxPHLQNjO9Po5KIq
+woIIlBZU8O8fJ5AluA0OKBtHd0e9HKgl8ZS0Zg==
+-----END CERTIFICATE-----
+
+Autoridad de Certificacion Firmaprofesional CIF A62634068
+=========================================================
+-----BEGIN CERTIFICATE-----
+MIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UEBhMCRVMxQjBA
+BgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2
+MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEyMzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIw
+QAYDVQQDDDlBdXRvcmlkYWQgZGUgQ2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBB
+NjI2MzQwNjgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDD
+Utd9thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQMcas9UX4P
+B99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefGL9ItWY16Ck6WaVICqjaY
+7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15iNA9wBj4gGFrO93IbJWyTdBSTo3OxDqqH
+ECNZXyAFGUftaI6SEspd/NYrspI8IM/hX68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyI
+plD9amML9ZMWGxmPsu2bm8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctX
+MbScyJCyZ/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirjaEbsX
+LZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/TKI8xWVvTyQKmtFLK
+bpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF6NkBiDkal4ZkQdU7hwxu+g/GvUgU
+vzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVhOSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1Ud
+EwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNH
+DhpkLzCBpgYDVR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp
+cm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBvACAAZABlACAA
+bABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBlAGwAbwBuAGEAIAAwADgAMAAx
+ADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx
+51tkljYyGOylMnfX40S2wBEqgLk9am58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qk
+R71kMrv2JYSiJ0L1ILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaP
+T481PyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS3a/DTg4f
+Jl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5kSeTy36LssUzAKh3ntLFl
+osS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF3dvd6qJ2gHN99ZwExEWN57kci57q13XR
+crHedUTnQn3iV2t93Jm8PYMo6oCTjcVMZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoR
+saS8I8nkvof/uZS2+F0gStRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTD
+KCOM/iczQ0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQBjLMi
+6Et8Vcad+qMUu2WFbm5PEn4KPJ2V
+-----END CERTIFICATE-----
+
+Izenpe.com
+==========
+-----BEGIN CERTIFICATE-----
+MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4MQswCQYDVQQG
+EwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wHhcNMDcxMjEz
+MTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMu
+QS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ
+03rKDx6sp4boFmVqscIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAK
+ClaOxdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6HLmYRY2xU
++zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFXuaOKmMPsOzTFlUFpfnXC
+PCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQDyCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxT
+OTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbK
+F7jJeodWLBoBHmy+E60QrLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK
+0GqfvEyNBjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8Lhij+
+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIBQFqNeb+Lz0vPqhbB
+leStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+HMh3/1uaD7euBUbl8agW7EekFwID
+AQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2luZm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+
+SVpFTlBFIFMuQS4gLSBDSUYgQTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBG
+NjIgUzgxQzBBBgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx
+MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O
+BBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUAA4ICAQB4pgwWSp9MiDrAyw6l
+Fn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWblaQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbga
+kEyrkgPH7UIBzg/YsfqikuFgba56awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8q
+hT/AQKM6WfxZSzwoJNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Cs
+g1lwLDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCTVyvehQP5
+aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGkLhObNA5me0mrZJfQRsN5
+nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJbUjWumDqtujWTI6cfSN01RpiyEGjkpTHC
+ClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZo
+Q0iy2+tzJOeRf1SktoA+naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1Z
+WrOZyGlsQyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw==
+-----END CERTIFICATE-----
+
+Chambers of Commerce Root - 2008
+================================
+-----BEGIN CERTIFICATE-----
+MIIHTzCCBTegAwIBAgIJAKPaQn6ksa7aMA0GCSqGSIb3DQEBBQUAMIGuMQswCQYDVQQGEwJFVTFD
+MEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNv
+bS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMu
+QS4xKTAnBgNVBAMTIENoYW1iZXJzIG9mIENvbW1lcmNlIFJvb3QgLSAyMDA4MB4XDTA4MDgwMTEy
+Mjk1MFoXDTM4MDczMTEyMjk1MFowga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNl
+ZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29tL2FkZHJlc3MpMRIwEAYDVQQF
+EwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVyZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJl
+cnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
+AQCvAMtwNyuAWko6bHiUfaN/Gh/2NdW928sNRHI+JrKQUrpjOyhYb6WzbZSm891kDFX29ufyIiKA
+XuFixrYp4YFs8r/lfTJqVKAyGVn+H4vXPWCGhSRv4xGzdz4gljUha7MI2XAuZPeEklPWDrCQiorj
+h40G072QDuKZoRuGDtqaCrsLYVAGUvGef3bsyw/QHg3PmTA9HMRFEFis1tPo1+XqxQEHd9ZR5gN/
+ikilTWh1uem8nk4ZcfUyS5xtYBkL+8ydddy/Js2Pk3g5eXNeJQ7KXOt3EgfLZEFHcpOrUMPrCXZk
+NNI5t3YRCQ12RcSprj1qr7V9ZS+UWBDsXHyvfuK2GNnQm05aSd+pZgvMPMZ4fKecHePOjlO+Bd5g
+D2vlGts/4+EhySnB8esHnFIbAURRPHsl18TlUlRdJQfKFiC4reRB7noI/plvg6aRArBsNlVq5331
+lubKgdaX8ZSD6e2wsWsSaR6s+12pxZjptFtYer49okQ6Y1nUCyXeG0+95QGezdIp1Z8XGQpvvwyQ
+0wlf2eOKNcx5Wk0ZN5K3xMGtr/R5JJqyAQuxr1yW84Ay+1w9mPGgP0revq+ULtlVmhduYJ1jbLhj
+ya6BXBg14JC7vjxPNyK5fuvPnnchpj04gftI2jE9K+OJ9dC1vX7gUMQSibMjmhAxhduub+84Mxh2
+EQIDAQABo4IBbDCCAWgwEgYDVR0TAQH/BAgwBgEB/wIBDDAdBgNVHQ4EFgQU+SSsD7K1+HnA+mCI
+G8TZTQKeFxkwgeMGA1UdIwSB2zCB2IAU+SSsD7K1+HnA+mCIG8TZTQKeFxmhgbSkgbEwga4xCzAJ
+BgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNlZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNh
+bWVyZmlybWEuY29tL2FkZHJlc3MpMRIwEAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENh
+bWVyZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDiC
+CQCj2kJ+pLGu2jAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUH
+AgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZIhvcNAQEFBQADggIBAJASryI1
+wqM58C7e6bXpeHxIvj99RZJe6dqxGfwWPJ+0W2aeaufDuV2I6A+tzyMP3iU6XsxPpcG1Lawk0lgH
+3qLPaYRgM+gQDROpI9CF5Y57pp49chNyM/WqfcZjHwj0/gF/JM8rLFQJ3uIrbZLGOU8W6jx+ekbU
+RWpGqOt1glanq6B8aBMz9p0w8G8nOSQjKpD9kCk18pPfNKXG9/jvjA9iSnyu0/VU+I22mlaHFoI6
+M6taIgj3grrqLuBHmrS1RaMFO9ncLkVAO+rcf+g769HsJtg1pDDFOqxXnrN2pSB7+R5KBWIBpih1
+YJeSDW4+TTdDDZIVnBgizVGZoCkaPF+KMjNbMMeJL0eYD6MDxvbxrN8y8NmBGuScvfaAFPDRLLmF
+9dijscilIeUcE5fuDr3fKanvNFNb0+RqE4QGtjICxFKuItLcsiFCGtpA8CnJ7AoMXOLQusxI0zcK
+zBIKinmwPQN/aUv0NCB9szTqjktk9T79syNnFQ0EuPAtwQlRPLJsFfClI9eDdOTlLsn+mCdCxqvG
+nrDQWzilm1DefhiYtUU79nm06PcaewaD+9CL2rvHvRirCG88gGtAPxkZumWK5r7VXNM21+9AUiRg
+OGcEMeyP84LG3rlV8zsxkVrctQgVrXYlCg17LofiDKYGvCYQbTed7N14jHyAxfDZd0jQ
+-----END CERTIFICATE-----
+
+Global Chambersign Root - 2008
+==============================
+-----BEGIN CERTIFICATE-----
+MIIHSTCCBTGgAwIBAgIJAMnN0+nVfSPOMA0GCSqGSIb3DQEBBQUAMIGsMQswCQYDVQQGEwJFVTFD
+MEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNv
+bS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMu
+QS4xJzAlBgNVBAMTHkdsb2JhbCBDaGFtYmVyc2lnbiBSb290IC0gMjAwODAeFw0wODA4MDExMjMx
+NDBaFw0zODA3MzExMjMxNDBaMIGsMQswCQYDVQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUg
+Y3VycmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJ
+QTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAlBgNVBAMTHkdsb2JhbCBD
+aGFtYmVyc2lnbiBSb290IC0gMjAwODCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDf
+VtPkOpt2RbQT2//BthmLN0EYlVJH6xedKYiONWwGMi5HYvNJBL99RDaxccy9Wglz1dmFRP+RVyXf
+XjaOcNFccUMd2drvXNL7G706tcuto8xEpw2uIRU/uXpbknXYpBI4iRmKt4DS4jJvVpyR1ogQC7N0
+ZJJ0YPP2zxhPYLIj0Mc7zmFLmY/CDNBAspjcDahOo7kKrmCgrUVSY7pmvWjg+b4aqIG7HkF4ddPB
+/gBVsIdU6CeQNR1MM62X/JcumIS/LMmjv9GYERTtY/jKmIhYF5ntRQOXfjyGHoiMvvKRhI9lNNgA
+TH23MRdaKXoKGCQwoze1eqkBfSbW+Q6OWfH9GzO1KTsXO0G2Id3UwD2ln58fQ1DJu7xsepeY7s2M
+H/ucUa6LcL0nn3HAa6x9kGbo1106DbDVwo3VyJ2dwW3Q0L9R5OP4wzg2rtandeavhENdk5IMagfe
+Ox2YItaswTXbo6Al/3K1dh3ebeksZixShNBFks4c5eUzHdwHU1SjqoI7mjcv3N2gZOnm3b2u/GSF
+HTynyQbehP9r6GsaPMWis0L7iwk+XwhSx2LE1AVxv8Rk5Pihg+g+EpuoHtQ2TS9x9o0o9oOpE9Jh
+wZG7SMA0j0GMS0zbaRL/UJScIINZc+18ofLx/d33SdNDWKBWY8o9PeU1VlnpDsogzCtLkykPAgMB
+AAGjggFqMIIBZjASBgNVHRMBAf8ECDAGAQH/AgEMMB0GA1UdDgQWBBS5CcqcHtvTbDprru1U8VuT
+BjUuXjCB4QYDVR0jBIHZMIHWgBS5CcqcHtvTbDprru1U8VuTBjUuXqGBsqSBrzCBrDELMAkGA1UE
+BhMCRVUxQzBBBgNVBAcTOk1hZHJpZCAoc2VlIGN1cnJlbnQgYWRkcmVzcyBhdCB3d3cuY2FtZXJm
+aXJtYS5jb20vYWRkcmVzcykxEjAQBgNVBAUTCUE4Mjc0MzI4NzEbMBkGA1UEChMSQUMgQ2FtZXJm
+aXJtYSBTLkEuMScwJQYDVQQDEx5HbG9iYWwgQ2hhbWJlcnNpZ24gUm9vdCAtIDIwMDiCCQDJzdPp
+1X0jzjAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUHAgEWHGh0
+dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZIhvcNAQEFBQADggIBAICIf3DekijZBZRG
+/5BXqfEv3xoNa/p8DhxJJHkn2EaqbylZUohwEurdPfWbU1Rv4WCiqAm57OtZfMY18dwY6fFn5a+6
+ReAJ3spED8IXDneRRXozX1+WLGiLwUePmJs9wOzL9dWCkoQ10b42OFZyMVtHLaoXpGNR6woBrX/s
+dZ7LoR/xfxKxueRkf2fWIyr0uDldmOghp+G9PUIadJpwr2hsUF1Jz//7Dl3mLEfXgTpZALVza2Mg
+9jFFCDkO9HB+QHBaP9BrQql0PSgvAm11cpUJjUhjxsYjV5KTXjXBjfkK9yydYhz2rXzdpjEetrHH
+foUm+qRqtdpjMNHvkzeyZi99Bffnt0uYlDXA2TopwZ2yUDMdSqlapskD7+3056huirRXhOukP9Du
+qqqHW2Pok+JrqNS4cnhrG+055F3Lm6qH1U9OAP7Zap88MQ8oAgF9mOinsKJknnn4SPIVqczmyETr
+P3iZ8ntxPjzxmKfFGBI/5rsoM0LpRQp8bfKGeS/Fghl9CYl8slR2iK7ewfPM4W7bMdaTrpmg7yVq
+c5iJWzouE4gev8CSlDQb4ye3ix5vQv/n6TebUB0tovkC7stYWDpxvGjjqsGvHCgfotwjZT+B6q6Z
+09gwzxMNTxXJhLynSC34MCN32EZLeW32jO06f2ARePTpm67VVMB0gNELQp/B
+-----END CERTIFICATE-----
+
+Go Daddy Root Certificate Authority - G2
+========================================
+-----BEGIN CERTIFICATE-----
+MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT
+B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoTEUdvRGFkZHkuY29tLCBJbmMu
+MTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5
+MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6
+b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8G
+A1UEAxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKDE6bFIEMBO4Tx5oVJnyfq
+9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD
++qK+ihVqf94Lw7YZFAXK6sOoBJQ7RnwyDfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutd
+fMh8+7ArU6SSYmlRJQVhGkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMl
+NAJWJwGRtDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEAAaNC
+MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFDqahQcQZyi27/a9
+BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmXWWcDYfF+OwYxdS2hII5PZYe096ac
+vNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r
+5N9ss4UXnT3ZJE95kTXWXwTrgIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYV
+N8Gb5DKj7Tjo2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO
+LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI4uJEvlz36hz1
+-----END CERTIFICATE-----
+
+Starfield Root Certificate Authority - G2
+=========================================
+-----BEGIN CERTIFICATE-----
+MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT
+B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s
+b2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVsZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0
+eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAw
+DgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQg
+VGVjaG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZpY2F0ZSBB
+dXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3twQP89o/8ArFv
+W59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMgnLRJdzIpVv257IzdIvpy3Cdhl+72WoTs
+bhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNk
+N3mSwOxGXn/hbVNMYq/NHwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7Nf
+ZTD4p7dNdloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0HZbU
+JtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
+AQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0GCSqGSIb3DQEBCwUAA4IBAQARWfol
+TwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjUsHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx
+4mcujJUDJi5DnUox9g61DLu34jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUw
+F5okxBDgBPfg8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K
+pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1mMpYjn0q7pBZ
+c2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0
+-----END CERTIFICATE-----
+
+Starfield Services Root Certificate Authority - G2
+==================================================
+-----BEGIN CERTIFICATE-----
+MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMxEDAOBgNVBAgT
+B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s
+b2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVsZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRl
+IEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNV
+BAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxT
+dGFyZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2VydmljZXMg
+Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
+AQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20pOsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2
+h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm28xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4Pa
+hHQUw2eeBGg6345AWh1KTs9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLP
+LJGmpufehRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk6mFB
+rMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAwDwYDVR0TAQH/BAUw
+AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+qAdcwKziIorhtSpzyEZGDMA0GCSqG
+SIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMIbw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPP
+E95Dz+I0swSdHynVv/heyNXBve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTy
+xQGjhdByPq1zqwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd
+iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn0q23KXB56jza
+YyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCNsSi6
+-----END CERTIFICATE-----
+
+AffirmTrust Commercial
+======================
+-----BEGIN CERTIFICATE-----
+MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UEBhMCVVMxFDAS
+BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMB4XDTEw
+MDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly
+bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEF
+AAOCAQ8AMIIBCgKCAQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6Eqdb
+DuKPHx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yrba0F8PrV
+C8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPALMeIrJmqbTFeurCA+ukV6
+BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1yHp52UKqK39c/s4mT6NmgTWvRLpUHhww
+MmWd5jyTXlBOeuM61G7MGvv50jeuJCqrVwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNV
+HQ4EFgQUnZPGU4teyq8/nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
+AQYwDQYJKoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYGXUPG
+hi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNjvbz4YYCanrHOQnDi
+qX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivtZ8SOyUOyXGsViQK8YvxO8rUzqrJv
+0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9gN53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0kh
+sUlHRUe072o0EclNmsxZt9YCnlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8=
+-----END CERTIFICATE-----
+
+AffirmTrust Networking
+======================
+-----BEGIN CERTIFICATE-----
+MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UEBhMCVVMxFDAS
+BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMB4XDTEw
+MDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly
+bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEF
+AAOCAQ8AMIIBCgKCAQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SE
+Hi3yYJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbuakCNrmreI
+dIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRLQESxG9fhwoXA3hA/Pe24
+/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gb
+h+0t+nvujArjqWaJGctB+d1ENmHP4ndGyH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNV
+HQ4EFgQUBx/S55zawm6iQLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
+AQYwDQYJKoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfOtDIu
+UFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzuQY0x2+c06lkh1QF6
+12S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZLgo/bNjR9eUJtGxUAArgFU2HdW23
+WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4uolu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9
+/ZFvgrG+CJPbFEfxojfHRZ48x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s=
+-----END CERTIFICATE-----
+
+AffirmTrust Premium
+===================
+-----BEGIN CERTIFICATE-----
+MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UEBhMCVVMxFDAS
+BgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMB4XDTEwMDEy
+OTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRy
+dXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
+MIICCgKCAgEAxBLfqV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtn
+BKAQJG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ+jjeRFcV
+5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrSs8PhaJyJ+HoAVt70VZVs
++7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmd
+GPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d770O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5R
+p9EixAqnOEhss/n/fauGV+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NI
+S+LI+H+SqHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S5u04
+6uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4IaC1nEWTJ3s7xgaVY5
+/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TXOwF0lkLgAOIua+rF7nKsu7/+6qqo
++Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYEFJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB
+/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByv
+MiPIs0laUZx2KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg
+Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B8OWycvpEgjNC
+6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQMKSOyARiqcTtNd56l+0OOF6S
+L5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK
++4w1IX2COPKpVJEZNZOUbWo6xbLQu4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmV
+BtWVyuEklut89pMFu+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFg
+IxpHYoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8GKa1qF60
+g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaORtGdFNrHF+QFlozEJLUb
+zxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6eKeC2uAloGRwYQw==
+-----END CERTIFICATE-----
+
+AffirmTrust Premium ECC
+=======================
+-----BEGIN CERTIFICATE-----
+MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMCVVMxFDASBgNV
+BAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQcmVtaXVtIEVDQzAeFw0xMDAx
+MjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1U
+cnVzdDEgMB4GA1UEAwwXQWZmaXJtVHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQA
+IgNiAAQNMF4bFZ0D0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQ
+N8O9ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0GA1UdDgQW
+BBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAK
+BggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/VsaobgxCd05DhT1wV/GzTjxi+zygk8N53X
+57hG8f2h4nECMEJZh0PUUd+60wkyWs6Iflc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKM
+eQ==
+-----END CERTIFICATE-----
+
+Certum Trusted Network CA
+=========================
+-----BEGIN CERTIFICATE-----
+MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBMMSIwIAYDVQQK
+ExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlv
+biBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBUcnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIy
+MTIwNzM3WhcNMjkxMjMxMTIwNzM3WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBU
+ZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5
+MSIwIAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rHUV+rpDKmYYe2bg+G0jAC
+l/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LMTXPb865Px1bVWqeWifrzq2jUI4ZZJ88J
+J7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVUBBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4
+fOQtf/WsX+sWn7Et0brMkUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0
+cvW0QM8xAcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNVHRMB
+Af8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNVHQ8BAf8EBAMCAQYw
+DQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15ysHhE49wcrwn9I0j6vSrEuVUEtRCj
+jSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfLI9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1
+mS1FhIrlQgnXdAIv94nYmem8J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5aj
+Zt3hrvJBW8qYVoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI
+03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw=
+-----END CERTIFICATE-----
+
+Certinomis - Autorité Racine
+=============================
+-----BEGIN CERTIFICATE-----
+MIIFnDCCA4SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJGUjETMBEGA1UEChMK
+Q2VydGlub21pczEXMBUGA1UECxMOMDAwMiA0MzM5OTg5MDMxJjAkBgNVBAMMHUNlcnRpbm9taXMg
+LSBBdXRvcml0w6kgUmFjaW5lMB4XDTA4MDkxNzA4Mjg1OVoXDTI4MDkxNzA4Mjg1OVowYzELMAkG
+A1UEBhMCRlIxEzARBgNVBAoTCkNlcnRpbm9taXMxFzAVBgNVBAsTDjAwMDIgNDMzOTk4OTAzMSYw
+JAYDVQQDDB1DZXJ0aW5vbWlzIC0gQXV0b3JpdMOpIFJhY2luZTCCAiIwDQYJKoZIhvcNAQEBBQAD
+ggIPADCCAgoCggIBAJ2Fn4bT46/HsmtuM+Cet0I0VZ35gb5j2CN2DpdUzZlMGvE5x4jYF1AMnmHa
+wE5V3udauHpOd4cN5bjr+p5eex7Ezyh0x5P1FMYiKAT5kcOrJ3NqDi5N8y4oH3DfVS9O7cdxbwly
+Lu3VMpfQ8Vh30WC8Tl7bmoT2R2FFK/ZQpn9qcSdIhDWerP5pqZ56XjUl+rSnSTV3lqc2W+HN3yNw
+2F1MpQiD8aYkOBOo7C+ooWfHpi2GR+6K/OybDnT0K0kCe5B1jPyZOQE51kqJ5Z52qz6WKDgmi92N
+jMD2AR5vpTESOH2VwnHu7XSu5DaiQ3XV8QCb4uTXzEIDS3h65X27uK4uIJPT5GHfceF2Z5c/tt9q
+c1pkIuVC28+BA5PY9OMQ4HL2AHCs8MF6DwV/zzRpRbWT5BnbUhYjBYkOjUjkJW+zeL9i9Qf6lSTC
+lrLooyPCXQP8w9PlfMl1I9f09bze5N/NgL+RiH2nE7Q5uiy6vdFrzPOlKO1Enn1So2+WLhl+HPNb
+xxaOu2B9d2ZHVIIAEWBsMsGoOBvrbpgT1u449fCfDu/+MYHB0iSVL1N6aaLwD4ZFjliCK0wi1F6g
+530mJ0jfJUaNSih8hp75mxpZuWW/Bd22Ql095gBIgl4g9xGC3srYn+Y3RyYe63j3YcNBZFgCQfna
+4NH4+ej9Uji29YnfAgMBAAGjWzBZMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G
+A1UdDgQWBBQNjLZh2kS40RR9w759XkjwzspqsDAXBgNVHSAEEDAOMAwGCiqBegFWAgIAAQEwDQYJ
+KoZIhvcNAQEFBQADggIBACQ+YAZ+He86PtvqrxyaLAEL9MW12Ukx9F1BjYkMTv9sov3/4gbIOZ/x
+WqndIlgVqIrTseYyCYIDbNc/CMf4uboAbbnW/FIyXaR/pDGUu7ZMOH8oMDX/nyNTt7buFHAAQCva
+R6s0fl6nVjBhK4tDrP22iCj1a7Y+YEq6QpA0Z43q619FVDsXrIvkxmUP7tCMXWY5zjKn2BCXwH40
+nJ+U8/aGH88bc62UeYdocMMzpXDn2NU4lG9jeeu/Cg4I58UvD0KgKxRA/yHgBcUn4YQRE7rWhh1B
+CxMjidPJC+iKunqjo3M3NYB9Ergzd0A4wPpeMNLytqOx1qKVl4GbUu1pTP+A5FPbVFsDbVRfsbjv
+JL1vnxHDx2TCDyhihWZeGnuyt++uNckZM6i4J9szVb9o4XVIRFb7zdNIu0eJOqxp9YDG5ERQL1TE
+qkPFMTFYvZbF6nVsmnWxTfj3l/+WFvKXTej28xH5On2KOG4Ey+HTRRWqpdEdnV1j6CTmNhTih60b
+WfVEm/vXd3wfAXBioSAaosUaKPQhA+4u2cGA6rnZgtZbdsLLO7XSAPCjDuGtbkD326C00EauFddE
+wk01+dIL8hf2rGbVJLJP0RyZwG71fet0BLj5TXcJ17TPBzAJ8bgAVtkXFhYKK4bfjwEZGuW7gmP/
+vgt2Fl43N+bYdJeimUV5
+-----END CERTIFICATE-----
+
+Root CA Generalitat Valenciana
+==============================
+-----BEGIN CERTIFICATE-----
+MIIGizCCBXOgAwIBAgIEO0XlaDANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJFUzEfMB0GA1UE
+ChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UECxMGUEtJR1ZBMScwJQYDVQQDEx5Sb290
+IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmEwHhcNMDEwNzA2MTYyMjQ3WhcNMjEwNzAxMTUyMjQ3
+WjBoMQswCQYDVQQGEwJFUzEfMB0GA1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UE
+CxMGUEtJR1ZBMScwJQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmEwggEiMA0G
+CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGKqtXETcvIorKA3Qdyu0togu8M1JAJke+WmmmO3I2
+F0zo37i7L3bhQEZ0ZQKQUgi0/6iMweDHiVYQOTPvaLRfX9ptI6GJXiKjSgbwJ/BXufjpTjJ3Cj9B
+ZPPrZe52/lSqfR0grvPXdMIKX/UIKFIIzFVd0g/bmoGlu6GzwZTNVOAydTGRGmKy3nXiz0+J2ZGQ
+D0EbtFpKd71ng+CT516nDOeB0/RSrFOyA8dEJvt55cs0YFAQexvba9dHq198aMpunUEDEO5rmXte
+JajCq+TA81yc477OMUxkHl6AovWDfgzWyoxVjr7gvkkHD6MkQXpYHYTqWBLI4bft75PelAgxAgMB
+AAGjggM7MIIDNzAyBggrBgEFBQcBAQQmMCQwIgYIKwYBBQUHMAGGFmh0dHA6Ly9vY3NwLnBraS5n
+dmEuZXMwEgYDVR0TAQH/BAgwBgEB/wIBAjCCAjQGA1UdIASCAiswggInMIICIwYKKwYBBAG/VQIB
+ADCCAhMwggHoBggrBgEFBQcCAjCCAdoeggHWAEEAdQB0AG8AcgBpAGQAYQBkACAAZABlACAAQwBl
+AHIAdABpAGYAaQBjAGEAYwBpAPMAbgAgAFIAYQDtAHoAIABkAGUAIABsAGEAIABHAGUAbgBlAHIA
+YQBsAGkAdABhAHQAIABWAGEAbABlAG4AYwBpAGEAbgBhAC4ADQAKAEwAYQAgAEQAZQBjAGwAYQBy
+AGEAYwBpAPMAbgAgAGQAZQAgAFAAcgDhAGMAdABpAGMAYQBzACAAZABlACAAQwBlAHIAdABpAGYA
+aQBjAGEAYwBpAPMAbgAgAHEAdQBlACAAcgBpAGcAZQAgAGUAbAAgAGYAdQBuAGMAaQBvAG4AYQBt
+AGkAZQBuAHQAbwAgAGQAZQAgAGwAYQAgAHAAcgBlAHMAZQBuAHQAZQAgAEEAdQB0AG8AcgBpAGQA
+YQBkACAAZABlACAAQwBlAHIAdABpAGYAaQBjAGEAYwBpAPMAbgAgAHMAZQAgAGUAbgBjAHUAZQBu
+AHQAcgBhACAAZQBuACAAbABhACAAZABpAHIAZQBjAGMAaQDzAG4AIAB3AGUAYgAgAGgAdAB0AHAA
+OgAvAC8AdwB3AHcALgBwAGsAaQAuAGcAdgBhAC4AZQBzAC8AYwBwAHMwJQYIKwYBBQUHAgEWGWh0
+dHA6Ly93d3cucGtpLmd2YS5lcy9jcHMwHQYDVR0OBBYEFHs100DSHHgZZu90ECjcPk+yeAT8MIGV
+BgNVHSMEgY0wgYqAFHs100DSHHgZZu90ECjcPk+yeAT8oWykajBoMQswCQYDVQQGEwJFUzEfMB0G
+A1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UECxMGUEtJR1ZBMScwJQYDVQQDEx5S
+b290IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmGCBDtF5WgwDQYJKoZIhvcNAQEFBQADggEBACRh
+TvW1yEICKrNcda3FbcrnlD+laJWIwVTAEGmiEi8YPyVQqHxK6sYJ2fR1xkDar1CdPaUWu20xxsdz
+Ckj+IHLtb8zog2EWRpABlUt9jppSCS/2bxzkoXHPjCpaF3ODR00PNvsETUlR4hTJZGH71BTg9J63
+NI8KJr2XXPR5OkowGcytT6CYirQxlyric21+eLj4iIlPsSKRZEv1UN4D2+XFducTZnV+ZfsBn5OH
+iJ35Rld8TWCvmHMTI6QgkYH60GFmuH3Rr9ZvHmw96RH9qfmCIoaZM3Fa6hlXPZHNqcCjbgcTpsnt
++GijnsNacgmHKNHEc8RzGF9QdRYxn7fofMM=
+-----END CERTIFICATE-----
+
+A-Trust-nQual-03
+================
+-----BEGIN CERTIFICATE-----
+MIIDzzCCAregAwIBAgIDAWweMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYDVQQGEwJBVDFIMEYGA1UE
+Cgw/QS1UcnVzdCBHZXMuIGYuIFNpY2hlcmhlaXRzc3lzdGVtZSBpbSBlbGVrdHIuIERhdGVudmVy
+a2VociBHbWJIMRkwFwYDVQQLDBBBLVRydXN0LW5RdWFsLTAzMRkwFwYDVQQDDBBBLVRydXN0LW5R
+dWFsLTAzMB4XDTA1MDgxNzIyMDAwMFoXDTE1MDgxNzIyMDAwMFowgY0xCzAJBgNVBAYTAkFUMUgw
+RgYDVQQKDD9BLVRydXN0IEdlcy4gZi4gU2ljaGVyaGVpdHNzeXN0ZW1lIGltIGVsZWt0ci4gRGF0
+ZW52ZXJrZWhyIEdtYkgxGTAXBgNVBAsMEEEtVHJ1c3QtblF1YWwtMDMxGTAXBgNVBAMMEEEtVHJ1
+c3QtblF1YWwtMDMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtPWFuA/OQO8BBC4SA
+zewqo51ru27CQoT3URThoKgtUaNR8t4j8DRE/5TrzAUjlUC5B3ilJfYKvUWG6Nm9wASOhURh73+n
+yfrBJcyFLGM/BWBzSQXgYHiVEEvc+RFZznF/QJuKqiTfC0Li21a8StKlDJu3Qz7dg9MmEALP6iPE
+SU7l0+m0iKsMrmKS1GWH2WrX9IWf5DMiJaXlyDO6w8dB3F/GaswADm0yqLaHNgBid5seHzTLkDx4
+iHQF63n1k3Flyp3HaxgtPVxO59X4PzF9j4fsCiIvI+n+u33J4PTs63zEsMMtYrWacdaxaujs2e3V
+cuy+VwHOBVWf3tFgiBCzAgMBAAGjNjA0MA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0OBAoECERqlWdV
+eRFPMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAVdRU0VlIXLOThaq/Yy/kgM40
+ozRiPvbY7meIMQQDbwvUB/tOdQ/TLtPAF8fGKOwGDREkDg6lXb+MshOWcdzUzg4NCmgybLlBMRmr
+sQd7TZjTXLDR8KdCoLXEjq/+8T/0709GAHbrAvv5ndJAlseIOrifEXnzgGWovR/TeIGgUUw3tKZd
+JXDRZslo+S4RFGjxVJgIrCaSD96JntT6s3kr0qN51OyLrIdTaEJMUVF0HhsnLuP1Hyl0Te2v9+GS
+mYHovjrHF1D2t8b8m7CKa9aIA5GPBnc6hQLdmNVDeD/GMBWsm2vLV7eJUYs66MmEDNuxUCAKGkq6
+ahq97BvIxYSazQ==
+-----END CERTIFICATE-----
+
+TWCA Root Certification Authority
+=================================
+-----BEGIN CERTIFICATE-----
+MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJ
+VEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlmaWNh
+dGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMzWhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQG
+EwJUVzESMBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NB
+IFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFEAcK0HMMx
+QhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HHK3XLfJ+utdGdIzdjp9xC
+oi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeXRfwZVzsrb+RH9JlF/h3x+JejiB03HFyP
+4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/zrX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1r
+y+UPizgN7gr8/g+YnzAx3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIB
+BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkqhkiG
+9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeCMErJk/9q56YAf4lC
+mtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdlsXebQ79NqZp4VKIV66IIArB6nCWlW
+QtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62Dlhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVY
+T0bf+215WfKEIlKuD8z7fDvnaspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocny
+Yh0igzyXxfkZYiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw==
+-----END CERTIFICATE-----
+
+Security Communication RootCA2
+==============================
+-----BEGIN CERTIFICATE-----
+MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDElMCMGA1UEChMc
+U0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMeU2VjdXJpdHkgQ29tbXVuaWNh
+dGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoXDTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMC
+SlAxJTAjBgNVBAoTHFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3Vy
+aXR5IENvbW11bmljYXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+ANAVOVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGrzbl+dp++
++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVMVAX3NuRFg3sUZdbcDE3R
+3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQhNBqyjoGADdH5H5XTz+L62e4iKrFvlNV
+spHEfbmwhRkGeC7bYRr6hfVKkaHnFtWOojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1K
+EOtOghY6rCcMU/Gt1SSwawNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8
+QIH4D5csOPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEB
+CwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpFcoJxDjrSzG+ntKEj
+u/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXcokgfGT+Ok+vx+hfuzU7jBBJV1uXk
+3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6q
+tnRGEmyR7jTV7JqR50S+kDFy1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29
+mvVXIwAHIRc/SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03
+-----END CERTIFICATE-----
+
+EC-ACC
+======
+-----BEGIN CERTIFICATE-----
+MIIFVjCCBD6gAwIBAgIQ7is969Qh3hSoYqwE893EATANBgkqhkiG9w0BAQUFADCB8zELMAkGA1UE
+BhMCRVMxOzA5BgNVBAoTMkFnZW5jaWEgQ2F0YWxhbmEgZGUgQ2VydGlmaWNhY2lvIChOSUYgUS0w
+ODAxMTc2LUkpMSgwJgYDVQQLEx9TZXJ2ZWlzIFB1YmxpY3MgZGUgQ2VydGlmaWNhY2lvMTUwMwYD
+VQQLEyxWZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAoYykwMzE1MDMGA1UE
+CxMsSmVyYXJxdWlhIEVudGl0YXRzIGRlIENlcnRpZmljYWNpbyBDYXRhbGFuZXMxDzANBgNVBAMT
+BkVDLUFDQzAeFw0wMzAxMDcyMzAwMDBaFw0zMTAxMDcyMjU5NTlaMIHzMQswCQYDVQQGEwJFUzE7
+MDkGA1UEChMyQWdlbmNpYSBDYXRhbGFuYSBkZSBDZXJ0aWZpY2FjaW8gKE5JRiBRLTA4MDExNzYt
+SSkxKDAmBgNVBAsTH1NlcnZlaXMgUHVibGljcyBkZSBDZXJ0aWZpY2FjaW8xNTAzBgNVBAsTLFZl
+Z2V1IGh0dHBzOi8vd3d3LmNhdGNlcnQubmV0L3ZlcmFycmVsIChjKTAzMTUwMwYDVQQLEyxKZXJh
+cnF1aWEgRW50aXRhdHMgZGUgQ2VydGlmaWNhY2lvIENhdGFsYW5lczEPMA0GA1UEAxMGRUMtQUND
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyLHT+KXQpWIR4NA9h0X84NzJB5R85iK
+w5K4/0CQBXCHYMkAqbWUZRkiFRfCQ2xmRJoNBD45b6VLeqpjt4pEndljkYRm4CgPukLjbo73FCeT
+ae6RDqNfDrHrZqJyTxIThmV6PttPB/SnCWDaOkKZx7J/sxaVHMf5NLWUhdWZXqBIoH7nF2W4onW4
+HvPlQn2v7fOKSGRdghST2MDk/7NQcvJ29rNdQlB50JQ+awwAvthrDk4q7D7SzIKiGGUzE3eeml0a
+E9jD2z3Il3rucO2n5nzbcc8tlGLfbdb1OL4/pYUKGbio2Al1QnDE6u/LDsg0qBIimAy4E5S2S+zw
+0JDnJwIDAQABo4HjMIHgMB0GA1UdEQQWMBSBEmVjX2FjY0BjYXRjZXJ0Lm5ldDAPBgNVHRMBAf8E
+BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUoMOLRKo3pUW/l4Ba0fF4opvpXY0wfwYD
+VR0gBHgwdjB0BgsrBgEEAfV4AQMBCjBlMCwGCCsGAQUFBwIBFiBodHRwczovL3d3dy5jYXRjZXJ0
+Lm5ldC92ZXJhcnJlbDA1BggrBgEFBQcCAjApGidWZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5l
+dC92ZXJhcnJlbCAwDQYJKoZIhvcNAQEFBQADggEBAKBIW4IB9k1IuDlVNZyAelOZ1Vr/sXE7zDkJ
+lF7W2u++AVtd0x7Y/X1PzaBB4DSTv8vihpw3kpBWHNzrKQXlxJ7HNd+KDM3FIUPpqojlNcAZQmNa
+Al6kSBg6hW/cnbw/nZzBh7h6YQjpdwt/cKt63dmXLGQehb+8dJahw3oS7AwaboMMPOhyRp/7SNVe
+l+axofjk70YllJyJ22k4vuxcDlbHZVHlUIiIv0LVKz3l+bqeLrPK9HOSAgu+TGbrIP65y7WZf+a2
+E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xlnJ2lYJU6Un/10asIbvPuW/mIPX64b24D
+5EI=
+-----END CERTIFICATE-----
+
+Hellenic Academic and Research Institutions RootCA 2011
+=======================================================
+-----BEGIN CERTIFICATE-----
+MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1IxRDBCBgNVBAoT
+O0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9y
+aXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25z
+IFJvb3RDQSAyMDExMB4XDTExMTIwNjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYT
+AkdSMUQwQgYDVQQKEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25z
+IENlcnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNo
+IEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPzdYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI
+1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJfel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa
+71HFK9+WXesyHgLacEnsbgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u
+8yBRQlqD75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSPFEDH
+3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNVHRMBAf8EBTADAQH/
+MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp5dgTBCPuQSUwRwYDVR0eBEAwPqA8
+MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQub3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQu
+b3JnMA0GCSqGSIb3DQEBBQUAA4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVt
+XdMiKahsog2p6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8
+TqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7dIsXRSZMFpGD
+/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8AcysNnq/onN694/BtZqhFLKPM58N
+7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXIl7WdmplNsDz4SgCbZN2fOUvRJ9e4
+-----END CERTIFICATE-----
+
+Actalis Authentication Root CA
+==============================
+-----BEGIN CERTIFICATE-----
+MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCSVQxDjAM
+BgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UE
+AwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDky
+MjExMjIwMlowazELMAkGA1UEBhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlz
+IFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290
+IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNvUTufClrJ
+wkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX4ay8IMKx4INRimlNAJZa
+by/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9KK3giq0itFZljoZUj5NDKd45RnijMCO6
+zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1f
+YVEiVRvjRuPjPdA1YprbrxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2
+oxgkg4YQ51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2Fbe8l
+EfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxeKF+w6D9Fz8+vm2/7
+hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4Fv6MGn8i1zeQf1xcGDXqVdFUNaBr8
+EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbnfpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5
+jF66CyCU3nuDuP/jVo23Eek7jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLY
+iDrIn3hm7YnzezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt
+ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQALe3KHwGCmSUyI
+WOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70jsNjLiNmsGe+b7bAEzlgqqI0
+JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDzWochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKx
+K3JCaKygvU5a2hi/a5iB0P2avl4VSM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+
+Xlff1ANATIGk0k9jpwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC
+4yyXX04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+OkfcvHlXHo
+2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7RK4X9p2jIugErsWx0Hbhz
+lefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btUZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXem
+OR/qnuOf0GZvBeyqdn6/axag67XH/JJULysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9
+vwGYT7JZVEc+NHt4bVaTLnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg==
+-----END CERTIFICATE-----
+
+Trustis FPS Root CA
+===================
+-----BEGIN CERTIFICATE-----
+MIIDZzCCAk+gAwIBAgIQGx+ttiD5JNM2a/fH8YygWTANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQG
+EwJHQjEYMBYGA1UEChMPVHJ1c3RpcyBMaW1pdGVkMRwwGgYDVQQLExNUcnVzdGlzIEZQUyBSb290
+IENBMB4XDTAzMTIyMzEyMTQwNloXDTI0MDEyMTExMzY1NFowRTELMAkGA1UEBhMCR0IxGDAWBgNV
+BAoTD1RydXN0aXMgTGltaXRlZDEcMBoGA1UECxMTVHJ1c3RpcyBGUFMgUm9vdCBDQTCCASIwDQYJ
+KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMVQe547NdDfxIzNjpvto8A2mfRC6qc+gIMPpqdZh8mQ
+RUN+AOqGeSoDvT03mYlmt+WKVoaTnGhLaASMk5MCPjDSNzoiYYkchU59j9WvezX2fihHiTHcDnlk
+H5nSW7r+f2C/revnPDgpai/lkQtV/+xvWNUtyd5MZnGPDNcE2gfmHhjjvSkCqPoc4Vu5g6hBSLwa
+cY3nYuUtsuvffM/bq1rKMfFMIvMFE/eC+XN5DL7XSxzA0RU8k0Fk0ea+IxciAIleH2ulrG6nS4zt
+o3Lmr2NNL4XSFDWaLk6M6jKYKIahkQlBOrTh4/L68MkKokHdqeMDx4gVOxzUGpTXn2RZEm0CAwEA
+AaNTMFEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS6+nEleYtXQSUhhgtx67JkDoshZzAd
+BgNVHQ4EFgQUuvpxJXmLV0ElIYYLceuyZA6LIWcwDQYJKoZIhvcNAQEFBQADggEBAH5Y//01GX2c
+GE+esCu8jowU/yyg2kdbw++BLa8F6nRIW/M+TgfHbcWzk88iNVy2P3UnXwmWzaD+vkAMXBJV+JOC
+yinpXj9WV4s4NvdFGkwozZ5BuO1WTISkQMi4sKUraXAEasP41BIy+Q7DsdwyhEQsb8tGD+pmQQ9P
+8Vilpg0ND2HepZ5dfWWhPBfnqFVO76DH7cZEf1T1o+CP8HxVIo8ptoGj4W1OLBuAZ+ytIJ8MYmHV
+l/9D7S3B2l0pKoU/rGXuhg8FjZBf3+6f9L/uHfuY5H+QK4R4EA5sSVPvFVtlRkpdr7r7OnIdzfYl
+iB6XzCGcKQENZetX2fNXlrtIzYE=
+-----END CERTIFICATE-----
+
+StartCom Certification Authority
+================================
+-----BEGIN CERTIFICATE-----
+MIIHhzCCBW+gAwIBAgIBLTANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMN
+U3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmlu
+ZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0
+NjM3WhcNMzYwOTE3MTk0NjM2WjB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRk
+LjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMg
+U3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw
+ggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZkpMyONvg45iPwbm2xPN1y
+o4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rfOQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/
+Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/CJi/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/d
+eMotHweXMAEtcnn6RtYTKqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt
+2PZE4XNiHzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMMAv+Z
+6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w+2OqqGwaVLRcJXrJ
+osmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/
+untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVc
+UjyJthkqcwEKDwOzEmDyei+B26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT
+37uMdBNSSwIDAQABo4ICEDCCAgwwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD
+VR0OBBYEFE4L7xqkQFulF2mHMMo0aEPQQa7yMB8GA1UdIwQYMBaAFE4L7xqkQFulF2mHMMo0aEPQ
+Qa7yMIIBWgYDVR0gBIIBUTCCAU0wggFJBgsrBgEEAYG1NwEBATCCATgwLgYIKwYBBQUHAgEWImh0
+dHA6Ly93d3cuc3RhcnRzc2wuY29tL3BvbGljeS5wZGYwNAYIKwYBBQUHAgEWKGh0dHA6Ly93d3cu
+c3RhcnRzc2wuY29tL2ludGVybWVkaWF0ZS5wZGYwgc8GCCsGAQUFBwICMIHCMCcWIFN0YXJ0IENv
+bW1lcmNpYWwgKFN0YXJ0Q29tKSBMdGQuMAMCAQEagZZMaW1pdGVkIExpYWJpbGl0eSwgcmVhZCB0
+aGUgc2VjdGlvbiAqTGVnYWwgTGltaXRhdGlvbnMqIG9mIHRoZSBTdGFydENvbSBDZXJ0aWZpY2F0
+aW9uIEF1dGhvcml0eSBQb2xpY3kgYXZhaWxhYmxlIGF0IGh0dHA6Ly93d3cuc3RhcnRzc2wuY29t
+L3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilTdGFydENvbSBG
+cmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQsFAAOCAgEAjo/n3JR5
+fPGFf59Jb2vKXfuM/gTFwWLRfUKKvFO3lANmMD+x5wqnUCBVJX92ehQN6wQOQOY+2IirByeDqXWm
+N3PH/UvSTa0XQMhGvjt/UfzDtgUx3M2FIk5xt/JxXrAaxrqTi3iSSoX4eA+D/i+tLPfkpLst0OcN
+Org+zvZ49q5HJMqjNTbOx8aHmNrs++myziebiMMEofYLWWivydsQD032ZGNcpRJvkrKTlMeIFw6T
+tn5ii5B/q06f/ON1FE8qMt9bDeD1e5MNq6HPh+GlBEXoPBKlCcWw0bdT82AUuoVpaiF8H3VhFyAX
+e2w7QSlc4axa0c2Mm+tgHRns9+Ww2vl5GKVFP0lDV9LdJNUso/2RjSe15esUBppMeyG7Oq0wBhjA
+2MFrLH9ZXF2RsXAiV+uKa0hK1Q8p7MZAwC+ITGgBF3f0JBlPvfrhsiAhS90a2Cl9qrjeVOwhVYBs
+HvUwyKMQ5bLmKhQxw4UtjJixhlpPiVktucf3HMiKf8CdBUrmQk9io20ppB+Fq9vlgcitKj1MXVuE
+JnHEhV5xJMqlG2zYYdMa4FTbzrqpMrUi9nNBCV24F10OD5mQ1kfabwo6YigUZ4LZ8dCAWZvLMdib
+D4x3TrVoivJs9iQOLWxwxXPR3hTQcY+203sC9uO41Alua551hDnmfyWl8kgAwKQB2j8=
+-----END CERTIFICATE-----
+
+StartCom Certification Authority G2
+===================================
+-----BEGIN CERTIFICATE-----
+MIIFYzCCA0ugAwIBAgIBOzANBgkqhkiG9w0BAQsFADBTMQswCQYDVQQGEwJJTDEWMBQGA1UEChMN
+U3RhcnRDb20gTHRkLjEsMCoGA1UEAxMjU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg
+RzIwHhcNMTAwMTAxMDEwMDAxWhcNMzkxMjMxMjM1OTAxWjBTMQswCQYDVQQGEwJJTDEWMBQGA1UE
+ChMNU3RhcnRDb20gTHRkLjEsMCoGA1UEAxMjU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3Jp
+dHkgRzIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2iTZbB7cgNr2Cu+EWIAOVeq8O
+o1XJJZlKxdBWQYeQTSFgpBSHO839sj60ZwNq7eEPS8CRhXBF4EKe3ikj1AENoBB5uNsDvfOpL9HG
+4A/LnooUCri99lZi8cVytjIl2bLzvWXFDSxu1ZJvGIsAQRSCb0AgJnooD/Uefyf3lLE3PbfHkffi
+Aez9lInhzG7TNtYKGXmu1zSCZf98Qru23QumNK9LYP5/Q0kGi4xDuFby2X8hQxfqp0iVAXV16iul
+Q5XqFYSdCI0mblWbq9zSOdIxHWDirMxWRST1HFSr7obdljKF+ExP6JV2tgXdNiNnvP8V4so75qbs
+O+wmETRIjfaAKxojAuuKHDp2KntWFhxyKrOq42ClAJ8Em+JvHhRYW6Vsi1g8w7pOOlz34ZYrPu8H
+vKTlXcxNnw3h3Kq74W4a7I/htkxNeXJdFzULHdfBR9qWJODQcqhaX2YtENwvKhOuJv4KHBnM0D4L
+nMgJLvlblnpHnOl68wVQdJVznjAJ85eCXuaPOQgeWeU1FEIT/wCc976qUM/iUUjXuG+v+E5+M5iS
+FGI6dWPPe/regjupuznixL0sAA7IF6wT700ljtizkC+p2il9Ha90OrInwMEePnWjFqmveiJdnxMa
+z6eg6+OGCtP95paV1yPIN93EfKo2rJgaErHgTuixO/XWb/Ew1wIDAQABo0IwQDAPBgNVHRMBAf8E
+BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUS8W0QGutHLOlHGVuRjaJhwUMDrYwDQYJ
+KoZIhvcNAQELBQADggIBAHNXPyzVlTJ+N9uWkusZXn5T50HsEbZH77Xe7XRcxfGOSeD8bpkTzZ+K
+2s06Ctg6Wgk/XzTQLwPSZh0avZyQN8gMjgdalEVGKua+etqhqaRpEpKwfTbURIfXUfEpY9Z1zRbk
+J4kd+MIySP3bmdCPX1R0zKxnNBFi2QwKN4fRoxdIjtIXHfbX/dtl6/2o1PXWT6RbdejF0mCy2wl+
+JYt7ulKSnj7oxXehPOBKc2thz4bcQ///If4jXSRK9dNtD2IEBVeC2m6kMyV5Sy5UGYvMLD0w6dEG
+/+gyRr61M3Z3qAFdlsHB1b6uJcDJHgoJIIihDsnzb02CVAAgp9KP5DlUFy6NHrgbuxu9mk47EDTc
+nIhT76IxW1hPkWLIwpqazRVdOKnWvvgTtZ8SafJQYqz7Fzf07rh1Z2AQ+4NQ+US1dZxAF7L+/Xld
+blhYXzD8AK6vM8EOTmy6p6ahfzLbOOCxchcKK5HsamMm7YnUeMx0HgX4a/6ManY5Ka5lIxKVCCIc
+l85bBu4M4ru8H0ST9tg4RQUh7eStqxK2A6RCLi3ECToDZ2mEmuFZkIoohdVddLHRDiBYmxOlsGOm
+7XtH/UVVMKTumtTm4ofvmMkyghEpIrwACjFeLQ/Ajulrso8uBtjRkcfGEvRM/TAXw8HaOFvjqerm
+obp573PYtlNXLfbQ4ddI
+-----END CERTIFICATE-----
+
+Buypass Class 2 Root CA
+=======================
+-----BEGIN CERTIFICATE-----
+MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU
+QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMiBSb290IENBMB4X
+DTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1owTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1
+eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIw
+DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1
+g1Lr6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPVL4O2fuPn
+9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC911K2GScuVr1QGbNgGE41b
+/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHxMlAQTn/0hpPshNOOvEu/XAFOBz3cFIqU
+CqTqc/sLUegTBxj6DvEr0VQVfTzh97QZQmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeff
+awrbD02TTqigzXsu8lkBarcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgI
+zRFo1clrUs3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLiFRhn
+Bkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRSP/TizPJhk9H9Z2vX
+Uq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN9SG9dKpN6nIDSdvHXx1iY8f93ZHs
+M+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxPAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD
+VR0OBBYEFMmAd+BikoL1RpzzuvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF
+AAOCAgEAU18h9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s
+A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3tOluwlN5E40EI
+osHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo+fsicdl9sz1Gv7SEr5AcD48S
+aq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYd
+DnkM/crqJIByw5c/8nerQyIKx+u2DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWD
+LfJ6v9r9jv6ly0UsH8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0
+oyLQI+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK75t98biGC
+wWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h3PFaTWwyI0PurKju7koS
+CTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPzY11aWOIv4x3kqdbQCtCev9eBCfHJxyYN
+rJgWVqA=
+-----END CERTIFICATE-----
+
+Buypass Class 3 Root CA
+=======================
+-----BEGIN CERTIFICATE-----
+MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU
+QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMyBSb290IENBMB4X
+DTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFowTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1
+eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIw
+DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRH
+sJ8YZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3EN3coTRiR
+5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9tznDDgFHmV0ST9tD+leh
+7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX0DJq1l1sDPGzbjniazEuOQAnFN44wOwZ
+ZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH
+2xc519woe2v1n/MuwU8XKhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV
+/afmiSTYzIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvSO1UQ
+RwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D34xFMFbG02SrZvPA
+Xpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgPK9Dx2hzLabjKSWJtyNBjYt1gD1iq
+j6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD
+VR0OBBYEFEe4zf/lb+74suwvTg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF
+AAOCAgEAACAjQTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV
+cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXSIGrs/CIBKM+G
+uIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2HJLw5QY33KbmkJs4j1xrG0aG
+Q0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsaO5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8
+ZORK15FTAaggiG6cX0S5y2CBNOxv033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2
+KSb12tjE8nVhz36udmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz
+6MkEkbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg413OEMXbug
+UZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvDu79leNKGef9JOxqDDPDe
+eOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq4/g7u9xN12TyUb7mqqta6THuBrxzvxNi
+Cp/HuZc=
+-----END CERTIFICATE-----
+
+T-TeleSec GlobalRoot Class 3
+============================
+-----BEGIN CERTIFICATE-----
+MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoM
+IlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBU
+cnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgx
+MDAxMTAyOTU2WhcNMzMxMDAxMjM1OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lz
+dGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBD
+ZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0GCSqGSIb3
+DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN8ELg63iIVl6bmlQdTQyK
+9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/RLyTPWGrTs0NvvAgJ1gORH8EGoel15YU
+NpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZF
+iP0Zf3WHHx+xGwpzJFu5ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W
+0eDrXltMEnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGjQjBA
+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1A/d2O2GCahKqGFPr
+AyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOyWL6ukK2YJ5f+AbGwUgC4TeQbIXQb
+fsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzT
+ucpH9sry9uetuUg/vBa3wW306gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7h
+P0HHRwA11fXT91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml
+e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4pTpPDpFQUWw==
+-----END CERTIFICATE-----
+
+EE Certification Centre Root CA
+===============================
+-----BEGIN CERTIFICATE-----
+MIIEAzCCAuugAwIBAgIQVID5oHPtPwBMyonY43HmSjANBgkqhkiG9w0BAQUFADB1MQswCQYDVQQG
+EwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEoMCYGA1UEAwwfRUUgQ2Vy
+dGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVlMCIYDzIw
+MTAxMDMwMTAxMDMwWhgPMjAzMDEyMTcyMzU5NTlaMHUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlB
+UyBTZXJ0aWZpdHNlZXJpbWlza2Vza3VzMSgwJgYDVQQDDB9FRSBDZXJ0aWZpY2F0aW9uIENlbnRy
+ZSBSb290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwggEiMA0GCSqGSIb3DQEBAQUAA4IB
+DwAwggEKAoIBAQDIIMDs4MVLqwd4lfNE7vsLDP90jmG7sWLqI9iroWUyeuuOF0+W2Ap7kaJjbMeM
+TC55v6kF/GlclY1i+blw7cNRfdCT5mzrMEvhvH2/UpvObntl8jixwKIy72KyaOBhU8E2lf/slLo2
+rpwcpzIP5Xy0xm90/XsY6KxX7QYgSzIwWFv9zajmofxwvI6Sc9uXp3whrj3B9UiHbCe9nyV0gVWw
+93X2PaRka9ZP585ArQ/dMtO8ihJTmMmJ+xAdTX7Nfh9WDSFwhfYggx/2uh8Ej+p3iDXE/+pOoYtN
+P2MbRMNE1CV2yreN1x5KZmTNXMWcg+HCCIia7E6j8T4cLNlsHaFLAgMBAAGjgYowgYcwDwYDVR0T
+AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBLyWj7qVhy/zQas8fElyalL1BSZ
+MEUGA1UdJQQ+MDwGCCsGAQUFBwMCBggrBgEFBQcDAQYIKwYBBQUHAwMGCCsGAQUFBwMEBggrBgEF
+BQcDCAYIKwYBBQUHAwkwDQYJKoZIhvcNAQEFBQADggEBAHv25MANqhlHt01Xo/6tu7Fq1Q+e2+Rj
+xY6hUFaTlrg4wCQiZrxTFGGVv9DHKpY5P30osxBAIWrEr7BSdxjhlthWXePdNl4dp1BUoMUq5KqM
+lIpPnTX/dqQGE5Gion0ARD9V04I8GtVbvFZMIi5GQ4okQC3zErg7cBqklrkar4dBGmoYDQZPxz5u
+uSlNDUmJEYcyW+ZLBMjkXOZ0c5RdFpgTlf7727FE5TpwrDdr5rMzcijJs1eg9gIWiAYLtqZLICjU
+3j2LrTcFU3T+bsy8QxdxXvnFzBqpYe73dgzzcvRyrc9yAjYHR8/vGVCJYMzpJJUPwssd8m92kMfM
+dcGWxZ0=
+-----END CERTIFICATE-----
diff --git a/conf/config.rb b/conf/config.rb
new file mode 100644
index 0000000..91d8453
--- /dev/null
+++ b/conf/config.rb
@@ -0,0 +1,50 @@
+Vines::Config.configure do
+ # Set the logging level to debug, info, warn, error, or fatal. The debug
+ # level logs all XML sent and received by the server.
+ # If you want logging to STDOUT remove the path
+ # e.g. `log 'log/vines.log' do` becomes `log do`
+ log 'log/vines.log' do
+ level :info
+ end
+
+ # Set the directory in which to look for virtual hosts' TLS certificates.
+ # This is optional and defaults to the conf/certs directory created during
+ # `vines init`.
+ certs 'conf/certs'
+
+ # Set the maximum of offline messages stored per user.
+ # If it exceeds, old messages will be deleted.
+ max_offline_msgs 150
+
+ host 'diaspora' do
+ cross_domain_messages true
+ accept_self_signed false
+ force_s2s_encryption false
+ storage 'sql'
+ end
+
+ # Configure the client-to-server port. The max_resources_per_account attribute
+ # limits how many concurrent connections one user can have to the server.
+ client '0.0.0.0', 5222 do
+ max_stanza_size 65536
+ max_resources_per_account 5
+ end
+
+ # Configure the server-to-server port. The max_stanza_size attribute should be
+ # much larger than the setting for client-to-server.
+ server '0.0.0.0', 5269 do
+ max_stanza_size 131072
+ blacklist []
+ end
+
+ # Configure the built-in HTTP server that serves static files and responds to
+ # XEP-0124 BOSH requests. This allows HTTP clients to connect to
+ # the XMPP server.
+ http '0.0.0.0', 5280 do
+ bind '/http-bind'
+ max_stanza_size 65536
+ max_resources_per_account 5
+ root 'public'
+ vroute ''
+ end
+end
diff --git a/lib/vines.rb b/lib/vines.rb
new file mode 100644
index 0000000..139e7ea
--- /dev/null
+++ b/lib/vines.rb
@@ -0,0 +1,216 @@
+# encoding: UTF-8
+
+module Vines
+ NAMESPACES = {
+ :stream => 'http://etherx.jabber.org/streams'.freeze,
+ :client => 'jabber:client'.freeze,
+ :server => 'jabber:server'.freeze,
+ :component => 'jabber:component:accept'.freeze,
+ :roster => 'jabber:iq:roster'.freeze,
+ :non_sasl => 'jabber:iq:auth'.freeze,
+ :storage => 'jabber:iq:private'.freeze,
+ :version => 'jabber:iq:version'.freeze,
+ :sasl => 'urn:ietf:params:xml:ns:xmpp-sasl'.freeze,
+ :tls => 'urn:ietf:params:xml:ns:xmpp-tls'.freeze,
+ :bind => 'urn:ietf:params:xml:ns:xmpp-bind'.freeze,
+ :session => 'urn:ietf:params:xml:ns:xmpp-session'.freeze,
+ :stanzas => 'urn:ietf:params:xml:ns:xmpp-stanzas'.freeze,
+ :ping => 'urn:xmpp:ping'.freeze,
+ :delay => 'urn:xmpp:delay'.freeze,
+ :pubsub => 'http://jabber.org/protocol/pubsub'.freeze,
+ :pubsub_event => 'http://jabber.org/protocol/pubsub#event'.freeze,
+ :pubsub_create => 'http://jabber.org/protocol/pubsub#create-nodes'.freeze,
+ :pubsub_delete => 'http://jabber.org/protocol/pubsub#delete-nodes'.freeze,
+ :pubsub_instant => 'http://jabber.org/protocol/pubsub#instant-nodes'.freeze,
+ :pubsub_item_ids => 'http://jabber.org/protocol/pubsub#item-ids'.freeze,
+ :pubsub_publish => 'http://jabber.org/protocol/pubsub#publish'.freeze,
+ :pubsub_subscribe => 'http://jabber.org/protocol/pubsub#subscribe'.freeze,
+ :disco_items => 'http://jabber.org/protocol/disco#items'.freeze,
+ :disco_info => 'http://jabber.org/protocol/disco#info'.freeze,
+ :http_bind => 'http://jabber.org/protocol/httpbind'.freeze,
+ :offline => 'msgoffline'.freeze,
+ :bosh => 'urn:xmpp:xbosh'.freeze,
+ :vcard => 'vcard-temp'.freeze,
+ :vcard_update => 'vcard-temp:x:update'.freeze,
+ :si => 'http://jabber.org/protocol/si'.freeze,
+ :byte_streams => 'http://jabber.org/protocol/bytestreams'.freeze,
+ :dialback => 'urn:xmpp:features:dialback'.freeze,
+ :legacy_dialback => 'jabber:server:dialback'.freeze
+ }.freeze
+
+ module Log
+ @@logger = nil
+ def log
+ unless @@logger
+ @@logger = Logger.new(STDOUT)
+ @@logger.level = Logger::INFO
+ @@logger.progname = 'vines'
+ @@logger.formatter = Class.new(Logger::Formatter) do
+ def initialize
+ @time = "%Y-%m-%dT%H:%M:%SZ".freeze
+ @fmt = "[%s] %5s -- %s: %s\n".freeze
+ end
+ def call(severity, time, program, msg)
+ @fmt % [time.utc.strftime(@time), severity, program, msg2str(msg)]
+ end
+ end.new
+ end
+ @@logger
+ end
+ end
+end
+
+%w[
+ active_record
+ base64
+ bcrypt
+ digest/sha1
+ em-hiredis
+ eventmachine
+ fiber
+ fileutils
+ http/parser
+ json
+ logger
+ nokogiri
+ openssl
+ optparse
+ resolv
+ set
+ socket
+ time
+ uri
+ yaml
+
+ vines/cli
+ vines/log
+ vines/jid
+
+ vines/stanza
+ vines/stanza/iq
+ vines/stanza/iq/query
+ vines/stanza/iq/auth
+ vines/stanza/iq/disco_info
+ vines/stanza/iq/disco_items
+ vines/stanza/iq/error
+ vines/stanza/iq/ping
+ vines/stanza/iq/private_storage
+ vines/stanza/iq/result
+ vines/stanza/iq/roster
+ vines/stanza/iq/session
+ vines/stanza/iq/vcard
+ vines/stanza/iq/version
+ vines/stanza/dialback
+ vines/stanza/message
+ vines/stanza/presence
+ vines/stanza/presence/error
+ vines/stanza/presence/probe
+ vines/stanza/presence/subscribe
+ vines/stanza/presence/subscribed
+ vines/stanza/presence/unavailable
+ vines/stanza/presence/unsubscribe
+ vines/stanza/presence/unsubscribed
+ vines/stanza/pubsub
+ vines/stanza/pubsub/create
+ vines/stanza/pubsub/delete
+ vines/stanza/pubsub/publish
+ vines/stanza/pubsub/subscribe
+ vines/stanza/pubsub/unsubscribe
+
+ vines/storage
+ vines/storage/local
+ vines/storage/sql
+ vines/storage/null
+
+ vines/config
+ vines/config/host
+ vines/config/port
+ vines/config/pubsub
+
+ vines/store
+ vines/contact
+ vines/daemon
+ vines/error
+ vines/kit
+ vines/node
+ vines/router
+ vines/token_bucket
+ vines/user
+ vines/version
+ vines/xmpp_server
+
+ vines/cluster
+ vines/cluster/connection
+ vines/cluster/publisher
+ vines/cluster/pubsub
+ vines/cluster/sessions
+ vines/cluster/subscriber
+
+ vines/stream
+ vines/stream/sasl
+ vines/stream/state
+ vines/stream/parser
+
+ vines/stream/client
+ vines/stream/client/session
+ vines/stream/client/start
+ vines/stream/client/tls
+ vines/stream/client/auth_restart
+ vines/stream/client/auth
+ vines/stream/client/bind_restart
+ vines/stream/client/bind
+ vines/stream/client/ready
+ vines/stream/client/closed
+
+ vines/stream/component
+ vines/stream/component/start
+ vines/stream/component/handshake
+ vines/stream/component/ready
+
+ vines/stream/http
+ vines/stream/http/session
+ vines/stream/http/sessions
+ vines/stream/http/request
+ vines/stream/http/start
+ vines/stream/http/auth
+ vines/stream/http/bind_restart
+ vines/stream/http/bind
+ vines/stream/http/ready
+
+ vines/stream/server
+ vines/stream/server/start
+ vines/stream/server/auth_method
+ vines/stream/server/auth_restart
+ vines/stream/server/auth
+ vines/stream/server/final_restart
+ vines/stream/server/ready
+
+ vines/stream/server/outbound/start
+ vines/stream/server/outbound/auth
+ vines/stream/server/outbound/tls_result
+ vines/stream/server/outbound/authoritative
+ vines/stream/server/outbound/auth_restart
+ vines/stream/server/outbound/auth_external
+ vines/stream/server/outbound/auth_external_result
+ vines/stream/server/outbound/auth_dialback_result
+ vines/stream/server/outbound/final_restart
+ vines/stream/server/outbound/final_features
+
+ vines/command/cert
+ vines/command/restart
+ vines/command/start
+ vines/command/stop
+].each {|f| require f }
+
+# Try loading diaspora configuration
+%w[
+ config/application.rb
+ config/load_config.rb
+ config/initializers/devise.rb
+].each {|c|
+ begin
+ require "#{Dir.pwd}/#{c}"
+ rescue LoadError
+ puts "Was not able to load #{c}! This not a standalone version. You should use it only in a diaspora environment."
+ end
+}
diff --git a/lib/vines/cli.rb b/lib/vines/cli.rb
new file mode 100644
index 0000000..c5c9029
--- /dev/null
+++ b/lib/vines/cli.rb
@@ -0,0 +1,103 @@
+module Vines
+ # The command line application that's invoked by the `vines` binary included
+ # in the gem. Parses the command line arguments to create a new server
+ # directory, and starts and stops the server.
+ class CLI
+ COMMANDS = %w[start stop restart cert]
+
+ def self.start
+ self.new.start
+ end
+
+ # Run the command line application to parse arguments and run sub-commands.
+ # Exits the process with a non-zero return code to indicate failure.
+ #
+ # Returns nothing.
+ def start
+ opts = parse(ARGV)
+ command = Command.const_get(opts[:command].capitalize).new
+ begin
+ command.run(opts)
+ rescue SystemExit
+ # do nothing
+ end
+ end
+
+ private
+
+ # Parse the command line arguments and run the matching sub-command
+ # (e.g. init, start, stop, etc).
+ #
+ # args - The ARGV array provided by the command line.
+ #
+ # Returns nothing.
+ def parse(args)
+ options = {}
+ parser = OptionParser.new do |opts|
+ opts.banner = "Usage: vines [options] #{COMMANDS.join('|')}"
+
+ opts.separator ""
+ opts.separator "Daemon options:"
+
+ opts.on('-d', '--daemonize', 'Run daemonized in the background') do |daemonize|
+ options[:daemonize] = daemonize
+ end
+
+ options[:log] = 'log/vines.log'
+ opts.on('-l', '--log FILE', 'File to redirect output (default: log/vines.log)') do |log|
+ options[:log] = log
+ end
+
+ options[:pid] = 'pid/vines.pid'
+ opts.on('-P', '--pid FILE', 'File to store PID (default: pid/vines.pid)') do |pid|
+ options[:pid] = pid
+ end
+
+ opts.separator ""
+ opts.separator "Common options:"
+
+ opts.on('-h', '--help', 'Show this message') do |help|
+ options[:help] = help
+ end
+
+ opts.on('-v', '--version', 'Show version') do |version|
+ options[:version] = version
+ end
+ end
+
+ begin
+ parser.parse!(args)
+ rescue
+ puts parser
+ exit(1)
+ end
+
+ if options[:version]
+ puts Vines::VERSION
+ exit
+ end
+
+ if options[:help]
+ puts parser
+ exit
+ end
+
+ command = args.shift
+ unless COMMANDS.include?(command)
+ puts parser
+ exit(1)
+ end
+
+ options.tap do |opts|
+ opts[:args] = args
+ opts[:command] = command
+ opts[:config] = File.expand_path("conf/config.rb")
+ opts[:pid] = File.expand_path(opts[:pid])
+ opts[:log] = File.expand_path(opts[:log])
+ if defined? AppConfig
+ opts[:config] = "vines/config/diaspora"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/cluster.rb b/lib/vines/cluster.rb
new file mode 100644
index 0000000..29f559e
--- /dev/null
+++ b/lib/vines/cluster.rb
@@ -0,0 +1,246 @@
+# encoding: UTF-8
+
+module Vines
+ # Server instances may be connected to one another in a cluster so they
+ # can host a single chat domain, or set of domains, across many servers,
+ # transparently to users. A redis database is used for the session routing
+ # table, mapping JIDs to their node's location. Redis pubsub channels are
+ # used to communicate amongst nodes.
+ #
+ # Using a shared in-memory cache, like redis, rather than synchronizing the
+ # cache to each node, allows us to add cluster nodes dynamically, without
+ # updating all other nodes' config files. It also greatly reduces the amount
+ # of memory required by the chat server processes.
+ class Cluster
+ include Vines::Log
+
+ attr_reader :id
+
+ %w[host port database password].each do |name|
+ define_method(name) do |*args|
+ if args.first
+ @connection.send("#{name}=", args.first)
+ else
+ @connection.send(name)
+ end
+ end
+ end
+
+ def initialize(config, &block)
+ @config, @id = config, Kit.uuid
+ @connection = Connection.new
+ @sessions = Sessions.new(self)
+ @publisher = Publisher.new(self)
+ @subscriber = Subscriber.new(self)
+ @pubsub = PubSub.new(self)
+ instance_eval(&block)
+ end
+
+ # Join this node to the cluster by broadcasting its state to the
+ # other nodes, subscribing to redis channels, and scheduling periodic
+ # heartbeat broadcasts. This method must be called after initialize
+ # or this node will not be a cluster member.
+ def start
+ @connection.connect
+ @publisher.broadcast(:online)
+ @subscriber.subscribe
+
+ EM.add_periodic_timer(1) { heartbeat }
+
+ at_exit do
+ @publisher.broadcast(:offline)
+ @sessions.delete_all(@id)
+ end
+ end
+
+ # Returns any streams hosted at remote nodes for these JIDs. The streams act
+ # like normal EM::Connections, but are actually proxies that route stanzas
+ # over redis pubsub channels to remote nodes.
+ def remote_sessions(*jids)
+ @sessions.find(*jids).map do |session|
+ StreamProxy.new(self, session)
+ end
+ end
+
+ # Persist the user's session to the shared redis cache so that other cluster
+ # nodes can locate the node hosting this user's connection and route messages
+ # to them.
+ def save_session(jid, attrs)
+ @sessions.save(jid, attrs)
+ end
+
+ # Remove this user from the cluster routing table so that no further stanzas
+ # may be routed to them. This must be called when the user's session is
+ # terminated, either by logout or stream disconnect.
+ def delete_session(jid)
+ @sessions.delete(jid)
+ end
+
+ # Remove all user sessions from the routing table associated with the
+ # given node ID. Cluster nodes call this themselves during normal shutdown.
+ # However, if a node dies without being properly shutdown, the other nodes
+ # will cleanup its sessions when they detect the node is offline.
+ def delete_sessions(node)
+ @sessions.delete_all(node)
+ end
+
+ # Notify the session store that this node is still alive. The node
+ # broadcasts its current time, so all cluster members' clocks don't
+ # necessarily need to be in sync.
+ def poke(node, time)
+ @sessions.poke(node, time)
+ end
+
+ # Send the stanza to the node hosting the user's session. The stanza is
+ # published to the channel to which the remote node is listening for
+ # messages.
+ def route(stanza, node)
+ @publisher.route(stanza, node)
+ end
+
+ # Notify the remote node that the user's roster has changed and it should
+ # reload the user from storage.
+ def update_user(jid, node)
+ @publisher.update_user(jid, node)
+ end
+
+ # Return the shared redis connection for most queries to use.
+ def connection
+ @connection.connect
+ end
+
+ # Create a new redis connection.
+ def connect
+ @connection.create
+ end
+
+ # Turn an asynchronous redis query into a blocking call by pausing the
+ # fiber in which this code is running. Return the result of the query
+ # from this method, rather than passing it to a callback block.
+ def query(name, *args)
+ fiber, yielding = Fiber.current, true
+ req = connection.send(name, *args)
+ req.errback { fiber.resume rescue yielding = false }
+ req.callback {|response| fiber.resume(response) }
+ Fiber.yield if yielding
+ end
+
+ # Return the connected streams for this user, without any proxy streams
+ # to remote cluster nodes (locally connected streams only).
+ def connected_resources(jid)
+ @config.router.connected_resources(jid, jid, false)
+ end
+
+ # Return the Storage implementation for this domain or nil if the
+ # domain is not hosted here.
+ def storage(domain)
+ @config.storage(domain)
+ end
+
+ # Create a pubsub topic (a.k.a. node), in the given domain, to which
+ # messages may be published. The domain argument will be one of the
+ # configured pubsub subdomains in conf/config.rb (e.g. games.wonderland.lit,
+ # topics.wonderland.lit, etc).
+ def add_pubsub_node(domain, node)
+ @pubsub.add_node(domain, node)
+ end
+
+ # Remove a pubsub topic so messages may no longer be broadcast to it.
+ def delete_pubsub_node(domain, node)
+ @pubsub.delete_node(domain, node)
+ end
+
+ # Subscribe the JID to the pubsub topic so it will receive any messages
+ # published to it.
+ def subscribe_pubsub(domain, node, jid)
+ @pubsub.subscribe(domain, node, jid)
+ end
+
+ # Unsubscribe the JID from the pubsub topic, deregistering its interest
+ # in receiving any messages published to it.
+ def unsubscribe_pubsub(domain, node, jid)
+ @pubsub.unsubscribe(domain, node, jid)
+ end
+
+ # Unsubscribe the JID from all pubsub topics. This is useful when the
+ # JID's session ends by logout or disconnect.
+ def unsubscribe_all_pubsub(domain, jid)
+ @pubsub.unsubscribe_all(domain, jid)
+ end
+
+ # Return true if the pubsub topic exists and messages may be published to it.
+ def pubsub_node?(domain, node)
+ @pubsub.node?(domain, node)
+ end
+
+ # Return true if the JID is a registered subscriber to the pubsub topic and
+ # messages published to it should be routed to the JID.
+ def pubsub_subscribed?(domain, node, jid)
+ @pubsub.subscribed?(domain, node, jid)
+ end
+
+ # Return a list of JIDs subscribed to the pubsub topic.
+ def pubsub_subscribers(domain, node)
+ @pubsub.subscribers(domain, node)
+ end
+
+ private
+
+ # Call this method once per second to broadcast this node's heartbeat and
+ # expire stale user sessions. This method must not raise exceptions or the
+ # timer will stop.
+ def heartbeat
+ @publisher.broadcast(:heartbeat)
+ @sessions.expire
+ rescue => e
+ log.error("Cluster session cleanup failed: #{e}")
+ end
+
+ # StreamProxy behaves like an EM::Connection so that stanzas may be sent to
+ # remote nodes just as they are to locally connected streams. The rest of the
+ # system doesn't know or care that these "streams" send their traffic over
+ # redis pubsub channels.
+ class StreamProxy
+ attr_reader :user
+
+ def initialize(cluster, session)
+ @cluster, @user = cluster, UserProxy.new(cluster, session)
+ @node, @available, @interested, @presence =
+ session.values_at('node', 'available', 'interested', 'presence')
+
+ unless @presence.nil? || @presence.empty?
+ @presence = Nokogiri::XML(@presence).root rescue nil
+ end
+ end
+
+ def available?
+ @available
+ end
+
+ def interested?
+ @interested
+ end
+
+ def last_broadcast_presence
+ @presence
+ end
+
+ def write(stanza)
+ @cluster.route(stanza, @node)
+ end
+ end
+
+ # Proxy User#update_from calls to remote cluster nodes over redis
+ # pubsub channels.
+ class UserProxy < User
+ def initialize(cluster, session)
+ super(jid: session['jid'])
+ @cluster, @node = cluster, session['node']
+ end
+
+ def update_from(user)
+ @cluster.update_user(@jid.bare, @node)
+ end
+ end
+ end
+end
diff --git a/lib/vines/cluster/connection.rb b/lib/vines/cluster/connection.rb
new file mode 100644
index 0000000..84d8dca
--- /dev/null
+++ b/lib/vines/cluster/connection.rb
@@ -0,0 +1,26 @@
+# encoding: UTF-8
+
+module Vines
+ class Cluster
+ # Create and cache a redis database connection.
+ class Connection
+ attr_accessor :host, :port, :database, :password
+
+ def initialize
+ @redis, @host, @port, @database, @password = nil, nil, nil, nil, nil
+ end
+
+ # Return a shared redis connection.
+ def connect
+ @redis ||= create
+ end
+
+ # Return a new redis connection.
+ def create
+ conn = EM::Hiredis::Client.new(@host, @port, @password, @database)
+ conn.connect
+ conn
+ end
+ end
+ end
+end
diff --git a/lib/vines/cluster/publisher.rb b/lib/vines/cluster/publisher.rb
new file mode 100644
index 0000000..83731ad
--- /dev/null
+++ b/lib/vines/cluster/publisher.rb
@@ -0,0 +1,55 @@
+# encoding: UTF-8
+
+module Vines
+ class Cluster
+ # Broadcast messages to other cluster nodes via redis pubsub channels. All
+ # members subscribe to a channel for heartbeats, online, and offline
+ # messages from other nodes. This allows new nodes to be added to the
+ # cluster dynamically, without configuring all other nodes.
+ class Publisher
+ include Vines::Log
+
+ ALL, STANZA, USER = %w[cluster:nodes:all stanza user].map {|s| s.freeze }
+
+ def initialize(cluster)
+ @cluster = cluster
+ end
+
+ # Publish a :heartbeat, :online, or :offline message to the nodes:all
+ # broadcast channel.
+ def broadcast(type)
+ redis.publish(ALL, {
+ from: @cluster.id,
+ type: type,
+ time: Time.now.to_i
+ }.to_json)
+ end
+
+ # Send the stanza to the node hosting the user's session. The stanza is
+ # published to the channel to which the remote node is listening for
+ # messages.
+ def route(stanza, node)
+ log.debug { "Sent cluster stanza: %s -> %s\n%s\n" % [@cluster.id, node, stanza] }
+ redis.publish("cluster:nodes:#{node}", {
+ from: @cluster.id,
+ type: STANZA,
+ stanza: stanza.to_s
+ }.to_json)
+ end
+
+ # Notify the remote node that the user's roster has changed and it should
+ # reload the user from storage.
+ def update_user(jid, node)
+ redis.publish("cluster:nodes:#{node}", {
+ from: @cluster.id,
+ type: USER,
+ jid: jid.to_s
+ }.to_json)
+ end
+
+ def redis
+ @cluster.connection
+ end
+ end
+ end
+end
diff --git a/lib/vines/cluster/pubsub.rb b/lib/vines/cluster/pubsub.rb
new file mode 100644
index 0000000..2e14679
--- /dev/null
+++ b/lib/vines/cluster/pubsub.rb
@@ -0,0 +1,92 @@
+# encoding: UTF-8
+
+module Vines
+ class Cluster
+ # Manages the pubsub topic list and subscribers stored in redis. When a
+ # message is published to a topic, the receiving cluster node broadcasts
+ # the message to all subscribers at all other cluster nodes.
+ class PubSub
+ def initialize(cluster)
+ @cluster = cluster
+ end
+
+ # Create a pubsub topic (a.k.a. node), in the given domain, to which
+ # messages may be published. The domain argument will be one of the
+ # configured pubsub subdomains in conf/config.rb (e.g. games.wonderland.lit,
+ # topics.wonderland.lit, etc).
+ def add_node(domain, node)
+ redis.sadd("pubsub:#{domain}:nodes", node)
+ end
+
+ # Remove a pubsub topic so messages may no longer be broadcast to it.
+ def delete_node(domain, node)
+ redis.smembers("pubsub:#{domain}:subscribers_#{node}") do |subscribers|
+ redis.multi
+ subscribers.each do |jid|
+ redis.srem("pubsub:#{domain}:subscriptions_#{jid}", node)
+ end
+ redis.del("pubsub:#{domain}:subscribers_#{node}")
+ redis.srem("pubsub:#{domain}:nodes", node)
+ redis.exec
+ end
+ end
+
+ # Subscribe the JID to the pubsub topic so it will receive any messages
+ # published to it.
+ def subscribe(domain, node, jid)
+ jid = JID.new(jid)
+ redis.multi
+ redis.sadd("pubsub:#{domain}:subscribers_#{node}", jid.to_s)
+ redis.sadd("pubsub:#{domain}:subscriptions_#{jid}", node)
+ redis.exec
+ end
+
+ # Unsubscribe the JID from the pubsub topic, deregistering its interest
+ # in receiving any messages published to it.
+ def unsubscribe(domain, node, jid)
+ jid = JID.new(jid)
+ redis.multi
+ redis.srem("pubsub:#{domain}:subscribers_#{node}", jid.to_s)
+ redis.srem("pubsub:#{domain}:subscriptions_#{jid}", node)
+ redis.exec
+ redis.scard("pubsub:#{domain}:subscribers_#{node}") do |count|
+ delete_node(domain, node) if count == 0
+ end
+ end
+
+ # Unsubscribe the JID from all pubsub topics. This is useful when the
+ # JID's session ends by logout or disconnect.
+ def unsubscribe_all(domain, jid)
+ jid = JID.new(jid)
+ redis.smembers("pubsub:#{domain}:subscriptions_#{jid}") do |nodes|
+ nodes.each do |node|
+ unsubscribe(domain, node, jid)
+ end
+ end
+ end
+
+ # Return true if the pubsub topic exists and messages may be published to it.
+ def node?(domain, node)
+ @cluster.query(:sismember, "pubsub:#{domain}:nodes", node) == 1
+ end
+
+ # Return true if the JID is a registered subscriber to the pubsub topic and
+ # messages published to it should be routed to the JID.
+ def subscribed?(domain, node, jid)
+ jid = JID.new(jid)
+ @cluster.query(:sismember, "pubsub:#{domain}:subscribers_#{node}", jid.to_s) == 1
+ end
+
+ # Return a list of JIDs subscribed to the pubsub topic.
+ def subscribers(domain, node)
+ @cluster.query(:smembers, "pubsub:#{domain}:subscribers_#{node}")
+ end
+
+ private
+
+ def redis
+ @cluster.connection
+ end
+ end
+ end
+end
diff --git a/lib/vines/cluster/sessions.rb b/lib/vines/cluster/sessions.rb
new file mode 100644
index 0000000..a343667
--- /dev/null
+++ b/lib/vines/cluster/sessions.rb
@@ -0,0 +1,125 @@
+# encoding: UTF-8
+
+module Vines
+ class Cluster
+ # Manages the cluster node list and user session routing table stored in
+ # redis. All cluster nodes share this in-memory database to quickly discover
+ # the node hosting a particular user session. Once a session is located,
+ # stanzas can be routed to that node via the +Publisher+.
+ class Sessions
+ include Vines::Log
+
+ NODES = 'cluster:nodes'.freeze
+
+ def initialize(cluster)
+ @cluster, @nodes = cluster, {}
+ end
+
+ # Return the sessions for these JIDs. If a bare JID is used, all sessions
+ # for that user will be returned. If a full JID is used, the session for
+ # that single connected stream is returned.
+ def find(*jids)
+ jids.flatten.map do |jid|
+ jid = JID.new(jid)
+ jid.bare? ? user_sessions(jid) : user_session(jid)
+ end.compact.flatten
+ end
+
+ # Persist the user's session to the shared redis cache so that other cluster
+ # nodes can locate the node hosting this user's connection and route messages
+ # to them.
+ def save(jid, attrs)
+ jid = JID.new(jid)
+ session = {node: @cluster.id}.merge(attrs)
+ redis.multi
+ redis.hset("sessions:#{jid.bare}", jid.resource, session.to_json)
+ redis.sadd("cluster:nodes:#{@cluster.id}", jid.to_s)
+ redis.exec
+ end
+
+ # Remove this user from the cluster routing table so that no further stanzas
+ # may be routed to them. This must be called when the user's session is
+ # terminated, either by logout or stream disconnect.
+ def delete(jid)
+ jid = JID.new(jid)
+ redis.hget("sessions:#{jid.bare}", jid.resource) do |response|
+ if doc = JSON.parse(response) rescue nil
+ redis.multi
+ redis.hdel("sessions:#{jid.bare}", jid.resource)
+ redis.srem("cluster:nodes:#{doc['node']}", jid.to_s)
+ redis.exec
+ end
+ end
+ end
+
+ # Remove all user sessions from the routing table associated with the
+ # given node ID. Cluster nodes call this themselves during normal shutdown.
+ # However, if a node dies without being properly shutdown, the other nodes
+ # will cleanup its sessions when they detect the node is offline.
+ def delete_all(node)
+ @nodes.delete(node)
+ redis.smembers("cluster:nodes:#{node}") do |jids|
+ redis.multi
+ redis.del("cluster:nodes:#{node}")
+ redis.hdel(NODES, node)
+ jids.each do |jid|
+ jid = JID.new(jid)
+ redis.hdel("sessions:#{jid.bare}", jid.resource)
+ end
+ redis.exec
+ end
+ end
+
+ # Cluster nodes broadcast a heartbeat to other members every second. If we
+ # haven't heard from a node in five seconds, assume it's offline and cleanup
+ # its session cache for it. Nodes may die abrubtly, without a chance to clear
+ # their sessions, so other members cleanup for them.
+ def expire
+ redis.hset(NODES, @cluster.id, Time.now.to_i)
+ redis.hgetall(NODES) do |response|
+ now = Time.now
+ expired = Hash[*response].select do |node, active|
+ offset = @nodes[node] || 0
+ (now - offset) - Time.at(active.to_i) > 5
+ end.keys
+ expired.each {|node| delete_all(node) }
+ end
+ end
+
+ # Notify the session store that this node is still alive. The node
+ # broadcasts its current time, so all cluster members' clocks don't
+ # necessarily need to be in sync.
+ def poke(node, time)
+ offset = Time.now.to_i - time
+ @nodes[node] = offset
+ end
+
+ private
+
+ # Return all remote sessions for this user's bare JID.
+ def user_sessions(jid)
+ response = @cluster.query(:hgetall, "sessions:#{jid.bare}") || []
+ Hash[*response].map do |resource, json|
+ if session = JSON.parse(json) rescue nil
+ session['jid'] = JID.new(jid.node, jid.domain, resource).to_s
+ end
+ session
+ end.compact.reject {|session| session['node'] == @cluster.id }
+ end
+
+ # Return the remote session for this full JID or nil if not found.
+ def user_session(jid)
+ response = @cluster.query(:hget, "sessions:#{jid.bare}", jid.resource)
+ return unless response
+ session = JSON.parse(response) rescue nil
+ return if session.nil? || session['node'] == @cluster.id
+ session['jid'] = jid.to_s
+ session
+ end
+
+ def redis
+ @cluster.connection
+ end
+ end
+ end
+end
diff --git a/lib/vines/cluster/subscriber.rb b/lib/vines/cluster/subscriber.rb
new file mode 100644
index 0000000..0d7845a
--- /dev/null
+++ b/lib/vines/cluster/subscriber.rb
@@ -0,0 +1,133 @@
+# encoding: UTF-8
+
+module Vines
+ class Cluster
+ # Subscribes to the redis `nodes:all` broadcast channel to listen for
+ # heartbeats from other cluster members. Also subscribes to a channel
+ # exclusively for this particular node, listening for stanzas routed to us
+ # from other nodes.
+ class Subscriber
+ include Vines::Log
+
+ ALL, FROM, HEARTBEAT, OFFLINE, ONLINE, STANZA, TIME, TO, TYPE, USER =
+ %w[cluster:nodes:all from heartbeat offline online stanza time to type user].map {|s| s.freeze }
+
+ def initialize(cluster)
+ @cluster = cluster
+ @channel = "cluster:nodes:#{@cluster.id}"
+ @messages = EM::Queue.new
+ process_messages
+ end
+
+ # Create a new redis connection and subscribe to the nodes:all broadcast
+ # channel as well as the channel for this cluster node. Redis connections
+ # in subscribe mode cannot be used for other key/value operations.
+ #
+ # Returns nothing.
+ def subscribe
+ conn = @cluster.connect
+ conn.subscribe(ALL)
+ conn.subscribe(@channel)
+ conn.on(:message) do |channel, message|
+ @messages.push([channel, message])
+ end
+ end
+
+ private
+
+ # Recursively process incoming messages from the queue, guaranteeing they
+ # are processed in the order they are received.
+ #
+ # Returns nothing.
+ def process_messages
+ @messages.pop do |channel, message|
+ Fiber.new do
+ on_message(channel, message)
+ process_messages
+ end.resume
+ end
+ end
+
+ # Process messages as they arrive on the pubsub channels to which we're
+ # subscribed.
+ #
+ # channel - The String channel name on which the message was received.
+ # message - The JSON formatted message String.
+ #
+ # Returns nothing.
+ def on_message(channel, message)
+ doc = JSON.parse(message)
+ case channel
+ when ALL then to_all(doc)
+ when @channel then to_node(doc)
+ end
+ rescue => e
+ log.error("Cluster subscription message failed: #{e}")
+ end
+
+ # Process a message sent to the `nodes:all` broadcast channel. In the case
+ # of node heartbeats, we update the last time we heard from this node so
+ # we can cleanup its session if it goes offline.
+ #
+ # message - The parsed Hash of received message data.
+ #
+ # Returns nothing.
+ def to_all(message)
+ case message[TYPE]
+ when ONLINE, HEARTBEAT
+ @cluster.poke(message[FROM], message[TIME])
+ when OFFLINE
+ @cluster.delete_sessions(message[FROM])
+ end
+ end
+
+ # Process a message published to this node's channel. Messages sent to
+ # this channel are stanzas that need to be routed to connections attached
+ # to this node.
+ #
+ # message - The parsed Hash of received message data.
+ #
+ # Returns nothing.
+ def to_node(message)
+ case message[TYPE]
+ when STANZA then route_stanza(message)
+ when USER then update_user(message)
+ end
+ end
+
+ # Send the stanza, from a remote cluster node, to locally connected
+ # streams for the destination user.
+ #
+ # message - The parsed Hash of received message data.
+ #
+ # Returns nothing.
+ def route_stanza(message)
+ node = Nokogiri::XML(message[STANZA]).root rescue nil
+ return unless node
+ log.debug { "Received cluster stanza: %s -> %s\n%s\n" % [message[FROM], @cluster.id, node] }
+ if node[TO]
+ @cluster.connected_resources(node[TO]).each do |recipient|
+ recipient.write(node)
+ end
+ else
+ log.warn("Cluster stanza missing address:\n#{node}")
+ end
+ end
+
+ # Update the roster information, that's cached in locally connected
+ # streams, for this user.
+ #
+ # message - The parsed Hash of received message data.
+ #
+ # Returns nothing.
+ def update_user(message)
+ jid = JID.new(message['jid']).bare
+ if user = @cluster.storage(jid.domain).find_user(jid)
+ @cluster.connected_resources(jid).each do |stream|
+ stream.user.update_from(user)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/command/cert.rb b/lib/vines/command/cert.rb
new file mode 100644
index 0000000..7bcbc16
--- /dev/null
+++ b/lib/vines/command/cert.rb
@@ -0,0 +1,50 @@
+# encoding: UTF-8
+
+module Vines
+ module Command
+ class Cert
+ def run(opts)
+ raise 'vines cert <domain>' unless opts[:args].size == 1
+ require opts[:config]
+ create_cert(opts[:args].first, Config.instance.certs)
+ end
+
+ def create_cert(domain, dir)
+ domain = domain.downcase
+ key = OpenSSL::PKey::RSA.generate(2048)
+ ca = OpenSSL::X509::Name.parse("/C=US/ST=Colorado/L=Denver/O=Vines XMPP Server/CN=#{domain}")
+ cert = OpenSSL::X509::Certificate.new
+ cert.version = 2
+ cert.subject = ca
+ cert.issuer = ca
+ cert.serial = Time.now.to_i
+ cert.public_key = key.public_key
+ cert.not_before = Time.now - (24 * 60 * 60)
+ cert.not_after = Time.now + (365 * 24 * 60 * 60)
+
+ factory = OpenSSL::X509::ExtensionFactory.new
+ factory.subject_certificate = cert
+ factory.issuer_certificate = cert
+ cert.extensions = [
+ %w[basicConstraints CA:TRUE],
+ %w[subjectKeyIdentifier hash],
+ %w[subjectAltName] << [domain, hostname].map {|n| "DNS:#{n}" }.join(',')
+ ].map {|k, v| factory.create_ext(k, v) }
+
+ cert.sign(key, OpenSSL::Digest::SHA1.new)
+
+ {'key' => key, 'crt' => cert}.each_pair do |ext, o|
+ name = File.join(dir, "#{domain}.#{ext}")
+ File.open(name, 'w:utf-8') {|f| f.write(o.to_pem) }
+ File.chmod(0600, name) if ext == 'key'
+ end
+ end
+
+ private
+
+ def hostname
+ Socket.gethostbyname(Socket.gethostname).first.downcase
+ end
+ end
+ end
+end
diff --git a/lib/vines/command/restart.rb b/lib/vines/command/restart.rb
new file mode 100644
index 0000000..f7f2530
--- /dev/null
+++ b/lib/vines/command/restart.rb
@@ -0,0 +1,12 @@
+# encoding: UTF-8
+
+module Vines
+ module Command
+ class Restart
+ def run(opts)
+ Stop.new.run(opts)
+ Start.new.run(opts)
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/lib/vines/command/start.rb b/lib/vines/command/start.rb
new file mode 100644
index 0000000..6ab3615
--- /dev/null
+++ b/lib/vines/command/start.rb
@@ -0,0 +1,28 @@
+# encoding: UTF-8
+
+module Vines
+ module Command
+ class Start
+ def run(opts)
+ raise 'vines [--pid FILE] start' unless opts[:args].size == 0
+ require opts[:config]
+ server = XmppServer.new(Config.instance)
+ daemonize(opts) if opts[:daemonize]
+ server.start
+ end
+
+ private
+
+ def daemonize(opts)
+ daemon = Daemon.new(:pid => opts[:pid], :stdout => opts[:log],
+ :stderr => opts[:log])
+ if daemon.running?
+ raise "Vines is running as process #{daemon.pid}"
+ else
+ puts "Vines has started"
+ daemon.start
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/command/stop.rb b/lib/vines/command/stop.rb
new file mode 100644
index 0000000..1474b4f
--- /dev/null
+++ b/lib/vines/command/stop.rb
@@ -0,0 +1,18 @@
+# encoding: UTF-8
+
+module Vines
+ module Command
+ class Stop
+ def run(opts)
+ raise 'vines [--pid FILE] stop' unless opts[:args].size == 0
+ daemon = Daemon.new(:pid => opts[:pid])
+ if daemon.running?
+ daemon.stop
+ puts 'Vines has been shutdown'
+ else
+ puts 'Vines is not running'
+ end
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/lib/vines/config.rb b/lib/vines/config.rb
new file mode 100644
index 0000000..a43c6b9
--- /dev/null
+++ b/lib/vines/config.rb
@@ -0,0 +1,236 @@
+# encoding: UTF-8
+
+module Vines
+
+ # A Config object is passed to the stream handlers to give them access
+ # to server configuration information like virtual host names, storage
+ # systems, etc. This class provides the DSL methods used in the
+ # conf/config.rb file.
+ class Config
+ LOG_LEVELS = %w[debug info warn error fatal].freeze
+
+ attr_reader :router
+
+ @@instance = nil
+ def self.configure(&block)
+ @@instance = self.new(&block)
+ end
+
+ def self.instance
+ @@instance
+ end
+
+ def initialize(&block)
+ @max_offline_msgs = 150
+ @certs = File.expand_path('conf/certs')
+ @vhosts, @ports, @cluster = {}, {}, nil
+ @null = Storage::Null.new
+ @router = Router.new(self)
+ instance_eval(&block)
+ raise "must define at least one virtual host" if @vhosts.empty?
+ end
+
+ def certs(dir=nil)
+ dir ? @certs = File.expand_path(dir) : @certs
+ end
+
+ def max_offline_msgs(count=nil)
+ count ? @max_offline_msgs = count : @max_offline_msgs
+ end
+
+ def host(*names, &block)
+ names = names.flatten.map {|name| name.downcase }
+ dupes = names.uniq.size != names.size || (@vhosts.keys & names).any?
+ raise "one host definition per domain allowed" if dupes
+ names.each do |name|
+ @vhosts[name] = Host.new(self, name, &block)
+ end
+ end
+
+ def diaspora_domain
+ AppConfig.environment.url
+ .gsub(/^http(s){0,1}:\/\/|\/$/, '')
+ .to_s rescue "localhost"
+ end
+
+ %w[client server http component].each do |name|
+ define_method(name) do |*args, &block|
+ port = Vines::Config.const_get("#{name.capitalize}Port")
+ raise "one #{name} port definition allowed" if @ports[name.to_sym]
+ @ports[name.to_sym] = port.new(self, *args) do
+ instance_eval(&block) if block
+ end
+ end
+ end
+
+ def cluster(&block)
+ return @cluster unless block
+ raise "one cluster definition allowed" if @cluster
+ @cluster = Cluster.new(self, &block)
+ end
+
+ def log(file=nil, &block)
+ unless file.nil?
+ unless File.exists?(file)
+ File.new(file, 'w') rescue raise "log directory doesn't exists"
+ end
+
+ if File.exists?(file)
+ Vines::Log.set_log_file(file)
+ end
+ end
+ instance_eval(&block) if block
+ end
+
+ def level(level)
+ const = Logger.const_get(level.to_s.upcase) rescue nil
+ unless LOG_LEVELS.include?(level.to_s) && const
+ raise "log level must be one of: #{LOG_LEVELS.join(', ')}"
+ end
+ Class.new.extend(Vines::Log).log.level = const
+ end
+
+ def ports
+ @ports.values
+ end
+
+ # Return true if the domain is virtual hosted by this server.
+ def vhost?(domain)
+ !!vhost(domain)
+ end
+
+ # Return the Host config object for this domain if it's hosted by this
+ # server.
+ def vhost(domain)
+ @vhosts[domain.to_s]
+ end
+
+ # Returns the storage system for the domain or a Storage::Null instance if
+ # the domain is not hosted at this server.
+ def storage(domain)
+ host = vhost(domain)
+ host ? host.storage : @null
+ end
+
+ # Returns the PubSub system for the domain or nil if pubsub is not enabled
+ # for this domain.
+ def pubsub(domain)
+ host = @vhosts.values.find {|host| host.pubsub?(domain) }
+ host.pubsubs[domain.to_s] if host
+ end
+
+ # Return true if the domain is a pubsub service hosted at a virtual host
+ # at this server.
+ def pubsub?(domain)
+ @vhosts.values.any? {|host| host.pubsub?(domain) }
+ end
+
+ # Return true if all JIDs belong to components hosted by this server.
+ def component?(*jids)
+ !jids.flatten.index do |jid|
+ !component_password(JID.new(jid).domain)
+ end
+ end
+
+ # Return the password for the component or nil if it's not hosted here.
+ def component_password(domain)
+ host = @vhosts.values.find {|host| host.component?(domain) }
+ host.password(domain) if host
+ end
+
+ # Return true if all of the JIDs are hosted by this server.
+ def local_jid?(*jids)
+ !jids.flatten.index do |jid|
+ !vhost?(JID.new(jid).domain)
+ end
+ end
+
+ # Return true if private XML fragment storage is enabled for this domain.
+ def private_storage?(domain)
+ host = vhost(domain)
+ host.private_storage? if host
+ end
+
+ # Returns true if server-to-server connections are allowed with the
+ # given domain.
+ def s2s?(domain)
+ # Disabled whitelisting to allow anonymous hosts,
+ # otherwise everyone has to add manually all hosts.
+ # Using blacklist in case we have to block a malicious host.
+ @ports[:server] && !@ports[:server].blacklist.include?(domain.to_s)
+ end
+
+ # Return true if the server is a member of a cluster, serving the same
+ # domains from different machines.
+ def cluster?
+ !!@cluster
+ end
+
+ # Retrieve the Port subclass with this name:
+ # [:client, :server, :http, :component]
+ def [](name)
+ @ports[name] or raise ArgumentError.new("no port named #{name}")
+ end
+
+ # Return true if the two JIDs are allowed to send messages to each other.
+ # Both domains must have enabled cross_domain_messages in their config files.
+ def allowed?(to, from)
+ to, from = JID.new(to), JID.new(from)
+ return false if to.empty? || from.empty?
+ return true if to.domain == from.domain # same domain always allowed
+ return cross_domain?(to, from) if local_jid?(to, from) # both virtual hosted here
+ return check_subdomains(to, from) if subdomain?(to, from) # component/pubsub to component/pubsub
+ return check_subdomain(to, from) if subdomain?(to) # to component/pubsub
+ return check_subdomain(from, to) if subdomain?(from) # from component/pubsub
+ return cross_domain?(to) if local_jid?(to) # from is remote
+ return cross_domain?(from) if local_jid?(from) # to is remote
+ return false
+ end
+
+ private
+
+ # Return true if all of the JIDs are some kind of subdomain resource hosted
+ # here (either a component or a pubsub domain).
+ def subdomain?(*jids)
+ !jids.flatten.index do |jid|
+ !component?(jid) && !pubsub?(jid)
+ end
+ end
+
+ # Return true if the third-level subdomain JIDs (components and pubsubs)
+ # are allowed to communicate with each other. For example, a
+ # tea.wonderland.lit component should be allowed to send messages to
+ # pubsub.wonderland.lit because they share the second-level wonderland.lit
+ # domain.
+ def check_subdomains(to, from)
+ sub1, sub2 = strip_domain(to), strip_domain(from)
+ (sub1 == sub2) || cross_domain?(sub1, sub2)
+ end
+
+ # Return true if the third-level subdomain JID (component or pubsub) is
+ # allowed to communicate with the other JID. For example,
+ # pubsub.wonderland.lit should be allowed to send messages to
+ # alice at wonderland.lit because they share the second-level wonderland.lit
+ # domain.
+ def check_subdomain(subdomain, jid)
+ comp = strip_domain(subdomain)
+ return true if comp.domain == jid.domain
+ local_jid?(jid) ? cross_domain?(comp, jid) : cross_domain?(comp)
+ end
+
+ # Return the third-level JID's domain with the first subdomain stripped off
+ # to create a second-level domain. For example, alice at tea.wonderland.lit
+ # returns wonderland.lit.
+ def strip_domain(jid)
+ domain = jid.domain.split('.').drop(1).join('.')
+ JID.new(domain)
+ end
+
+ # Return true if all JIDs are allowed to exchange cross domain messages.
+ def cross_domain?(*jids)
+ !jids.flatten.index do |jid|
+ !vhost(jid.domain).cross_domain_messages?
+ end
+ end
+ end
+end
diff --git a/lib/vines/config/diaspora.rb b/lib/vines/config/diaspora.rb
new file mode 100644
index 0000000..24d7907
--- /dev/null
+++ b/lib/vines/config/diaspora.rb
@@ -0,0 +1,37 @@
+# ############################################################## #
+# Do NOT touch this file you'll find the options in diaspora.yml #
+# ############################################################## #
+
+Vines::Config.configure do
+ log AppConfig.chat.server.log.file.to_s do
+ level AppConfig.chat.server.log.level.to_sym
+ end
+
+ certs AppConfig.chat.server.certs.to_s
+
+ max_offline_msgs AppConfig.chat.server.max_offline_msgs.to_i
+
+ host diaspora_domain do
+ cross_domain_messages AppConfig.chat.server.cross_domain_messages
+ accept_self_signed AppConfig.chat.server.accept_self_signed
+ storage 'sql'
+ end
+
+ client AppConfig.chat.server.c2s.address.to_s, AppConfig.chat.server.c2s.port.to_i do
+ max_stanza_size AppConfig.chat.server.c2s.max_stanza_size.to_i
+ max_resources_per_account AppConfig.chat.server.c2s.max_resources_per_account.to_i
+ end
+
+ server AppConfig.chat.server.s2s.address.to_s, AppConfig.chat.server.s2s.port.to_i do
+ max_stanza_size AppConfig.chat.server.s2s.max_stanza_size.to_i
+ blacklist AppConfig.chat.server.s2s.blacklist.get
+ end
+
+ http AppConfig.chat.server.bosh.address.to_s, AppConfig.chat.server.bosh.port.to_i do
+ bind AppConfig.chat.server.bosh.bind.to_s
+ max_stanza_size AppConfig.chat.server.bosh.max_stanza_size.to_i
+ max_resources_per_account AppConfig.chat.server.bosh.max_resources_per_account.to_i
+ root 'public'
+ vroute ''
+ end
+end
diff --git a/lib/vines/config/host.rb b/lib/vines/config/host.rb
new file mode 100644
index 0000000..92ad26e
--- /dev/null
+++ b/lib/vines/config/host.rb
@@ -0,0 +1,137 @@
+# encoding: UTF-8
+
+module Vines
+ class Config
+
+ # Provides the DSL methods for the virtual host definitions in the
+ # conf/config.rb file. Host instances can be accessed at runtime through
+ # the +Config#vhosts+ method.
+ class Host
+ attr_reader :pubsubs
+
+ def initialize(config, name, &block)
+ @config, @name = config, name.downcase
+ @storage = nil
+ @cross_domain_messages = false
+ @private_storage = false
+ @accept_self_signed = false
+ @force_s2s_encryption = false
+ @components, @pubsubs = {}, {}
+ validate_domain(@name)
+ instance_eval(&block)
+ raise "storage required for #{@name}" unless @storage
+ end
+
+ def storage(name=nil, &block)
+ if name
+ raise "one storage mechanism per host allowed" if @storage
+ @storage = Storage.from_name(name, &block)
+ else
+ @storage
+ end
+ end
+
+ def force_s2s_encryption(enabled)
+ @force_s2s_encryption = !!enabled
+ end
+
+ def force_s2s_encryption?
+ @force_s2s_encryption
+ end
+
+ def accept_self_signed(enabled)
+ @accept_self_signed = !!enabled
+ end
+
+ def accept_self_signed?
+ @accept_self_signed
+ end
+
+ def cross_domain_messages(enabled)
+ @cross_domain_messages = !!enabled
+ end
+
+ def cross_domain_messages?
+ @cross_domain_messages
+ end
+
+ def components(options=nil)
+ return @components unless options
+
+ names = options.keys.map {|domain| "#{domain}.#{@name}".downcase }
+ raise "duplicate component domains not allowed" if dupes?(names, @components.keys)
+ raise "pubsub domains overlap component domains" if dupes?(names, @pubsubs.keys)
+
+ options.each do |domain, password|
+ raise 'component domain required' if (domain || '').to_s.strip.empty?
+ raise 'component password required' if (password || '').strip.empty?
+ name = "#{domain}.#{@name}".downcase
+ raise "components must be one level below their host: #{name}" if domain.to_s.include?('.')
+ validate_domain(name)
+ @components[name] = password
+ end
+ end
+
+ def component?(domain)
+ !!@components[domain.to_s]
+ end
+
+ def password(domain)
+ @components[domain.to_s]
+ end
+
+ def pubsub(*domains)
+ domains.flatten!
+ raise 'define at least one pubsub domain' if domains.empty?
+ names = domains.map {|domain| "#{domain}.#{@name}".downcase }
+ raise "duplicate pubsub domains not allowed" if dupes?(names, @pubsubs.keys)
+ raise "pubsub domains overlap component domains" if dupes?(names, @components.keys)
+ domains.each do |domain|
+ raise 'pubsub domain required' if (domain || '').to_s.strip.empty?
+ name = "#{domain}.#{@name}".downcase
+ raise "pubsub domains must be one level below their host: #{name}" if domain.to_s.include?('.')
+ validate_domain(name)
+ @pubsubs[name] = PubSub.new(@config, name)
+ end
+ end
+
+ def pubsub?(domain)
+ @pubsubs.key?(domain.to_s)
+ end
+
+ # Unsubscribe this JID from all pubsub topics hosted at this virtual host.
+ # This should be called when the user's session ends via logout or
+ # disconnect.
+ def unsubscribe_pubsub(jid)
+ @pubsubs.values.each do |pubsub|
+ pubsub.unsubscribe_all(jid)
+ end
+ end
+
+ def disco_items
+ [@components.keys, @pubsubs.keys].flatten.sort
+ end
+
+ def private_storage(enabled)
+ @private_storage = !!enabled
+ end
+
+ def private_storage?
+ @private_storage
+ end
+
+ private
+
+ # Return true if the arrays contain any duplicate items.
+ def dupes?(a, b)
+ a.uniq.size != a.size || b.uniq.size != b.size || (a & b).any?
+ end
+
+ # Prevent domains in config files that won't form valid JIDs.
+ def validate_domain(name)
+ jid = JID.new(name)
+ raise "incorrect domain: #{name}" if jid.node || jid.resource
+ end
+ end
+ end
+end
diff --git a/lib/vines/config/port.rb b/lib/vines/config/port.rb
new file mode 100644
index 0000000..40f7fad
--- /dev/null
+++ b/lib/vines/config/port.rb
@@ -0,0 +1,132 @@
+# encoding: UTF-8
+
+module Vines
+ class Config
+ class Port
+ include Vines::Log
+
+ attr_reader :config, :stream
+
+ %w[host port].each do |name|
+ define_method(name) do
+ @settings[name.to_sym]
+ end
+ end
+
+ def initialize(config, host, port, &block)
+ @config, @settings = config, {}
+ instance_eval(&block) if block
+ defaults = {:host => host, :port => port,
+ :max_resources_per_account => 5, :max_stanza_size => 128 * 1024}
+ @settings = defaults.merge(@settings)
+ end
+
+ def max_stanza_size(max=nil)
+ if max
+ # rfc 6120 section 13.12
+ @settings[:max_stanza_size] = [10000, max].max
+ else
+ @settings[:max_stanza_size]
+ end
+ end
+
+ def start
+ type = stream.name.split('::').last.downcase
+ log.info("Accepting #{type} connections on #{host}:#{port}")
+ EventMachine::start_server(host, port, stream, config)
+ end
+ end
+
+ class ClientPort < Port
+ def initialize(config, host='0.0.0.0', port=5222, &block)
+ @stream = Vines::Stream::Client
+ super(config, host, port, &block)
+ end
+
+ def max_resources_per_account(max=nil)
+ if max
+ @settings[:max_resources_per_account] = max
+ else
+ @settings[:max_resources_per_account]
+ end
+ end
+
+ def start
+ super
+ config.cluster.start if config.cluster?
+ end
+ end
+
+ class ServerPort < Port
+ def initialize(config, host='0.0.0.0', port=5269, &block)
+ @blacklist, @stream = [], Vines::Stream::Server
+ super(config, host, port, &block)
+ end
+
+ def blacklist(*blacklist)
+ if blacklist.any?
+ @blacklist << blacklist
+ @blacklist.flatten!
+ else
+ @blacklist
+ end
+ end
+ end
+
+ class HttpPort < Port
+ def initialize(config, host='0.0.0.0', port=5280, &block)
+ @stream = Vines::Stream::Http
+ super(config, host, port, &block)
+ defaults = {:root => File.expand_path('web'), :bind => '/xmpp'}
+ @settings = defaults.merge(@settings)
+ end
+
+ def max_resources_per_account(max=nil)
+ if max
+ @settings[:max_resources_per_account] = max
+ else
+ @settings[:max_resources_per_account]
+ end
+ end
+
+ def root(dir=nil)
+ if dir
+ @settings[:root] = File.expand_path(dir)
+ else
+ @settings[:root]
+ end
+ end
+
+ def bind(url=nil)
+ if url
+ @settings[:bind] = url
+ else
+ @settings[:bind]
+ end
+ end
+
+ def vroute(id=nil)
+ if id
+ id = id.to_s.strip
+ @settings[:vroute] = id.empty? ? nil : id
+ else
+ @settings[:vroute]
+ end
+ end
+
+ def start
+ super
+ if config.cluster? && vroute.nil?
+ log.warn("vroute sticky session cookie not set")
+ end
+ end
+ end
+
+ class ComponentPort < Port
+ def initialize(config, host='0.0.0.0', port=5347, &block)
+ @stream = Vines::Stream::Component
+ super(config, host, port, &block)
+ end
+ end
+ end
+end
diff --git a/lib/vines/config/pubsub.rb b/lib/vines/config/pubsub.rb
new file mode 100644
index 0000000..2423629
--- /dev/null
+++ b/lib/vines/config/pubsub.rb
@@ -0,0 +1,108 @@
+# encoding: UTF-8
+
+module Vines
+ class Config
+ # Provides the configuration DSL to conf/config.rb for pubsub subdomains and
+ # exposes the storage and notification systems that the pubsub stanzas need
+ # to process. This class hides the complexity of determining pubsub behavior
+ # in a standalone vs. clustered chat server environment from the stanzas.
+ class PubSub
+ def initialize(config, name)
+ @config, @name = config, name
+ @nodes = {}
+ end
+
+ def add_node(id)
+ if @config.cluster?
+ @config.cluster.add_pubsub_node(@name, id)
+ else
+ @nodes[id] ||= Set.new
+ end
+ end
+
+ def delete_node(id)
+ if @config.cluster?
+ @config.cluster.delete_pubsub_node(@name, id)
+ else
+ @nodes.delete(id)
+ end
+ end
+
+ def subscribe(node, jid)
+ return unless node?(node) && @config.allowed?(jid, @name)
+ if @config.cluster?
+ @config.cluster.subscribe_pubsub(@name, node, jid)
+ else
+ @nodes[node] << JID.new(jid)
+ end
+ end
+
+ def unsubscribe(node, jid)
+ return unless node?(node)
+ if @config.cluster?
+ @config.cluster.unsubscribe_pubsub(@name, node, jid)
+ else
+ @nodes[node].delete(JID.new(jid))
+ delete_node(node) if subscribers(node).empty?
+ end
+ end
+
+ def unsubscribe_all(jid)
+ if @config.cluster?
+ @config.cluster.unsubscribe_all_pubsub(@name, jid)
+ else
+ @nodes.keys.each do |node|
+ unsubscribe(node, jid)
+ end
+ end
+ end
+
+ def node?(node)
+ if @config.cluster?
+ @config.cluster.pubsub_node?(@name, node)
+ else
+ @nodes.key?(node)
+ end
+ end
+
+ def subscribed?(node, jid)
+ return false unless node?(node)
+ if @config.cluster?
+ @config.cluster.pubsub_subscribed?(@name, node, jid)
+ else
+ @nodes[node].include?(JID.new(jid))
+ end
+ end
+
+ def publish(node, stanza)
+ stanza['id'] = Kit.uuid
+ stanza['from'] = @name
+
+ local, remote = subscribers(node).partition {|jid| @config.local_jid?(jid) }
+
+ local.flat_map do |jid|
+ @config.router.connected_resources(jid, @name)
+ end.each do |recipient|
+ stanza['to'] = recipient.user.jid.to_s
+ recipient.write(stanza)
+ end
+
+ remote.each do |jid|
+ el = stanza.clone
+ el['to'] = jid.to_s
+ @config.router.route(el) rescue nil # ignore RemoteServerNotFound
+ end
+ end
+
+ private
+
+ def subscribers(node)
+ if @config.cluster?
+ @config.cluster.pubsub_subscribers(@name, node)
+ else
+ @nodes[node] || []
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/contact.rb b/lib/vines/contact.rb
new file mode 100644
index 0000000..4701da9
--- /dev/null
+++ b/lib/vines/contact.rb
@@ -0,0 +1,115 @@
+# encoding: UTF-8
+
+module Vines
+ class Contact
+ include Comparable
+
+ attr_accessor :name, :subscription, :ask, :groups, :from_diaspora
+ attr_reader :jid
+
+ def initialize(args={})
+ @jid = JID.new(args[:jid]).bare
+ raise ArgumentError, 'invalid jid' if @jid.empty?
+ @name = args[:name]
+ @subscription = args[:subscription] || 'none'
+ @from_diaspora = args[:from_diaspora] || false
+ @ask = args[:ask]
+ @groups = args[:groups] || []
+ end
+
+ def <=>(contact)
+ contact.is_a?(Contact) ? self.jid.to_s <=> contact.jid.to_s : nil
+ end
+
+ alias :eql? :==
+
+ def hash
+ jid.to_s.hash
+ end
+
+ def update_from(contact)
+ @name = contact.name
+ @subscription = contact.subscription
+ @from_diaspora = contact.from_diaspora
+ @ask = contact.ask
+ @groups = contact.groups.clone
+ end
+
+ # Returns true if this contact is in a state that allows the user
+ # to subscribe to their presence updates.
+ def can_subscribe?
+ @ask == 'subscribe' && %w[none from].include?(@subscription)
+ end
+
+ def subscribe_to
+ @subscription = (@subscription == 'none') ? 'to' : 'both'
+ @ask = nil
+ end
+
+ def unsubscribe_to
+ @subscription = (@subscription == 'both') ? 'from' : 'none'
+ end
+
+ def subscribe_from
+ @subscription = (@subscription == 'none') ? 'from' : 'both'
+ @ask = nil
+ end
+
+ def unsubscribe_from
+ @subscription = (@subscription == 'both') ? 'to' : 'none'
+ end
+
+ # Returns true if the user is subscribed to this contact's
+ # presence updates.
+ def subscribed_to?
+ %w[to both].include?(@subscription)
+ end
+
+ # Returns true if the user has a presence subscription from
+ # this contact. The contact is subscribed to this user's presence.
+ def subscribed_from?
+ %w[from both].include?(@subscription)
+ end
+
+ # Returns a hash of this contact's attributes suitable for persisting in
+ # a document store.
+ def to_h
+ {
+ 'name' => @name,
+ 'subscription' => @subscription,
+ 'from_diaspora' => @from_diaspora,
+ 'ask' => @ask,
+ 'groups' => @groups.sort!
+ }
+ end
+
+ # Write an iq stanza to the recipient stream representing this contact's
+ # current roster item state.
+ def send_roster_push(recipient)
+ doc = Nokogiri::XML::Document.new
+ node = doc.create_element('iq',
+ 'id' => Kit.uuid,
+ 'to' => recipient.user.jid.to_s,
+ 'type' => 'set')
+ node << doc.create_element('query', 'xmlns' => NAMESPACES[:roster]) do |query|
+ query << to_roster_xml
+ end
+ recipient.write(node)
+ end
+
+ # Returns this contact as an xmpp <item> element.
+ def to_roster_xml
+ doc = Nokogiri::XML::Document.new
+ doc.create_element('item') do |el|
+ el['ask'] = @ask unless @ask.nil? || @ask.empty?
+ el['jid'] = @jid.bare.to_s
+ el['name'] = @name unless @name.nil? || @name.empty?
+ el['subscription'] = @subscription
+ el['from_diaspora'] = @from_diaspora
+ @groups.sort!.each do |group|
+ el << doc.create_element('group', group)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/daemon.rb b/lib/vines/daemon.rb
new file mode 100644
index 0000000..a666b38
--- /dev/null
+++ b/lib/vines/daemon.rb
@@ -0,0 +1,78 @@
+# encoding: UTF-8
+
+module Vines
+
+ # Fork the current process into the background and manage pid
+ # files so we can kill the process later.
+ class Daemon
+
+ # Configure a new daemon process. Arguments hash can include the following
+ # keys: :pid (pid file name, required),
+ # :stdin, :stdout, :stderr (default to /dev/null)
+ def initialize(args)
+ @pid = args[:pid]
+ raise ArgumentError.new('pid file is required') unless @pid
+ raise ArgumentError.new('pid must be a file name') if File.directory?(@pid)
+ raise ArgumentError.new('pid file must be writable') unless File.writable?(File.dirname(@pid))
+ @stdin, @stdout, @stderr = [:stdin, :stdout, :stderr].map {|k| args[k] || '/dev/null' }
+ end
+
+ # Fork the current process into the background to start the
+ # daemon. Do nothing if the daemon is already running.
+ def start
+ daemonize unless running?
+ end
+
+ # Use the pid stored in the pid file created from a previous
+ # call to start to send a TERM signal to the process. Do nothing
+ # if the daemon is not running.
+ def stop
+ 10.times do
+ break unless running?
+ Process.kill('TERM', pid)
+ sleep(0.1)
+ end
+ end
+
+ # Returns true if the process is running as determined by the numeric
+ # pid stored in the pid file created by a previous call to start.
+ def running?
+ begin
+ pid && Process.kill(0, pid)
+ rescue Errno::ESRCH
+ delete_pid
+ false
+ rescue Errno::EPERM
+ true
+ end
+ end
+
+ # Returns the numeric process ID from the pid file.
+ # If the pid file does not exist, returns nil.
+ def pid
+ File.read(@pid).to_i if File.exists?(@pid)
+ end
+
+ private
+
+ def delete_pid
+ File.delete(@pid) if File.exists?(@pid)
+ end
+
+ # Fork process into background twice to release it from
+ # the controlling tty. Point open file descriptors shared
+ # with the parent process to separate destinations (e.g. /dev/null).
+ def daemonize
+ exit if fork
+ Process.setsid
+ exit if fork
+ Dir.chdir('/')
+ $stdin.reopen(@stdin)
+ $stdout.reopen(@stdout, 'a').sync = true
+ $stderr.reopen(@stderr, 'a').sync = true
+ File.open(@pid, 'w') {|f| f.write(Process.pid) }
+ at_exit { delete_pid }
+ trap('TERM') { exit }
+ end
+ end
+end
diff --git a/lib/vines/error.rb b/lib/vines/error.rb
new file mode 100644
index 0000000..2098ed0
--- /dev/null
+++ b/lib/vines/error.rb
@@ -0,0 +1,150 @@
+# encoding: UTF-8
+
+module Vines
+ class XmppError < StandardError
+ include Nokogiri::XML
+
+ # Returns the XML element name based on the exception class name.
+ # For example, Vines::BadFormat becomes bad-format.
+ def element_name
+ name = self.class.name.split('::').last
+ name.gsub(/([A-Z])/, '-\1').downcase[1..-1]
+ end
+ end
+
+ class SaslError < XmppError
+ NAMESPACE = 'urn:ietf:params:xml:ns:xmpp-sasl'.freeze
+
+ def initialize(text=nil)
+ @text = text
+ end
+
+ def to_xml
+ doc = Document.new
+ doc.create_element('failure') do |node|
+ node.add_namespace(nil, NAMESPACE)
+ node << doc.create_element(element_name)
+ if @text
+ node << doc.create_element('text') do |text|
+ text['xml:lang'] = 'en'
+ text.content = @text
+ end
+ end
+ end.to_xml(:indent => 0).gsub(/\n/, '')
+ end
+ end
+
+ class StreamError < XmppError
+ NAMESPACE = 'urn:ietf:params:xml:ns:xmpp-streams'.freeze
+
+ def initialize(text=nil)
+ @text = text
+ end
+
+ def to_xml
+ doc = Document.new
+ doc.create_element('stream:error') do |el|
+ el << doc.create_element(element_name, 'xmlns' => NAMESPACE)
+ if @text
+ el << doc.create_element('text', @text, 'xmlns' => NAMESPACE, 'xml:lang' => 'en')
+ end
+ end.to_xml(:indent => 0).gsub(/\n/, '')
+ end
+ end
+
+ class StanzaError < XmppError
+ TYPES = %w[auth cancel continue modify wait].freeze
+ KINDS = %w[message presence iq].freeze
+ NAMESPACE = 'urn:ietf:params:xml:ns:xmpp-stanzas'.freeze
+
+ def initialize(el, type, text=nil)
+ raise "type must be one of: %s" % TYPES.join(', ') unless TYPES.include?(type)
+ raise "stanza must be one of: %s" % KINDS.join(', ') unless KINDS.include?(el.name)
+ @stanza_kind, @type, @text = el.name, type, text
+ @id, @from, @to = %w[id from to].map {|a| el[a] }
+ end
+
+ def to_xml
+ doc = Document.new
+ doc.create_element(@stanza_kind) do |el|
+ el['from'] = @to if @to
+ el['id'] = @id if @id
+ el['to'] = @from if @from
+ el['type'] = 'error'
+ el << doc.create_element('error', 'type' => @type) do |error|
+ error << doc.create_element(element_name, 'xmlns' => NAMESPACE)
+ if @text
+ error << doc.create_element('text', @text, 'xmlns' => NAMESPACE, 'xml:lang' => 'en')
+ end
+ end
+ end.to_xml(:indent => 0).gsub(/\n/, '')
+ end
+ end
+
+ module SaslErrors
+ class Aborted < SaslError; end
+ class AccountDisabled < SaslError; end
+ class CredentialsExpired < SaslError; end
+ class EncryptionRequired < SaslError; end
+ class IncorrectEncoding < SaslError; end
+ class InvalidAuthzid < SaslError; end
+ class InvalidMechanism < SaslError; end
+ class MalformedRequest < SaslError; end
+ class MechanismTooWeak < SaslError; end
+ class NotAuthorized < SaslError; end
+ class TemporaryAuthFailure < SaslError; end
+ end
+
+ module StreamErrors
+ class BadFormat < StreamError; end
+ class BadNamespacePrefix < StreamError; end
+ class Conflict < StreamError; end
+ class ConnectionTimeout < StreamError; end
+ class HostGone < StreamError; end
+ class HostUnknown < StreamError; end
+ class ImproperAddressing < StreamError; end
+ class InternalServerError < StreamError; end
+ class InvalidFrom < StreamError; end
+ class InvalidNamespace < StreamError; end
+ class InvalidXml < StreamError; end
+ class NotAuthorized < StreamError; end
+ class NotWellFormed < StreamError; end
+ class PolicyViolation < StreamError; end
+ class RemoteConnectionFailed < StreamError; end
+ class Reset < StreamError; end
+ class ResourceConstraint < StreamError; end
+ class RestrictedXml < StreamError; end
+ class SeeOtherHost < StreamError; end
+ class SystemShutdown < StreamError; end
+ class UndefinedCondition < StreamError; end
+ class UnsupportedEncoding < StreamError; end
+ class UnsupportedFeature < StreamError; end
+ class UnsupportedStanzaType < StreamError; end
+ class UnsupportedVersion < StreamError; end
+ end
+
+ module StanzaErrors
+ class BadRequest < StanzaError; end
+ class Conflict < StanzaError; end
+ class FeatureNotImplemented < StanzaError; end
+ class Forbidden < StanzaError; end
+ class Gone < StanzaError; end
+ class InternalServerError < StanzaError; end
+ class ItemNotFound < StanzaError; end
+ class JidMalformed < StanzaError; end
+ class NotAcceptable < StanzaError; end
+ class NotAllowed < StanzaError; end
+ class NotAuthorized < StanzaError; end
+ class PolicyViolation < StanzaError; end
+ class RecipientUnavailable < StanzaError; end
+ class Redirect < StanzaError; end
+ class RegistrationRequired < StanzaError; end
+ class RemoteServerNotFound < StanzaError; end
+ class RemoteServerTimeout < StanzaError; end
+ class ResourceConstraint < StanzaError; end
+ class ServiceUnavailable < StanzaError; end
+ class SubscriptionRequired < StanzaError; end
+ class UndefinedCondition < StanzaError; end
+ class UnexpectedRequest < StanzaError; end
+ end
+end
diff --git a/lib/vines/jid.rb b/lib/vines/jid.rb
new file mode 100644
index 0000000..b84c00c
--- /dev/null
+++ b/lib/vines/jid.rb
@@ -0,0 +1,95 @@
+# encoding: UTF-8
+
+module Vines
+ class JID
+ include Comparable
+
+ PATTERN = /\A(?:([^@]*)@)??([^@\/]*)(?:\/(.*?))?\Z/.freeze
+
+ # http://tools.ietf.org/html/rfc6122#appendix-A
+ NODE_PREP = /[[:cntrl:] "&'\/:<>@]/.freeze
+
+ # http://tools.ietf.org/html/rfc3454#appendix-C
+ NAME_PREP = /[[:cntrl:] ]/.freeze
+
+ # http://tools.ietf.org/html/rfc6122#appendix-B
+ RESOURCE_PREP = /[[:cntrl:]]/.freeze
+
+ attr_reader :node, :domain, :resource
+ attr_writer :resource
+
+ def self.new(node, domain=nil, resource=nil)
+ node.is_a?(JID) ? node : super
+ end
+
+ def initialize(node, domain=nil, resource=nil)
+ @node, @domain, @resource = node, domain, resource
+
+ if @domain.nil? && @resource.nil?
+ @node, @domain, @resource = @node.to_s.scan(PATTERN).first
+ end
+ [@node, @domain].each {|part| part.downcase! if part }
+
+ validate
+ end
+
+ # Strip the resource part from this JID and return it as a new
+ # JID object. The new JID contains only the optional node part
+ # and the required domain part from the original. This JID remains
+ # unchanged.
+ def bare
+ JID.new(@node, @domain)
+ end
+
+ # Return true if this is a bare JID without a resource part.
+ def bare?
+ @resource.nil?
+ end
+
+ # Return true if this is a domain-only JID without a node or resource part.
+ def domain?
+ !empty? && to_s == @domain
+ end
+
+ # Return true if this JID is equal to the empty string ''. That is, it's
+ # missing the node, domain, and resource parts that form a valid JID. It
+ # makes for easier error handling to be able to create JID objects from
+ # strings and then check if they're empty rather than nil.
+ def empty?
+ to_s == ''
+ end
+
+ def <=>(jid)
+ self.to_s <=> jid.to_s
+ end
+
+ def eql?(jid)
+ jid.is_a?(JID) && self == jid
+ end
+
+ def hash
+ self.to_s.hash
+ end
+
+ def to_s
+ s = @domain
+ s = "#{@node}@#{s}" if @node
+ s = "#{s}/#{@resource}" if @resource
+ s
+ end
+
+ private
+
+ def validate
+ [@node, @domain, @resource].each do |part|
+ raise ArgumentError, 'jid too long' if (part || '').size > 1023
+ end
+ raise ArgumentError, 'empty node' if @node && @node.strip.empty?
+ raise ArgumentError, 'node contains invalid characters' if @node && @node =~ NODE_PREP
+ raise ArgumentError, 'empty resource' if @resource && @resource.strip.empty?
+ raise ArgumentError, 'resource contains invalid characters' if @resource && @resource =~ RESOURCE_PREP
+ raise ArgumentError, 'empty domain' if @domain == '' && (@node || @resource)
+ raise ArgumentError, 'domain contains invalid characters' if @domain && @domain =~ NAME_PREP
+ end
+ end
+end
diff --git a/lib/vines/kit.rb b/lib/vines/kit.rb
new file mode 100644
index 0000000..33b71c4
--- /dev/null
+++ b/lib/vines/kit.rb
@@ -0,0 +1,30 @@
+# encoding: UTF-8
+
+module Vines
+ # A module for utility methods with no better home.
+ module Kit
+ # Create a hex-encoded, SHA-512 HMAC of the data, using the secret key.
+ def self.hmac(key, data)
+ digest = OpenSSL::Digest.new("sha512")
+ OpenSSL::HMAC.hexdigest(digest, key, data)
+ end
+
+ # Generates a random uuid per rfc 4122 that's useful for including in
+ # stream, iq, and other xmpp stanzas.
+ def self.uuid
+ SecureRandom.uuid
+ end
+
+ # Generates a random 128 character authentication token.
+ def self.auth_token
+ SecureRandom.hex(64)
+ end
+
+ # Generate a HMAC for dialback as recommended in XEP-0185
+ def self.dialback_key(key, receiving, originating, id)
+ digest = OpenSSL::Digest.new('sha256')
+ data = "#{receiving} #{originating} #{id}"
+ OpenSSL::HMAC.hexdigest(digest, digest.hexdigest(key), data)
+ end
+ end
+end
diff --git a/lib/vines/log.rb b/lib/vines/log.rb
new file mode 100644
index 0000000..8041c29
--- /dev/null
+++ b/lib/vines/log.rb
@@ -0,0 +1,28 @@
+# encoding: UTF-8
+
+module Vines
+ module Log
+ @@logger = nil
+ def log
+ unless @@logger
+ @@logger = Logger.new(STDOUT)
+ @@logger.level = Logger::INFO
+ @@logger.progname = 'vines'
+ @@logger.formatter = Class.new(Logger::Formatter) do
+ def initialize
+ @time = "%Y-%m-%dT%H:%M:%SZ".freeze
+ @fmt = "[%s] %5s -- %s: %s\n".freeze
+ end
+ def call(severity, time, program, msg)
+ @fmt % [time.utc.strftime(@time), severity, program, msg2str(msg)]
+ end
+ end.new
+ end
+ @@logger
+ end
+
+ def self.set_log_file(file)
+ @@logger = Logger.new(file)
+ end
+ end
+end
diff --git a/lib/vines/node.rb b/lib/vines/node.rb
new file mode 100644
index 0000000..2cc4f3d
--- /dev/null
+++ b/lib/vines/node.rb
@@ -0,0 +1,31 @@
+module Vines
+ # Utility functions to work with nodes
+ module Node
+
+ STREAM = 'stream'.freeze
+ BODY = 'body'.freeze
+
+ module_function
+
+ # Check if node starts a new stream
+ def stream?(node)
+ node.name == STREAM && namespace(node) == NAMESPACES[:stream]
+ end
+
+ # Check if BOSH body
+ def body?(node)
+ node.name == BODY && namespace(node) == NAMESPACES[:http_bind]
+ end
+
+ # Get the namespace
+ def namespace(node)
+ namespace = node.namespace
+ namespace && namespace.href
+ end
+
+ # Convert to stanza
+ def to_stanza(node, stream)
+ Stanza.from_node(node, stream)
+ end
+ end
+end
diff --git a/lib/vines/router.rb b/lib/vines/router.rb
new file mode 100644
index 0000000..b8ab653
--- /dev/null
+++ b/lib/vines/router.rb
@@ -0,0 +1,184 @@
+# encoding: UTF-8
+
+module Vines
+ # The router tracks all stream connections to the server for all clients,
+ # servers, and components. It sends stanzas to the correct stream based on
+ # the 'to' attribute. Router is a singleton, shared by all streams, that must
+ # be accessed with +Config#router+.
+ class Router
+ EMPTY = [].freeze
+
+ STREAM_TYPES = [:client, :server, :component].freeze
+
+ def initialize(config)
+ @config = config
+ @clients, @servers, @components = {}, [], []
+ @pending = Hash.new {|h,k| h[k] = [] }
+ end
+
+ # Returns streams for all connected resources for this JID. A resource is
+ # considered connected after it has completed authentication and resource
+ # binding.
+ def connected_resources(jid, from, proxies=true)
+ jid, from = JID.new(jid), JID.new(from)
+ return [] unless @config.allowed?(jid, from)
+
+ local = @clients[jid.bare] || EMPTY
+ local = local.select {|stream| stream.user.jid == jid } unless jid.bare?
+ remote = proxies ? proxies(jid) : EMPTY
+ [local, remote].flatten
+ end
+
+ # Returns streams for all available resources for this JID. A resource is
+ # marked available after it sends initial presence.
+ def available_resources(*jids, from)
+ clients(jids, from) do |stream|
+ stream.available?
+ end
+ end
+
+ # Returns streams for all interested resources for this JID. A resource is
+ # marked interested after it requests the roster.
+ def interested_resources(*jids, from)
+ clients(jids, from) do |stream|
+ stream.interested?
+ end
+ end
+
+ # Add the connection to the routing table. The connection must return
+ # :client, :server, or :component from its +stream_type+ method so the
+ # router can properly route stanzas to the stream.
+ def <<(stream)
+ case stream_type(stream)
+ when :client then
+ return unless stream.connected?
+ jid = stream.user.jid.bare
+ @clients[jid] ||= []
+ @clients[jid] << stream
+ when :server then @servers << stream
+ when :component then @components << stream
+ end
+ end
+
+ # Remove the connection from the routing table.
+ def delete(stream)
+ case stream_type(stream)
+ when :client then
+ return unless stream.connected?
+ jid = stream.user.jid.bare
+ streams = @clients[jid] || []
+ streams.delete(stream)
+ @clients.delete(jid) if streams.empty?
+ when :server then @servers.delete(stream)
+ when :component then @components.delete(stream)
+ end
+ end
+
+ # Send the stanza to the appropriate remote server-to-server stream
+ # or an external component stream.
+ def route(stanza)
+ to, from = %w[to from].map {|attr| JID.new(stanza[attr]) }
+ return unless @config.allowed?(to, from)
+ key = [to.domain, from.domain]
+
+ if stream = connection_to(to, from)
+ stream.write(stanza)
+ elsif @pending.key?(key)
+ @pending[key] << stanza
+ elsif @config.s2s?(to.domain)
+ @pending[key] << stanza
+ Vines::Stream::Server.start(@config, to.domain, from.domain) do |stream|
+ stream ? send_pending(key, stream) : return_pending(key)
+ @pending.delete(key)
+ end
+ else
+ raise StanzaErrors::RemoteServerNotFound.new(stanza, 'cancel')
+ end
+ end
+
+ # Return stream by id
+ def stream_by_id(id)
+ (@servers+ at clients.values.flatten+@components).find {|stream| stream.id == id }
+ end
+
+ # Returns the total number of streams connected to the server.
+ def size
+ clients = @clients.values.inject(0) {|sum, arr| sum + arr.size }
+ clients + @servers.size + @components.size
+ end
+
+ private
+
+ # Write all pending stanzas for this domain to the stream. Called after a
+ # s2s stream has successfully connected and we need to dequeue all stanzas
+ # we received while waiting for the connection to finish.
+ def send_pending(key, stream)
+ @pending[key].each do |stanza|
+ stream.write(stanza)
+ end
+ end
+
+ # Return all pending stanzas to their senders as remote-server-not-found
+ # errors. Called after a s2s stream has failed to connect.
+ def return_pending(key)
+ @pending[key].each do |stanza|
+ to, from = JID.new(stanza['to']), JID.new(stanza['from'])
+ xml = StanzaErrors::RemoteServerNotFound.new(stanza, 'cancel').to_xml
+ if @config.component?(from)
+ connection_to(from, to).write(xml) rescue nil
+ else
+ connected_resources(from, to).each {|c| c.write(xml) }
+ end
+ end
+ end
+
+ # Return the client streams to which the from address is allowed to
+ # contact. Apply the filter block to each stream to narrow the results
+ # before returning the streams.
+ def clients(jids, from, &filter)
+ jids = filter_allowed(jids, from)
+ local = @clients.values_at(*jids).compact.flatten.select(&filter)
+ proxies = proxies(*jids).select(&filter)
+ [local, proxies].flatten
+ end
+
+ # Return the bare JIDs from the list that are allowed to talk to
+ # the +from+ JID.
+ def filter_allowed(jids, from)
+ from = JID.new(from)
+ jids.flatten.map {|jid| JID.new(jid).bare }
+ .select {|jid| @config.allowed?(jid, from) }
+ end
+
+ def proxies(*jids)
+ return EMPTY unless @config.cluster?
+ @config.cluster.remote_sessions(*jids)
+ end
+
+ def connection_to(to, from)
+ component_stream(to) || server_stream(to, from)
+ end
+
+ def component_stream(to)
+ @components.select do |stream|
+ stream.ready? && stream.remote_domain == to.domain
+ end.sample
+ end
+
+ def server_stream(to, from)
+ @servers.select do |stream|
+ stream.ready? &&
+ stream.remote_domain == to.domain &&
+ stream.domain == from.domain
+ end.sample
+ end
+
+ def stream_type(connection)
+ connection.stream_type.tap do |type|
+ unless STREAM_TYPES.include?(type)
+ raise ArgumentError, "unexpected stream type: #{type}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza.rb b/lib/vines/stanza.rb
new file mode 100644
index 0000000..bc426b8
--- /dev/null
+++ b/lib/vines/stanza.rb
@@ -0,0 +1,175 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ include Nokogiri::XML
+
+ attr_reader :stream
+
+ EMPTY = ''.freeze
+ FROM, MESSAGE, TO = %w[from message to].map {|s| s.freeze }
+ ROUTABLE_STANZAS = %w[message iq presence].freeze
+
+ @@types = {}
+
+ def self.register(xpath, ns={})
+ @@types[[xpath, ns]] = self
+ end
+
+ def self.from_node(node, stream)
+ # optimize common case
+ return Message.new(node, stream) if node.name == MESSAGE
+ found = @@types.select {|pair, v| node.xpath(*pair).any? }
+ .sort {|a, b| b[0][0].length - a[0][0].length }.first
+ found ? found[1].new(node, stream) : nil
+ end
+
+ def initialize(node, stream)
+ @node, @stream = node, stream
+ end
+
+ # Send the stanza to all recipients, stamping it with from and
+ # to addresses first.
+ def broadcast(recipients)
+ @node[FROM] = stream.user.jid.to_s
+ recipients.each do |recipient|
+ @node[TO] = recipient.user.jid.to_s
+ recipient.write(@node)
+ end
+ end
+
+ # Returns true if this stanza should be processed locally. Returns false
+ # if it's destined for a remote domain or external component.
+ def local?
+ return true unless ROUTABLE_STANZAS.include?(@node.name)
+ to = JID.new(@node[TO])
+ to.empty? || local_jid?(to)
+ end
+
+ def local_jid?(*jids)
+ stream.config.local_jid?(*jids)
+ end
+
+ # Return true if this stanza is addressed to a pubsub subdomain hosted
+ # at this server. This helps differentiate between IQ stanzas addressed
+ # to the server and stanzas addressed to pubsub domains, both of which must
+ # be handled locally and not routed.
+ def to_pubsub_domain?
+ stream.config.pubsub?(validate_to)
+ end
+
+ def route
+ stream.router.route(@node)
+ end
+
+ def router
+ stream.router
+ end
+
+ def storage(domain=stream.domain)
+ stream.storage(domain)
+ end
+
+ def process
+ raise 'subclass must implement'
+ end
+
+ # Broadcast unavailable presence from the user's available resources to the
+ # recipient's available resources. Route the stanza to a remote server if
+ # the recipient isn't hosted locally.
+ def send_unavailable(from, to)
+ available = router.available_resources(from, to)
+ stanzas = available.map {|stream| unavailable(stream.user.jid) }
+ broadcast_to_available_resources(stanzas, to)
+ end
+
+ # Return an unavailable presence stanza addressed from the given JID.
+ def unavailable(from)
+ doc = Document.new
+ doc.create_element('presence',
+ 'from' => from.to_s,
+ 'id' => Kit.uuid,
+ 'type' => 'unavailable')
+ end
+
+ # Return nil if this stanza has no 'to' attribute. Return a Vines::JID
+ # if it contains a valid 'to' attribute. Raise a JidMalformed error if
+ # the JID is invalid.
+ def validate_to
+ validate_address(TO)
+ end
+
+ # Return nil if this stanza has no 'from' attribute. Return a Vines::JID
+ # if it contains a valid 'from' attribute. Raise a JidMalformed error if
+ # the JID is invalid.
+ def validate_from
+ validate_address(FROM)
+ end
+
+ def method_missing(method, *args, &block)
+ @node.send(method, *args, &block)
+ end
+
+ private
+
+ # Send the stanzas to the destination JID, routing to a s2s stream
+ # if the address is remote. This method properly stamps the to address
+ # on each stanza before it's sent. The caller must set the from address.
+ def broadcast_to_available_resources(stanzas, to)
+ return if send_to_remote(stanzas, to)
+ send_to_recipients(stanzas, stream.available_resources(to))
+ end
+
+ # Send the stanzas to the destination JID, routing to a s2s stream
+ # if the address is remote. This method properly stamps the to address
+ # on each stanza before it's sent. The caller must set the from address.
+ def broadcast_to_interested_resources(stanzas, to)
+ return if send_to_remote(stanzas, to)
+ send_to_recipients(stanzas, stream.interested_resources(to))
+ end
+
+ # Route the stanzas to a remote server, stamping a bare JID as the
+ # to address. Bare JIDs are required for presence subscription stanzas
+ # sent to the remote contact's server. Return true if the stanzas were
+ # routed, false if they must be delivered locally.
+ def send_to_remote(stanzas, to)
+ return false if local_jid?(to)
+ to = JID.new(to)
+ stanzas.each do |el|
+ el[TO] = to.bare.to_s
+ router.route(el)
+ end
+ true
+ end
+
+ # Send the stanzas to the local recipient streams, stamping a full JID as
+ # the to address. It's important to use full JIDs, even when sending to
+ # local clients, because the stanzas may be routed to other cluster nodes
+ # for delivery. We need the receiving cluster node to send the stanza just
+ # to this full JID, not to lookup all JIDs for this user.
+ def send_to_recipients(stanzas, recipients)
+ recipients.each do |recipient|
+ stanzas.each do |el|
+ el[TO] = recipient.user.jid.to_s
+ recipient.write(el)
+ end
+ end
+ end
+
+ # Return true if the to and from JIDs are allowed to communicate with one
+ # another based on the cross_domain_messages setting in conf/config.rb. If
+ # a domain's users are isolated to sending messages only within their own
+ # domain, pubsub stanzas must not be processed from remote JIDs.
+ def allowed?
+ stream.config.allowed?(validate_to || stream.domain, stream.user.jid)
+ end
+
+ def validate_address(attr)
+ jid = (self[attr] || EMPTY)
+ return if jid.empty?
+ JID.new(jid)
+ rescue
+ raise StanzaErrors::JidMalformed.new(self, 'modify')
+ end
+ end
+end
diff --git a/lib/vines/stanza/dialback.rb b/lib/vines/stanza/dialback.rb
new file mode 100644
index 0000000..ff51ef9
--- /dev/null
+++ b/lib/vines/stanza/dialback.rb
@@ -0,0 +1,28 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class Dialback < Stanza
+ VALID_TYPE, INVALID_TYPE = %w[valid invalid].map {|t| t.freeze }
+ NS = NAMESPACES[:legacy_dialback]
+
+ register "/db:verify", 'db' => NS
+
+ def process
+ id, from, to = %w[id from to].map {|a| @node[a] }
+ key = @node.text
+
+ outbound_stream = router.stream_by_id(id)
+ unless outbound_stream && outbound_stream.state.is_a?(Stream::Server::Outbound::AuthDialbackResult)
+ @stream.write(%Q{<db:verify from="#{to}" to="#{from}" id="#{id}" type="error"><error type="cancel"><item-not-found xmlns="#{NAMESPACES[:stanzas]}"/></error></db:verify>})
+ return
+ end
+
+ secret = outbound_stream.state.dialback_secret
+ type = Kit.dialback_key(secret, from, to, id) == key ? VALID_TYPE : INVALID_TYPE
+ @stream.write(%Q{<db:verify from="#{to}" to="#{from}" id="#{id}" type="#{type}"/>})
+ @stream.close_connection_after_writing
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza/iq.rb b/lib/vines/stanza/iq.rb
new file mode 100644
index 0000000..3e4e21d
--- /dev/null
+++ b/lib/vines/stanza/iq.rb
@@ -0,0 +1,48 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class Iq < Stanza
+ register "/iq"
+
+ VALID_TYPES = %w[get set result error].freeze
+
+ VALID_TYPES.each do |type|
+ define_method "#{type}?" do
+ self['type'] == type
+ end
+ end
+
+ def process
+ if self['id'] && VALID_TYPES.include?(self['type'])
+ route_iq or raise StanzaErrors::FeatureNotImplemented.new(@node, 'cancel')
+ else
+ raise StanzaErrors::BadRequest.new(@node, 'modify')
+ end
+ end
+
+ def to_result
+ doc = Document.new
+ doc.create_element('iq',
+ 'from' => validate_to || stream.domain,
+ 'id' => self['id'],
+ 'to' => stream.user.jid,
+ 'type' => 'result')
+ end
+
+ private
+
+ # Return false if this IQ stanza is addressed to the server, or a pubsub
+ # service hosted here, and must be handled locally. Return true if the
+ # stanza must not be handled locally and has been routed to the appropriate
+ # component, s2s, or c2s stream.
+ def route_iq
+ to = validate_to
+ return false if to.nil? || stream.config.vhost?(to) || to_pubsub_domain?
+ self['from'] = stream.user.jid.to_s
+ local? ? broadcast(stream.connected_resources(to)) : route
+ true
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza/iq/auth.rb b/lib/vines/stanza/iq/auth.rb
new file mode 100644
index 0000000..d854bc1
--- /dev/null
+++ b/lib/vines/stanza/iq/auth.rb
@@ -0,0 +1,18 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class Iq
+ class Auth < Query
+ register "/iq[@id and @type='get']/ns:query", 'ns' => NAMESPACES[:non_sasl]
+
+ def process
+ # XEP-0078 says we MUST send a service-unavailable error
+ # here, but Adium 1.4.1 won't login if we do that, so just
+ # swallow this stanza.
+ # raise StanzaErrors::ServiceUnavailable.new(@node, 'cancel')
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza/iq/disco_info.rb b/lib/vines/stanza/iq/disco_info.rb
new file mode 100644
index 0000000..0073df6
--- /dev/null
+++ b/lib/vines/stanza/iq/disco_info.rb
@@ -0,0 +1,45 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class Iq
+ class DiscoInfo < Query
+ NS = NAMESPACES[:disco_info]
+
+ register "/iq[@id and @type='get']/ns:query", 'ns' => NS
+
+ def process
+ return if route_iq || !allowed?
+ result = to_result.tap do |el|
+ el << el.document.create_element('query') do |query|
+ query.default_namespace = NS
+ if to_pubsub_domain?
+ identity(query, 'pubsub', 'service')
+ pubsub = [:pubsub_create, :pubsub_delete, :pubsub_instant, :pubsub_item_ids, :pubsub_publish, :pubsub_subscribe]
+ features(query, :disco_info, :ping, :pubsub, *pubsub)
+ else
+ identity(query, 'server', 'im')
+ features = [:disco_info, :disco_items, :offline, :ping, :vcard, :version]
+ features << :storage if stream.config.private_storage?(validate_to || stream.domain)
+ features(query, features)
+ end
+ end
+ end
+ stream.write(result)
+ end
+
+ private
+
+ def identity(query, category, type)
+ query << query.document.create_element('identity', 'category' => category, 'type' => type)
+ end
+
+ def features(query, *features)
+ features.flatten.each do |feature|
+ query << query.document.create_element('feature', 'var' => NAMESPACES[feature])
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza/iq/disco_items.rb b/lib/vines/stanza/iq/disco_items.rb
new file mode 100644
index 0000000..8d95615
--- /dev/null
+++ b/lib/vines/stanza/iq/disco_items.rb
@@ -0,0 +1,29 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class Iq
+ class DiscoItems < Query
+ NS = NAMESPACES[:disco_items]
+
+ register "/iq[@id and @type='get']/ns:query", 'ns' => NS
+
+ def process
+ return if route_iq || !allowed?
+ result = to_result.tap do |el|
+ el << el.document.create_element('query') do |query|
+ query.default_namespace = NS
+ unless to_pubsub_domain?
+ to = (validate_to || stream.domain).to_s
+ stream.config.vhost(to).disco_items.each do |domain|
+ query << el.document.create_element('item', 'jid' => domain)
+ end
+ end
+ end
+ end
+ stream.write(result)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza/iq/error.rb b/lib/vines/stanza/iq/error.rb
new file mode 100644
index 0000000..8530c5c
--- /dev/null
+++ b/lib/vines/stanza/iq/error.rb
@@ -0,0 +1,16 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class Iq
+ class Error < Iq
+ register "/iq[@id and @type='error']"
+
+ def process
+ return if route_iq
+ # do nothing
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza/iq/ping.rb b/lib/vines/stanza/iq/ping.rb
new file mode 100644
index 0000000..6584617
--- /dev/null
+++ b/lib/vines/stanza/iq/ping.rb
@@ -0,0 +1,16 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class Iq
+ class Ping < Iq
+ register "/iq[@id and @type='get']/ns:ping", 'ns' => NAMESPACES[:ping]
+
+ def process
+ return if route_iq || !allowed?
+ stream.write(to_result)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza/iq/private_storage.rb b/lib/vines/stanza/iq/private_storage.rb
new file mode 100644
index 0000000..6a2aadb
--- /dev/null
+++ b/lib/vines/stanza/iq/private_storage.rb
@@ -0,0 +1,83 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class Iq
+ # Implements the Private Storage feature defined in XEP-0049. Clients are
+ # allowed to save arbitrary XML documents on the server, identified by
+ # element name and namespace.
+ class PrivateStorage < Query
+ NS = NAMESPACES[:storage]
+
+ register "/iq[@id and (@type='get' or @type='set')]/ns:query", 'ns' => NS
+
+ def process
+ validate_to_address
+ validate_storage_enabled
+ validate_children_size
+ validate_namespaces
+ get? ? retrieve_fragment : update_fragment
+ end
+
+ private
+
+ def retrieve_fragment
+ found = storage.find_fragment(stream.user.jid, elements.first.elements.first)
+ raise StanzaErrors::ItemNotFound.new(self, 'cancel') unless found
+
+ result = to_result do |node|
+ node << node.document.create_element('query') do |query|
+ query.default_namespace = NS
+ query << found
+ end
+ end
+ stream.write(result)
+ end
+
+ def update_fragment
+ elements.first.elements.each do |node|
+ storage.save_fragment(stream.user.jid, node)
+ end
+ stream.write(to_result)
+ end
+
+ private
+
+ def to_result
+ super.tap do |node|
+ node['from'] = stream.user.jid.to_s
+ yield node if block_given?
+ end
+ end
+
+ def validate_children_size
+ size = elements.first.elements.size
+ if (get? && size != 1) || (set? && size == 0)
+ raise StanzaErrors::NotAcceptable.new(self, 'modify')
+ end
+ end
+
+ def validate_to_address
+ to = validate_to
+ unless to.nil? || to == stream.user.jid.bare
+ raise StanzaErrors::Forbidden.new(self, 'cancel')
+ end
+ end
+
+ def validate_storage_enabled
+ unless stream.config.private_storage?(stream.domain)
+ raise StanzaErrors::ServiceUnavailable.new(self, 'cancel')
+ end
+ end
+
+ def validate_namespaces
+ elements.first.elements.each do |node|
+ if node.namespace.nil? || NAMESPACES.values.include?(node.namespace.href)
+ raise StanzaErrors::NotAcceptable.new(self, 'modify')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza/iq/query.rb b/lib/vines/stanza/iq/query.rb
new file mode 100644
index 0000000..d741129
--- /dev/null
+++ b/lib/vines/stanza/iq/query.rb
@@ -0,0 +1,10 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class Iq
+ class Query < Iq
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza/iq/result.rb b/lib/vines/stanza/iq/result.rb
new file mode 100644
index 0000000..341c594
--- /dev/null
+++ b/lib/vines/stanza/iq/result.rb
@@ -0,0 +1,16 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class Iq
+ class Result < Iq
+ register "/iq[@id and @type='result']"
+
+ def process
+ return if route_iq
+ # do nothing
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza/iq/roster.rb b/lib/vines/stanza/iq/roster.rb
new file mode 100644
index 0000000..21eae66
--- /dev/null
+++ b/lib/vines/stanza/iq/roster.rb
@@ -0,0 +1,140 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class Iq
+ class Roster < Query
+ NS = NAMESPACES[:roster]
+
+ register "/iq[@id and (@type='get' or @type='set')]/ns:query", 'ns' => NS
+
+ def process
+ validate_to_address
+ get? ? roster_query : update_roster
+ end
+
+ private
+
+ # Send an iq result stanza containing roster items to the user in
+ # response to their roster get request. Requesting the roster makes
+ # this stream an "interested resource" that can now receive roster
+ # updates.
+ def roster_query
+ stream.requested_roster!
+ stream.write(stream.user.to_roster_xml(self['id']))
+ end
+
+ # Roster sets must have no 'to' address or be addressed to the same
+ # JID that sent the stanza. RFC 6121 sections 2.1.5 and 2.3.3.
+ def validate_to_address
+ to = validate_to
+ unless to.nil? || to.bare == stream.user.jid.bare
+ raise StanzaErrors::Forbidden.new(self, 'auth')
+ end
+ end
+
+ # Add, update, or delete the roster item contained in the iq set
+ # stanza received from the client. RFC 6121 sections 2.3, 2.4, 2.5.
+ def update_roster
+ items = self.xpath('ns:query/ns:item', 'ns' => NS)
+ raise StanzaErrors::BadRequest.new(self, 'modify') if items.size != 1
+ item = items.first
+
+ jid = JID.new(item['jid']) rescue (raise StanzaErrors::JidMalformed.new(self, 'modify'))
+ raise StanzaErrors::BadRequest.new(self, 'modify') if jid.empty? || !jid.bare?
+
+ if item['subscription'] == 'remove'
+ remove_contact(jid)
+ return
+ end
+
+ raise StanzaErrors::NotAllowed.new(self, 'modify') if jid == stream.user.jid.bare
+ groups = item.xpath('ns:group', 'ns' => NS).map {|g| g.text.strip }
+ raise StanzaErrors::BadRequest.new(self, 'modify') if groups.uniq!
+ raise StanzaErrors::NotAcceptable.new(self, 'modify') if groups.include?('')
+
+ contact = stream.user.contact(jid)
+ unless contact
+ contact = Contact.new(jid: jid)
+ stream.user.roster << contact
+ end
+ contact.name = item['name']
+ contact.groups = groups
+ storage.save_user(stream.user)
+ stream.update_user_streams(stream.user)
+ send_result_iq
+ push_roster_updates(stream.user.jid, contact)
+ end
+
+ # Remove the contact with this JID from the user's roster and send
+ # roster pushes to the user's interested resources. This is triggered
+ # by receiving an iq set with an item element like
+ # <item jid="alice at wonderland.lit" subscription="remove"/>. RFC 6121
+ # section 2.5.
+ def remove_contact(jid)
+ contact = stream.user.contact(jid)
+ raise StanzaErrors::ItemNotFound.new(self, 'modify') unless contact
+ if local_jid?(contact.jid)
+ user = storage(contact.jid.domain).find_user(contact.jid)
+ end
+
+ if user && user.contact(stream.user.jid)
+ user.contact(stream.user.jid).subscription = 'none'
+ user.contact(stream.user.jid).ask = nil
+ end
+ stream.user.remove_contact(contact.jid)
+ [user, stream.user].compact.each do |save|
+ storage(save.jid.domain).save_user(save)
+ stream.update_user_streams(save)
+ end
+
+ send_result_iq
+ push_roster_updates(stream.user.jid,
+ Contact.new(jid: contact.jid, subscription: 'remove'))
+
+ if local_jid?(contact.jid)
+ send_unavailable(stream.user.jid, contact.jid) if contact.subscribed_from?
+ send_unsubscribe(contact)
+ if user && user.contact(stream.user.jid)
+ push_roster_updates(contact.jid, user.contact(stream.user.jid))
+ end
+ else
+ send_unsubscribe(contact)
+ end
+ end
+
+ # Notify the contact that it's been removed from the user's roster
+ # and no longer has any presence relationship with the user.
+ def send_unsubscribe(contact)
+ presence = [%w[to unsubscribe], %w[from unsubscribed]].map do |meth, type|
+ presence(contact.jid, type) if contact.send("subscribed_#{meth}?")
+ end.compact
+ broadcast_to_interested_resources(presence, contact.jid)
+ end
+
+ def presence(to, type)
+ doc = Document.new
+ doc.create_element('presence',
+ 'from' => stream.user.jid.bare.to_s,
+ 'id' => Kit.uuid,
+ 'to' => to.to_s,
+ 'type' => type)
+ end
+
+ # Send an iq set stanza to the user's interested resources, letting them
+ # know their roster has been updated.
+ def push_roster_updates(to, contact)
+ stream.interested_resources(to).each do |recipient|
+ contact.send_roster_push(recipient)
+ end
+ end
+
+ def send_result_iq
+ node = to_result
+ node.remove_attribute('from')
+ stream.write(node)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza/iq/session.rb b/lib/vines/stanza/iq/session.rb
new file mode 100644
index 0000000..b51fc04
--- /dev/null
+++ b/lib/vines/stanza/iq/session.rb
@@ -0,0 +1,17 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class Iq
+ # Session support is deprecated, but Adium requires it, so reply with an
+ # iq result stanza.
+ class Session < Iq
+ register "/iq[@id and @type='set']/ns:session", 'ns' => NAMESPACES[:session]
+
+ def process
+ stream.write(to_result)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza/iq/vcard.rb b/lib/vines/stanza/iq/vcard.rb
new file mode 100644
index 0000000..11c558c
--- /dev/null
+++ b/lib/vines/stanza/iq/vcard.rb
@@ -0,0 +1,56 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class Iq
+ class Vcard < Iq
+ NS = NAMESPACES[:vcard]
+
+ register "/iq[@id and @type='get' or @type='set']/ns:vCard", 'ns' => NS
+
+ def process
+ return unless allowed?
+ if local?
+ get? ? vcard_query : vcard_update
+ else
+ self['from'] = stream.user.jid.to_s
+ route
+ end
+ end
+
+ private
+
+ def vcard_query
+ to = validate_to
+ jid = to ? to.bare : stream.user.jid.bare
+ card = storage(jid.domain).find_vcard(jid)
+
+ raise StanzaErrors::ItemNotFound.new(self, 'cancel') unless card
+
+ doc = Document.new
+ result = doc.create_element('iq') do |node|
+ node['from'] = jid.to_s unless jid == stream.user.jid.bare
+ node['id'] = self['id']
+ node['to'] = stream.user.jid.to_s
+ node['type'] = 'result'
+ node << card
+ end
+ stream.write(result)
+ end
+
+ def vcard_update
+ to = validate_to
+ unless to.nil? || to == stream.user.jid.bare
+ raise StanzaErrors::Forbidden.new(self, 'auth')
+ end
+
+ storage.save_vcard(stream.user.jid, elements.first)
+
+ result = to_result
+ result.remove_attribute('from')
+ stream.write(result)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza/iq/version.rb b/lib/vines/stanza/iq/version.rb
new file mode 100644
index 0000000..1da59f9
--- /dev/null
+++ b/lib/vines/stanza/iq/version.rb
@@ -0,0 +1,25 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class Iq
+ class Version < Query
+ NS = NAMESPACES[:version]
+
+ register "/iq[@id and @type='get']/ns:query", 'ns' => NS
+
+ def process
+ return if route_iq || to_pubsub_domain? || !allowed?
+ result = to_result.tap do |node|
+ node << node.document.create_element('query') do |query|
+ query.default_namespace = NS
+ query << node.document.create_element('name', 'Vines')
+ query << node.document.create_element('version', VERSION)
+ end
+ end
+ stream.write(result)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza/message.rb b/lib/vines/stanza/message.rb
new file mode 100644
index 0000000..97dced9
--- /dev/null
+++ b/lib/vines/stanza/message.rb
@@ -0,0 +1,43 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class Message < Stanza
+ register "/message"
+
+ TYPE, FROM = %w[type from].map {|s| s.freeze }
+ VALID_TYPES = %w[chat error groupchat headline normal].freeze
+
+ VALID_TYPES.each do |type|
+ define_method "#{type}?" do
+ self[TYPE] == type
+ end
+ end
+
+ def process
+ unless self[TYPE].nil? || VALID_TYPES.include?(self[TYPE])
+ raise StanzaErrors::BadRequest.new(self, 'modify')
+ end
+
+ if local?
+ to = validate_to || stream.user.jid.bare
+ recipients = stream.connected_resources(to)
+ if recipients.empty?
+ if user = storage(to.domain).find_user(to)
+ if Config.instance.max_offline_msgs > 0 && self[TYPE].match(/(chat|normal)/i)
+ storage(to.domain).save_message(stream.user.jid.bare.to_s, to.to_s, @node.text)
+ else
+ raise StanzaErrors::ServiceUnavailable.new(self, 'cancel')
+ end
+ end
+ else
+ broadcast(recipients)
+ end
+ else
+ self[FROM] = stream.user.jid.to_s
+ route
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza/presence.rb b/lib/vines/stanza/presence.rb
new file mode 100644
index 0000000..db68f10
--- /dev/null
+++ b/lib/vines/stanza/presence.rb
@@ -0,0 +1,182 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class Presence < Stanza
+ register "/presence"
+
+ VALID_TYPES = %w[subscribe subscribed unsubscribe unsubscribed unavailable probe error].freeze
+
+ VALID_TYPES.each do |type|
+ define_method "#{type}?" do
+ self['type'] == type
+ end
+ end
+
+ def process
+ stream.last_broadcast_presence = @node.clone unless validate_to
+ unless self['type'].nil?
+ raise StanzaErrors::BadRequest.new(self, 'modify')
+ end
+ if Config.instance.max_offline_msgs > 0 && !validate_to
+ check_offline_messages(stream.last_broadcast_presence)
+ end
+ dir = outbound? ? 'outbound' : 'inbound'
+ method("#{dir}_broadcast_presence").call
+ end
+
+ def check_offline_messages(presence)
+ priority = presence.xpath("//priority").text.to_i rescue nil
+ if priority != nil && priority >= 0
+ jid = stream.user.jid.to_s
+ storage.find_messages(jid).each do |id, m|
+ stamp = Time.parse(m[:created_at].to_s)
+ doc = Nokogiri::XML::Builder.new
+ doc.message(:type => "chat", :from => m[:from], :to => m[:to]) do |msg|
+ msg.send(:"body", m[:message])
+ msg.send(:"delay", "Offline Storage",
+ :xmlns => NAMESPACES[:delay],
+ :from => m[:from],
+ :stamp => stamp.iso8601)
+ end
+ xml = doc.to_xml :save_with => Nokogiri::XML::Node::SaveOptions::NO_DECLARATION
+ stream.write(xml)
+ # after delivering it we should
+ # delete the message from database
+ storage.destroy_message(id)
+ end
+ end
+ end
+
+ def outbound?
+ !inbound?
+ end
+
+ def inbound?
+ stream.class == Vines::Stream::Server ||
+ stream.class == Vines::Stream::Component
+ end
+
+ def outbound_broadcast_presence
+ self['from'] = stream.user.jid.to_s
+ to = validate_to
+ type = (self['type'] || '').strip
+ initial = to.nil? && type.empty? && !stream.available?
+
+ recipients = if to.nil?
+ stream.available_subscribers
+ else
+ stream.user.subscribed_from?(to) ? stream.available_resources(to) : []
+ end
+
+ # NOTE overriding vCard information is not concurring
+ # with XEP-153 due the fact that the user can only update
+ # his vCard via the Diaspora environment we should act
+ # the same way for the avatar update
+ override_vcard_update
+
+ broadcast(recipients)
+ broadcast(stream.available_resources(stream.user.jid))
+
+ if initial
+ stream.available_subscribed_to_resources.each do |recipient|
+ if recipient.last_broadcast_presence
+ el = recipient.last_broadcast_presence.clone
+ el['to'] = stream.user.jid.to_s
+ el['from'] = recipient.user.jid.to_s
+ stream.write(el)
+ end
+ end
+ stream.remote_subscribed_to_contacts.each do |contact|
+ send_probe(contact.jid.bare)
+ end
+ stream.available!
+ end
+
+ stream.remote_subscribers(to).each do |contact|
+ node = @node.clone
+ node['to'] = contact.jid.bare.to_s
+ router.route(node) rescue nil # ignore RemoteServerNotFound
+ end
+ end
+
+ def inbound_broadcast_presence
+ broadcast(stream.available_resources(validate_to))
+ end
+
+ private
+
+ def send_probe(to)
+ to = JID.new(to)
+ doc = Document.new
+ probe = doc.create_element('presence',
+ 'from' => stream.user.jid.bare.to_s,
+ 'id' => Kit.uuid,
+ 'to' => to.bare.to_s,
+ 'type' => 'probe')
+ router.route(probe) rescue nil # ignore RemoteServerNotFound
+ end
+
+ def auto_reply_to_subscription_request(from, type)
+ doc = Document.new
+ node = doc.create_element('presence') do |el|
+ el['from'] = from.to_s
+ el['id'] = self['id'] if self['id']
+ el['to'] = stream.user.jid.bare.to_s
+ el['type'] = type
+ end
+ stream.write(node)
+ end
+
+ # Send the contact's roster item to the current user's interested streams.
+ # Roster pushes are required, following presence subscription updates, to
+ # notify the user's clients of the contact's current state.
+ def send_roster_push(to)
+ contact = stream.user.contact(to)
+ stream.interested_resources(stream.user.jid).each do |recipient|
+ contact.send_roster_push(recipient)
+ end
+ end
+
+ # Notify the current user's interested streams of a contact's subscription
+ # state change as a result of receiving a subscribed, unsubscribe, or
+ # unsubscribed presence stanza.
+ def broadcast_subscription_change(contact)
+ stamp_from
+ stream.interested_resources(stamp_to).each do |recipient|
+ @node['to'] = recipient.user.jid.to_s
+ recipient.write(@node)
+ contact.send_roster_push(recipient)
+ end
+ end
+
+ # Validate that the incoming stanza has a 'to' attribute and strip any
+ # resource part from it so it's a bare jid. Return the bare JID object
+ # that was stamped.
+ def stamp_to
+ to = validate_to
+ raise StanzaErrors::BadRequest.new(self, 'modify') unless to
+ to.bare.tap do |bare|
+ self['to'] = bare.to_s
+ end
+ end
+
+ # Presence subscription stanzas must be addressed from the user's bare
+ # JID. Return the user's bare JID object that was stamped.
+ def stamp_from
+ stream.user.jid.bare.tap do |bare|
+ self['from'] = bare.to_s
+ end
+ end
+
+ def override_vcard_update
+ image_path = storage.find_avatar_by_jid(@node['from'])
+ return if image_path.nil?
+ photo_tag = "<photo><EXTVAL>#{image_path}</EXTVAL></photo>"
+ node = @node.xpath("//xmlns:x", 'xmlns' => NAMESPACES[:vcard_update]).first
+ node.remove unless node.blank?
+ @node << "<x xmlns=\"#{NAMESPACES[:vcard_update]}\">#{photo_tag}</x>"
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza/presence/error.rb b/lib/vines/stanza/presence/error.rb
new file mode 100644
index 0000000..7df6d64
--- /dev/null
+++ b/lib/vines/stanza/presence/error.rb
@@ -0,0 +1,23 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class Presence
+ class Error < Presence
+ register "/presence[@type='error']"
+
+ def process
+ inbound? ? process_inbound : process_outbound
+ end
+
+ def process_outbound
+ # FIXME Implement error handling
+ end
+
+ def process_inbound
+ # FIXME Implement error handling
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza/presence/probe.rb b/lib/vines/stanza/presence/probe.rb
new file mode 100644
index 0000000..ea92d48
--- /dev/null
+++ b/lib/vines/stanza/presence/probe.rb
@@ -0,0 +1,37 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class Presence
+ class Probe < Presence
+ register "/presence[@type='probe']"
+
+ def process
+ inbound? ? process_inbound : process_outbound
+ end
+
+ def process_outbound
+ self['from'] = stream.user.jid.to_s
+ local? ? process_inbound : route
+ end
+
+ def process_inbound
+ to = validate_to
+ raise StanzaErrors::BadRequest.new(self, 'modify') unless to
+
+ user = storage(to.domain).find_user(to)
+ unless user && user.subscribed_from?(stream.user.jid)
+ auto_reply_to_subscription_request(to.bare, 'unsubscribed')
+ else
+ stream.available_resources(to).each do |recipient|
+ el = recipient.last_broadcast_presence.clone
+ el['from'] = recipient.user.jid.to_s
+ el['to'] = stream.user.jid.to_s
+ stream.write(el)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza/presence/subscribe.rb b/lib/vines/stanza/presence/subscribe.rb
new file mode 100644
index 0000000..524aab5
--- /dev/null
+++ b/lib/vines/stanza/presence/subscribe.rb
@@ -0,0 +1,42 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class Presence
+ class Subscribe < Presence
+ register "/presence[@type='subscribe']"
+
+ def process
+ stamp_from
+ inbound? ? process_inbound : process_outbound
+ end
+
+ def process_outbound
+ to = stamp_to
+ stream.user.request_subscription(to)
+ storage.save_user(stream.user)
+ stream.update_user_streams(stream.user)
+ local? ? process_inbound : route
+ send_roster_push(to)
+ end
+
+ def process_inbound
+ to = stamp_to
+ contact = storage(to.domain).find_user(to)
+ if contact.nil?
+ auto_reply_to_subscription_request(to, 'unsubscribed')
+ elsif contact.subscribed_from?(stream.user.jid)
+ auto_reply_to_subscription_request(to, 'subscribed')
+ else
+ recipients = stream.available_resources(to)
+ if recipients.empty?
+ # TODO store subscription request per RFC 6121 3.1.3 #4
+ else
+ broadcast_to_available_resources([@node], to)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza/presence/subscribed.rb b/lib/vines/stanza/presence/subscribed.rb
new file mode 100644
index 0000000..bc9768d
--- /dev/null
+++ b/lib/vines/stanza/presence/subscribed.rb
@@ -0,0 +1,51 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class Presence
+ class Subscribed < Presence
+ register "/presence[@type='subscribed']"
+
+ def process
+ stamp_from
+ inbound? ? process_inbound : process_outbound
+ end
+
+ def process_outbound
+ to = stamp_to
+ stream.user.add_subscription_from(to)
+ storage.save_user(stream.user)
+ stream.update_user_streams(stream.user)
+ local? ? process_inbound : route
+ send_roster_push(to)
+ send_known_presence(to)
+ end
+
+ def process_inbound
+ to = stamp_to
+ user = storage(to.domain).find_user(to)
+ contact = user.contact(stream.user.jid) if user
+ return unless contact && contact.can_subscribe?
+ contact.subscribe_to
+ storage(to.domain).save_user(user)
+ stream.update_user_streams(user)
+ broadcast_subscription_change(contact)
+ end
+
+ private
+
+ # After approving a contact's subscription to this user's presence,
+ # broadcast this user's most recent presence stanzas to the contact.
+ def send_known_presence(to)
+ stanzas = stream.available_resources(stream.user.jid).map do |stream|
+ stream.last_broadcast_presence.clone.tap do |node|
+ node['from'] = stream.user.jid.to_s
+ node['id'] = Kit.uuid
+ end
+ end
+ broadcast_to_available_resources(stanzas, to)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza/presence/unavailable.rb b/lib/vines/stanza/presence/unavailable.rb
new file mode 100644
index 0000000..c0b119a
--- /dev/null
+++ b/lib/vines/stanza/presence/unavailable.rb
@@ -0,0 +1,15 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class Presence
+ class Unavailable < Presence
+ register "/presence[@type='unavailable']"
+
+ def process
+ inbound? ? inbound_broadcast_presence : outbound_broadcast_presence
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza/presence/unsubscribe.rb b/lib/vines/stanza/presence/unsubscribe.rb
new file mode 100644
index 0000000..b06d3f6
--- /dev/null
+++ b/lib/vines/stanza/presence/unsubscribe.rb
@@ -0,0 +1,38 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class Presence
+ class Unsubscribe < Presence
+ register "/presence[@type='unsubscribe']"
+
+ def process
+ stamp_from
+ inbound? ? process_inbound : process_outbound
+ end
+
+ def process_outbound
+ to = stamp_to
+ return unless stream.user.subscribed_to?(to)
+ stream.user.remove_subscription_to(to)
+ storage.save_user(stream.user)
+ stream.update_user_streams(stream.user)
+ local? ? process_inbound : route
+ send_roster_push(to)
+ end
+
+ def process_inbound
+ to = stamp_to
+ user = storage(to.domain).find_user(to)
+ return unless user && user.subscribed_from?(stream.user.jid)
+ contact = user.contact(stream.user.jid)
+ contact.unsubscribe_from
+ storage(to.domain).save_user(user)
+ stream.update_user_streams(user)
+ broadcast_subscription_change(contact)
+ send_unavailable(to, stream.user.jid.bare)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza/presence/unsubscribed.rb b/lib/vines/stanza/presence/unsubscribed.rb
new file mode 100644
index 0000000..94be9a9
--- /dev/null
+++ b/lib/vines/stanza/presence/unsubscribed.rb
@@ -0,0 +1,38 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class Presence
+ class Unsubscribed < Presence
+ register "/presence[@type='unsubscribed']"
+
+ def process
+ stamp_from
+ inbound? ? process_inbound : process_outbound
+ end
+
+ def process_outbound
+ to = stamp_to
+ return unless stream.user.subscribed_from?(to)
+ send_unavailable(stream.user.jid, to)
+ stream.user.remove_subscription_from(to)
+ storage.save_user(stream.user)
+ stream.update_user_streams(stream.user)
+ local? ? process_inbound : route
+ send_roster_push(to)
+ end
+
+ def process_inbound
+ to = stamp_to
+ user = storage(to.domain).find_user(to)
+ return unless user && user.subscribed_to?(stream.user.jid)
+ contact = user.contact(stream.user.jid)
+ contact.unsubscribe_to
+ storage(to.domain).save_user(user)
+ stream.update_user_streams(user)
+ broadcast_subscription_change(contact)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza/pubsub.rb b/lib/vines/stanza/pubsub.rb
new file mode 100644
index 0000000..b9ff731
--- /dev/null
+++ b/lib/vines/stanza/pubsub.rb
@@ -0,0 +1,22 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class PubSub < Iq
+
+ private
+
+ # Return the Config::PubSub system for the domain to which this stanza is
+ # addressed or nil if it's not to a pubsub subdomain.
+ def pubsub
+ stream.config.pubsub(validate_to)
+ end
+
+ # Raise feature-not-implemented if this stanza is addressed to the chat
+ # server itself, rather than a pubsub subdomain.
+ def validate_to_address
+ raise StanzaErrors::FeatureNotImplemented.new(self, 'cancel') unless pubsub
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza/pubsub/create.rb b/lib/vines/stanza/pubsub/create.rb
new file mode 100644
index 0000000..2e43ea1
--- /dev/null
+++ b/lib/vines/stanza/pubsub/create.rb
@@ -0,0 +1,39 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class PubSub
+ class Create < PubSub
+ NS = NAMESPACES[:pubsub]
+
+ register "/iq[@id and @type='set']/ns:pubsub/ns:create", 'ns' => NS
+
+ def process
+ return if route_iq || !allowed?
+ validate_to_address
+
+ node = self.xpath('ns:pubsub/ns:create', 'ns' => NS)
+ raise StanzaErrors::BadRequest.new(self, 'modify') if node.size != 1
+ node = node.first
+
+ id = (node['node'] || '').strip
+ id = Kit.uuid if id.empty?
+ raise StanzaErrors::Conflict.new(self, 'cancel') if pubsub.node?(id)
+ pubsub.add_node(id)
+ send_result_iq(id)
+ end
+
+ private
+
+ def send_result_iq(id)
+ el = to_result
+ el << el.document.create_element('pubsub') do |node|
+ node.default_namespace = NS
+ node << el.document.create_element('create', 'node' => id)
+ end
+ stream.write(el)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza/pubsub/delete.rb b/lib/vines/stanza/pubsub/delete.rb
new file mode 100644
index 0000000..9980b15
--- /dev/null
+++ b/lib/vines/stanza/pubsub/delete.rb
@@ -0,0 +1,41 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class PubSub
+ class Delete < PubSub
+ NS = NAMESPACES[:pubsub]
+
+ register "/iq[@id and @type='set']/ns:pubsub/ns:delete", 'ns' => NS
+
+ def process
+ return if route_iq || !allowed?
+ validate_to_address
+
+ node = self.xpath('ns:pubsub/ns:delete', 'ns' => NS)
+ raise StanzaErrors::BadRequest.new(self, 'modify') if node.size != 1
+ node = node.first
+
+ id = node['node']
+ raise StanzaErrors::ItemNotFound.new(self, 'cancel') unless pubsub.node?(id)
+
+ pubsub.publish(id, message(id))
+ pubsub.delete_node(id)
+ stream.write(to_result)
+ end
+
+ private
+
+ def message(id)
+ doc = Document.new
+ doc.create_element('message') do |node|
+ node << node.document.create_element('event') do |event|
+ event.default_namespace = NAMESPACES[:pubsub_event]
+ event << node.document.create_element('delete', 'node' => id)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza/pubsub/publish.rb b/lib/vines/stanza/pubsub/publish.rb
new file mode 100644
index 0000000..be97339
--- /dev/null
+++ b/lib/vines/stanza/pubsub/publish.rb
@@ -0,0 +1,66 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class PubSub
+ class Publish < PubSub
+ NS = NAMESPACES[:pubsub]
+
+ register "/iq[@id and @type='set']/ns:pubsub/ns:publish", 'ns' => NS
+
+ def process
+ return if route_iq || !allowed?
+ validate_to_address
+
+ node = self.xpath('ns:pubsub/ns:publish', 'ns' => NS)
+ raise StanzaErrors::BadRequest.new(self, 'modify') if node.size != 1
+ node = node.first
+ id = node['node']
+
+ raise StanzaErrors::ItemNotFound.new(self, 'cancel') unless pubsub.node?(id)
+
+ item = node.xpath('ns:item', 'ns' => NS)
+ raise StanzaErrors::BadRequest.new(self, 'modify') unless item.size == 1
+ item = item.first
+ unless item['id']
+ item['id'] = Kit.uuid
+ include_item = true
+ end
+
+ raise StanzaErrors::BadRequest.new(self, 'modify') unless item.elements.size == 1
+ pubsub.publish(id, message(id, item))
+ send_result_iq(id, include_item ? item : nil)
+ end
+
+ private
+
+ def message(node, item)
+ doc = Document.new
+ doc.create_element('message') do |message|
+ message << doc.create_element('event') do |event|
+ event.default_namespace = NAMESPACES[:pubsub_event]
+ event << doc.create_element('items', 'node' => node) do |items|
+ items << doc.create_element('item', 'id' => item['id'], 'publisher' => stream.user.jid.to_s) do |el|
+ el << item.elements.first
+ end
+ end
+ end
+ end
+ end
+
+ def send_result_iq(node, item)
+ result = to_result
+ if item
+ result << result.document.create_element('pubsub') do |pubsub|
+ pubsub.default_namespace = NS
+ pubsub << result.document.create_element('publish', 'node' => node) do |publish|
+ publish << result.document.create_element('item', 'id' => item['id'])
+ end
+ end
+ end
+ stream.write(result)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza/pubsub/subscribe.rb b/lib/vines/stanza/pubsub/subscribe.rb
new file mode 100644
index 0000000..24934e8
--- /dev/null
+++ b/lib/vines/stanza/pubsub/subscribe.rb
@@ -0,0 +1,44 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class PubSub
+ class Subscribe < PubSub
+ NS = NAMESPACES[:pubsub]
+
+ register "/iq[@id and @type='set']/ns:pubsub/ns:subscribe", 'ns' => NS
+
+ def process
+ return if route_iq || !allowed?
+ validate_to_address
+
+ node = self.xpath('ns:pubsub/ns:subscribe', 'ns' => NS)
+ raise StanzaErrors::BadRequest.new(self, 'modify') if node.size != 1
+ node = node.first
+ id, jid = node['node'], JID.new(node['jid'])
+
+ raise StanzaErrors::BadRequest.new(self, 'modify') unless stream.user.jid.bare == jid.bare
+ raise StanzaErrors::ItemNotFound.new(self, 'cancel') unless pubsub.node?(id)
+ raise StanzaErrors::PolicyViolation.new(self, 'wait') if pubsub.subscribed?(id, jid)
+
+ pubsub.subscribe(id, jid)
+ send_result_iq(id, jid)
+ end
+
+ private
+
+ def send_result_iq(id, jid)
+ result = to_result
+ result << result.document.create_element('pubsub') do |node|
+ node.default_namespace = NS
+ node << result.document.create_element('subscription',
+ 'node' => id,
+ 'jid' => jid.to_s,
+ 'subscription' => 'subscribed')
+ end
+ stream.write(result)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stanza/pubsub/unsubscribe.rb b/lib/vines/stanza/pubsub/unsubscribe.rb
new file mode 100644
index 0000000..bda5887
--- /dev/null
+++ b/lib/vines/stanza/pubsub/unsubscribe.rb
@@ -0,0 +1,30 @@
+# encoding: UTF-8
+
+module Vines
+ class Stanza
+ class PubSub
+ class Unsubscribe < PubSub
+ NS = NAMESPACES[:pubsub]
+
+ register "/iq[@id and @type='set']/ns:pubsub/ns:unsubscribe", 'ns' => NS
+
+ def process
+ return if route_iq || !allowed?
+ validate_to_address
+
+ node = self.xpath('ns:pubsub/ns:unsubscribe', 'ns' => NS)
+ raise StanzaErrors::BadRequest.new(self, 'modify') if node.size != 1
+ node = node.first
+ id, jid = node['node'], JID.new(node['jid'])
+
+ raise StanzaErrors::Forbidden.new(self, 'auth') unless stream.user.jid.bare == jid.bare
+ raise StanzaErrors::ItemNotFound.new(self, 'cancel') unless pubsub.node?(id)
+ raise StanzaErrors::UnexpectedRequest.new(self, 'cancel') unless pubsub.subscribed?(id, jid)
+
+ pubsub.unsubscribe(id, jid)
+ stream.write(to_result)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/storage.rb b/lib/vines/storage.rb
new file mode 100644
index 0000000..0e09590
--- /dev/null
+++ b/lib/vines/storage.rb
@@ -0,0 +1,291 @@
+# encoding: UTF-8
+
+module Vines
+ class Storage
+ include Vines::Log
+
+ @@nicks = {}
+
+ # Register a nickname that can be used in the config file to specify this
+ # storage implementation.
+ #
+ # name - The String name for this storage backend.
+ #
+ # Returns nothing.
+ def self.register(name)
+ @@nicks[name.to_sym] = self
+ end
+
+ def self.from_name(name, &block)
+ klass = @@nicks[name.to_sym]
+ raise "#{name} storage class not found" unless klass
+ klass.new(&block)
+ end
+
+ # Wrap a blocking IO method in a new method that pushes the original method
+ # onto EventMachine's thread pool using EM#defer. Storage classes implemented
+ # with blocking IO don't need to worry about threading or blocking the
+ # EventMachine reactor thread if they wrap their methods with this one.
+ #
+ # Examples
+ #
+ # def find_user(jid)
+ # some_blocking_lookup(jid)
+ # end
+ # defer :find_user
+ #
+ # Storage classes that use asynchronous IO (through an EventMachine
+ # enabled library like em-http-request or em-redis) don't need any special
+ # consideration and must not use this method.
+ #
+ # Returns nothing.
+ def self.defer(method)
+ old = instance_method(method)
+ define_method method do |*args|
+ fiber = Fiber.current
+ op = operation { old.bind(self).call(*args) }
+ cb = proc {|result| fiber.resume(result) }
+ EM.defer(op, cb)
+ Fiber.yield
+ end
+ end
+
+ # Wrap a method with Fiber yield and resume logic. The method must yield
+ # its result to a block. This makes it easier to write asynchronous
+ # implementations of `authenticate`, `find_user`, and `save_user` that
+ # block and return a result rather than yielding.
+ #
+ # Examples
+ #
+ # def find_user(jid)
+ # http = EM::HttpRequest.new(url).get
+ # http.callback { yield build_user_from_http_response(http) }
+ # end
+ # fiber :find_user
+ #
+ # Because `find_user` has been wrapped in Fiber logic, we can call it
+ # synchronously even though it uses asynchronous EventMachine calls.
+ #
+ # user = storage.find_user('alice at wonderland.lit')
+ # puts user.nil?
+ #
+ # Returns nothing.
+ def self.fiber(method)
+ old = instance_method(method)
+ define_method method do |*args|
+ fiber, yielding = Fiber.current, true
+ old.bind(self).call(*args) do |user|
+ fiber.resume(user) rescue yielding = false
+ end
+ Fiber.yield if yielding
+ end
+ end
+
+ # Validate the username and password pair.
+ #
+ # username - The String login JID to verify.
+ # password - The String password the user presented to the server.
+ #
+ # Examples
+ #
+ # user = storage.authenticate('alice at wonderland.lit', 'secr3t')
+ # puts user.nil?
+ #
+ # This default implementation validates the password against a bcrypt hash
+ # of the password stored in the database. Sub-classes not using bcrypt
+ # passwords must override this method.
+ #
+ # Returns a Vines::User object on success, nil on failure.
+ def authenticate(username, password)
+ user = find_user(username)
+ hash = BCrypt::Password.new(user.password) rescue nil
+ (hash && hash == password) ? user : nil
+ end
+
+ # Find the user in the storage database by their unique JID.
+ #
+ # jid - The String or JID of the user, possibly nil. This may be either a
+ # bare JID or full JID. Implementations of this method must convert
+ # the JID to a bare JID before searching for the user in the database.
+ #
+ # Examples
+ #
+ # # Bare JID lookup.
+ # user = storage.find_user('alice at wonderland.lit')
+ # puts user.nil?
+ #
+ # # Full JID lookup.
+ # user = storage.find_user('alice at wonderland.lit/tea')
+ # puts user.nil?
+ #
+ # Returns the User identified by the JID, nil if not found.
+ def find_user(jid)
+ raise 'subclass must implement'
+ end
+
+ # Persist the user to the database, and return when the save is complete.
+ #
+ # user - The User to persist.
+ #
+ # Examples
+ #
+ # alice = Vines::User.new(jid: 'alice at wonderland.lit')
+ # storage.save_user(alice)
+ # puts 'saved'
+ #
+ # Returns nothing.
+ def save_user(user)
+ raise 'subclass must implement'
+ end
+
+ # Find the user's vcard by their unique JID.
+ #
+ # jid - The String or JID of the user, possibly nil. This may be either a
+ # bare JID or full JID. Implementations of this method must convert
+ # the JID to a bare JID before searching for the vcard in the database.
+ #
+ # Examples
+ #
+ # card = storage.find_vcard('alice at wonderland.lit')
+ # puts card.nil?
+ #
+ # Returns the vcard's Nokogiri::XML::Node, nil if not found.
+ def find_vcard(jid)
+ raise 'subclass must implement'
+ end
+
+ # Save the vcard to the database, and return when the save is complete.
+ #
+ # jid - The String or JID of the user, possibly nil. This may be either a
+ # bare JID or full JID. Implementations of this method must convert
+ # the JID to a bare JID before saving the vcard.
+ # card - The vcard's Nokogiri::XML::Node.
+ #
+ # Examples
+ #
+ # card = Nokogiri::XML('<vCard>...</vCard>').root
+ # storage.save_vcard('alice at wonderland.lit', card)
+ # puts 'saved'
+ #
+ # Returns nothing.
+ def save_vcard(jid, card)
+ raise 'subclass must implement'
+ end
+
+ # Find the private XML fragment previously stored by the user. Private
+ # XML storage uniquely identifies fragments by JID, root element name,
+ # and root element namespace.
+ #
+ # jid - The String or JID of the user, possibly nil. This may be either a
+ # bare JID or full JID. Implementations of this method must convert
+ # the JID to a bare JID before searching for the fragment in the database.
+ # node - The XML::Node that uniquely identifies the fragment by element
+ # name and namespace.
+ #
+ # Examples
+ #
+ # root = Nokogiri::XML('<custom xmlns="urn:custom:ns"/>').root
+ # fragment = storage.find_fragment('alice at wonderland.lit', root)
+ # puts fragment.nil?
+ #
+ # Returns the fragment's Nokogiri::XML::Node or nil if not found.
+ def find_fragment(jid, node)
+ raise 'subclass must implement'
+ end
+
+ # Save the XML fragment to the database, and return when the save is complete.
+ #
+ # jid - The String or JID of the user, possibly nil. This may be
+ # either a bare JID or full JID. Implementations of this method
+ # must convert the JID to a bare JID before searching for the
+ # fragment.
+ # fragment - The XML::Node to save.
+ #
+ # Examples
+ #
+ # fragment = Nokogiri::XML('<custom xmlns="urn:custom:ns">some data</custom>').root
+ # storage.save_fragment('alice at wonderland.lit', fragment)
+ # puts 'saved'
+ #
+ # Returns nothing.
+ def save_fragment(jid, fragment)
+ raise 'subclass must implement'
+ end
+
+ # Check whether offline messages are available for the user
+ # jid - The String or JID of the user, possibly nil. This may be
+ # either a bare JID or full JID. Implementations of this method
+ # must convert the JID to a bare JID before searching for
+ # offline messages.
+ #
+ # Returns hash
+ def find_messages(jid)
+ raise 'subclass must implement'
+ end
+
+ # Save the offline message to the database, and return when the save is complete.
+ #
+ # from - The String or JID of the user.
+ # to - The String or JID of the user.
+ # message - The message you want to store.
+ #
+ # Returns nothing.
+ def save_message(from, to, message)
+ raise 'subclass must implement'
+ end
+
+ # Delete a offline message from database.
+ #
+ # id - The identifier of the offline message
+ #
+ # Returns nothing.
+ def destroy_message(id)
+ raise 'subclass must implement'
+ end
+
+ # Retrieve the avatar url by jid.
+ #
+ # jid - The String or JID of the user.
+ #
+ # Returns string
+ def find_avatar_by_jid(jid)
+ raise 'subclass must implement'
+ end
+
+ private
+
+ # Determine if any of the arguments are nil or empty strings.
+ #
+ # Examples
+ #
+ # username, password = 'alice at wonderland.lit', ''
+ # empty?(username, password) #=> true
+ #
+ # Returns true if any of the arguments are nil or empty strings.
+ def empty?(*args)
+ args.flatten.any? {|arg| (arg || '').strip.empty? }
+ end
+
+ # Create a proc suitable for running on the EM.defer thread pool, that
+ # traps and logs any errors thrown by the provided block.
+ #
+ # block - The block to wrap in error handling.
+ #
+ # Examples
+ #
+ # op = operation { do_something_on_thread_pool() }
+ # EM.defer(op)
+ #
+ # Returns a Proc.
+ def operation
+ proc do
+ begin
+ yield
+ rescue => e
+ log.error("Thread pool operation failed: #{e.message}")
+ nil
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/storage/local.rb b/lib/vines/storage/local.rb
new file mode 100644
index 0000000..c0e47ea
--- /dev/null
+++ b/lib/vines/storage/local.rb
@@ -0,0 +1,151 @@
+# encoding: UTF-8
+
+module Vines
+ class Storage
+
+ # A storage implementation that persists data to YAML files on the
+ # local file system.
+ class Local < Storage
+ register :fs
+
+ def initialize(&block)
+ @dir = nil
+ instance_eval(&block)
+ unless @dir && File.directory?(@dir) && File.writable?(@dir)
+ raise 'Must provide a writable storage directory'
+ end
+
+ %w[user vcard fragment].each do |sub|
+ sub = File.expand_path(sub, @dir)
+ Dir.mkdir(sub, 0700) unless File.exists?(sub)
+ end
+ end
+
+ def dir(dir=nil)
+ dir ? @dir = File.expand_path(dir) : @dir
+ end
+
+ def find_user(jid)
+ jid = JID.new(jid).bare.to_s
+ file = "user/#{jid}" unless jid.empty?
+ record = YAML.load(read(file)) rescue nil
+ return User.new(jid: jid).tap do |user|
+ user.name, user.password = record.values_at('name', 'password')
+ (record['roster'] || {}).each_pair do |jid, props|
+ user.roster << Contact.new(
+ jid: jid,
+ name: props['name'],
+ subscription: props['subscription'],
+ ask: props['ask'],
+ groups: props['groups'] || [])
+ end
+ end if record
+ end
+
+ def save_user(user)
+ record = {'name' => user.name, 'password' => user.password, 'roster' => {}}
+ user.roster.each do |contact|
+ record['roster'][contact.jid.bare.to_s] = contact.to_h
+ end
+ save("user/#{user.jid.bare}", YAML.dump(record))
+ end
+
+ def find_vcard(jid)
+ jid = JID.new(jid).bare.to_s
+ return if jid.empty?
+ file = "vcard/#{jid}"
+ Nokogiri::XML(read(file)).root rescue nil
+ end
+
+ def save_vcard(jid, card)
+ jid = JID.new(jid).bare.to_s
+ return if jid.empty?
+ save("vcard/#{jid}", card.to_xml)
+ end
+
+ def find_fragment(jid, node)
+ jid = JID.new(jid).bare.to_s
+ return if jid.empty?
+ file = 'fragment/%s' % fragment_id(jid, node)
+ Nokogiri::XML(read(file)).root rescue nil
+ end
+
+ def save_fragment(jid, node)
+ jid = JID.new(jid).bare.to_s
+ return if jid.empty?
+ file = 'fragment/%s' % fragment_id(jid, node)
+ save(file, node.to_xml)
+ end
+
+ def find_messages(jid)
+ {}
+ end
+
+ def save_message(from, to, message)
+ # do nothing
+ end
+
+ def destroy_message(id)
+ # do nothing
+ end
+
+ private
+
+ # Resolves a relative file name into an absolute path inside the
+ # storage directory.
+ #
+ # file - A fully-qualified or relative file name String.
+ #
+ # Returns the fully-qualified file path String.
+ #
+ # Raises RuntimeError if the resolved path is outside of the storage
+ # directory. This prevents directory path traversals with maliciously
+ # crafted JIDs.
+ def absolute_path(file)
+ File.expand_path(file, @dir).tap do |absolute|
+ parent = File.dirname(File.dirname(absolute))
+ raise 'path traversal' unless parent == @dir
+ end
+ end
+
+ # Read the file from the filesystem and return its contents as a String.
+ # All files are assumed to be encoded as UTF-8.
+ #
+ # file - A fully-qualified or relative file name String.
+ #
+ # Returns the file content as a UTF-8 encoded String.
+ def read(file)
+ file = absolute_path(file)
+ File.read(file, encoding: 'utf-8')
+ end
+
+ # Write the content to the file. Make sure to consistently encode files
+ # we read and write as UTF-8.
+ #
+ # file - A fully-qualified or relative file name String.
+ # content - The String to write.
+ #
+ # Returns nothing.
+ def save(file, content)
+ file = absolute_path(file)
+ File.open(file, 'w:utf-8') {|f| f.write(content) }
+ File.chmod(0600, file)
+ end
+
+ # Generates a unique file id for the user's private XML fragment.
+ #
+ # Private XML fragment storage needs to uniquely identify fragment files
+ # on disk. We combine the user's JID with a SHA-1 hash of the element's
+ # name and namespace to avoid special characters in the file name.
+ #
+ # jid - A bare JID identifying the user who owns this fragment.
+ # node - A Nokogiri::XML::Node for the XML to be stored.
+ #
+ # Returns an id String suitable for use in a file name.
+ def fragment_id(jid, node)
+ id = Digest::SHA1.hexdigest("#{node.name}:#{node.namespace.href}")
+ "#{jid}-#{id}"
+ end
+ end
+ end
+end
diff --git a/lib/vines/storage/null.rb b/lib/vines/storage/null.rb
new file mode 100644
index 0000000..d06ce59
--- /dev/null
+++ b/lib/vines/storage/null.rb
@@ -0,0 +1,51 @@
+# encoding: UTF-8
+
+module Vines
+ class Storage
+ # A storage implementation that does not persist data to any form of storage.
+ # When looking up the storage object for a domain, it's easier to treat a
+ # missing domain with a Null storage than checking for nil.
+ #
+ # For example, presence subscription stanzas sent to a pubsub subdomain
+ # have no storage. Rather than checking for nil storage or pubsub addresses,
+ # it's easier to treat stanzas to pubsub domains as Null storage that never
+ # finds or saves users and their rosters.
+ class Null < Storage
+ def find_user(jid)
+ nil
+ end
+
+ def save_user(user)
+ # do nothing
+ end
+
+ def find_vcard(jid)
+ nil
+ end
+
+ def save_vcard(jid, card)
+ # do nothing
+ end
+
+ def find_fragment(jid, node)
+ nil
+ end
+
+ def save_fragment(jid, node)
+ # do nothing
+ end
+
+ def find_messages(jid)
+ {}
+ end
+
+ def save_message(from, to, message)
+ # do nothing
+ end
+
+ def destroy_message(id)
+ # do nothing
+ end
+ end
+ end
+end
diff --git a/lib/vines/storage/sql.rb b/lib/vines/storage/sql.rb
new file mode 100644
index 0000000..1ea0047
--- /dev/null
+++ b/lib/vines/storage/sql.rb
@@ -0,0 +1,346 @@
+# encoding: UTF-8
+
+module Vines
+ class Storage
+ class Sql < Storage
+ include Vines::Log
+
+ register :sql
+
+ class Profile < ActiveRecord::Base
+ belongs_to :person
+ end
+ class Person < ActiveRecord::Base
+ has_one :profile
+
+ def local?
+ !self.owner_id.nil?
+ end
+
+ def name(opts = {})
+ self.profile.first_name.blank? && self.profile.last_name.blank? ?
+ self.diaspora_handle : "#{self.profile.first_name.to_s.strip} #{self.profile.last_name.to_s.strip}".strip
+ end
+ end
+
+ class Aspect < ActiveRecord::Base
+ belongs_to :users
+
+ has_many :aspect_memberships
+ has_many :contacts
+ end
+
+ class AspectMembership < ActiveRecord::Base
+ belongs_to :aspect
+ belongs_to :contact
+
+ has_one :users, :through => :contact
+ has_one :person, :through => :contact
+ end
+
+ class Contact < ActiveRecord::Base
+ scope :chat_enabled, -> {
+ joins(:aspects)
+ .where("aspects.chat_enabled = ?", true)
+ .group("person_id, contacts.id")
+ }
+
+ belongs_to :users
+ belongs_to :person
+
+ has_many :aspect_memberships
+ has_many :aspects, :through => :aspect_memberships
+ end
+
+ class User < ActiveRecord::Base
+ has_many :contacts
+ has_many :chat_contacts, :dependent => :destroy
+ has_many :fragments, :dependent => :delete_all
+
+ has_one :person, :foreign_key => :owner_id
+ end
+
+ class ChatOfflineMessage < ActiveRecord::Base; end
+
+ class ChatContact < ActiveRecord::Base
+ belongs_to :users
+ end
+
+ class ChatFragment < ActiveRecord::Base
+ belongs_to :users
+ end
+
+ # Wrap the method with ActiveRecord connection pool logic, so we properly
+ # return connections to the pool when we're finished with them. This also
+ # defers the original method by pushing it onto the EM thread pool because
+ # ActiveRecord uses blocking IO.
+ def self.with_connection(method, args={})
+ deferrable = args.key?(:defer) ? args[:defer] : true
+ old = instance_method(method)
+ define_method method do |*args|
+ ActiveRecord::Base.connection_pool.with_connection do
+ old.bind(self).call(*args)
+ end
+ end
+ defer(method) if deferrable
+ end
+
+ def initialize(&block)
+ @config = {}
+ unless defined? Rails
+ raise "You configured diaspora-sql adapter without Diaspora environment"
+ end
+
+ config = Rails.application.config.database_configuration[Rails.env]
+ %w[adapter database host port username password].each do |key|
+ @config[key.to_sym] = config[key]
+ end
+
+ required = [:adapter, :database]
+ required << [:host, :port] unless @config[:adapter] == 'sqlite3'
+ required.flatten.each {|key| raise "Must provide #{key}" unless @config[key] }
+ [:username, :password].each {|key| @config.delete(key) if empty?(@config[key]) }
+ establish_connection
+ end
+
+ def find_user(jid)
+ jid = JID.new(jid).bare.to_s
+ return if jid.empty?
+ xuser = user_by_jid(jid)
+ return Vines::User.new(jid: jid).tap do |user|
+ user.name, user.password, user.token =
+ xuser.username,
+ xuser.encrypted_password,
+ xuser.authentication_token
+
+ # add diaspora contacts
+ xuser.contacts.chat_enabled.each do |contact|
+ handle = contact.person.diaspora_handle
+ profile = contact.person.profile
+ name = "#{profile.first_name} #{profile.last_name}"
+ name = handle.gsub(/\@.*?$/, '') if name.strip.empty?
+ ask, subscription, groups = get_diaspora_flags(contact)
+ user.roster << Vines::Contact.new(
+ jid: handle,
+ name: name,
+ subscription: subscription,
+ from_diaspora: true,
+ groups: groups,
+ ask: ask)
+ end
+
+ # add external contacts
+ xuser.chat_contacts.each do |contact|
+ user.roster << Vines::Contact.new(
+ jid: contact.jid,
+ name: contact.name,
+ subscription: contact.subscription,
+ groups: get_external_groups,
+ ask: contact.ask)
+ end
+ end if xuser
+ end
+ with_connection :find_user
+
+ def authenticate(username, password)
+ user = find_user(username)
+
+ pepper = "#{password}#{Devise.pepper}" rescue password
+ dbhash = BCrypt::Password.new(user.password) rescue nil
+ hash = BCrypt::Engine.hash_secret(pepper, dbhash.salt) rescue nil
+
+ userAuth = ((hash && dbhash) && hash == dbhash)
+ tokenAuth = ((password && user) && password == user.token)
+ (tokenAuth || userAuth)? user : nil
+ end
+
+ def save_user(user)
+ # it is not possible to register an account via xmpp server
+ xuser = user_by_jid(user.jid) || return
+
+ # remove deleted contacts from roster
+ xuser.chat_contacts.delete(xuser.chat_contacts.select do |contact|
+ !user.contact?(contact.jid)
+ end)
+
+ # update contacts
+ xuser.chat_contacts.each do |contact|
+ fresh = user.contact(contact.jid)
+ contact.update_attributes(
+ name: fresh.name,
+ ask: fresh.ask,
+ subscription: fresh.subscription)
+ end
+
+ # add new contacts to roster
+ jids = xuser.chat_contacts.map {|c|
+ c.jid if (c.user_id == xuser.id)
+ }.compact
+ user.roster.select {|contact|
+ unless contact.from_diaspora
+ xuser.chat_contacts.build(
+ user_id: xuser.id,
+ jid: contact.jid.bare.to_s,
+ name: contact.name,
+ ask: contact.ask,
+ subscription: contact.subscription) unless jids.include?(contact.jid.bare.to_s)
+ end
+ }
+ xuser.save
+ end
+ with_connection :save_user
+
+ def find_vcard(jid)
+ jid = JID.new(jid).bare.to_s
+ return nil if jid.empty?
+ person = Sql::Person.find_by_diaspora_handle(jid)
+ return nil unless person.nil? || person.local?
+
+ build_vcard(person)
+ end
+ with_connection :find_vcard
+
+ def save_vcard(jid, card)
+ # NOTE this is not supported. If you'd like to change your
+ # vcard details you can edit it via diaspora-web-interface
+ nil
+ end
+ with_connection :save_vcard
+
+ def find_messages(jid)
+ jid = JID.new(jid).bare.to_s
+ return if jid.empty?
+ results = Hash.new
+ Sql::ChatOfflineMessage.where(:to => jid).each do |r|
+ results[r.id] = {
+ :from => r.from,
+ :to => r.to,
+ :message => r.message,
+ :created_at => r.created_at
+ }
+ end
+ return results
+ end
+ with_connection :find_messages
+
+ def save_message(from, to, msg)
+ return if from.empty? || to.empty? || msg.empty?
+ com = Sql::ChatOfflineMessage
+ current = com.count(:to => to)
+ unless current < Config.instance.max_offline_msgs
+ com.where(:to => to)
+ .order(created_at: :asc)
+ .first
+ .delete
+ end
+ com.create(:from => from, :to => to, :message => msg)
+ end
+ with_connection :save_message
+
+ def destroy_message(id)
+ id = id.to_i rescue nil
+ return if id.nil?
+ Sql::ChatOfflineMessage.find(id).destroy
+ end
+ with_connection :destroy_message
+
+ def find_fragment(jid, node)
+ jid = JID.new(jid).bare.to_s
+ return if jid.empty?
+ if fragment = fragment_by_jid(jid, node)
+ Nokogiri::XML(fragment.xml).root rescue nil
+ end
+ end
+ with_connection :find_fragment
+
+ def save_fragment(jid, node)
+ jid = JID.new(jid).bare.to_s
+ fragment = fragment_by_jid(jid, node) ||
+ Sql::ChatFragment.new(
+ user: user_by_jid(jid),
+ root: node.name,
+ namespace: node.namespace.href)
+ fragment.xml = node.to_xml
+ fragment.save
+ end
+ with_connection :save_fragment
+
+ def find_avatar_by_jid(jid)
+ jid = JID.new(jid).bare.to_s
+ return nil if jid.empty?
+
+ person = Sql::Person.find_by_diaspora_handle(jid)
+ return nil if person.nil?
+ return nil if person.profile.nil?
+ return nil unless person.local?
+ person.profile.image_url
+ end
+ with_connection :find_avatar_by_jid
+
+ private
+ def establish_connection
+ ActiveRecord::Base.logger = log # using vines logger
+ ActiveRecord::Base.establish_connection(@config)
+ end
+
+ def user_by_jid(jid)
+ name = JID.new(jid).node
+ Sql::User.find_by_username(name)
+ end
+
+ def get_external_groups
+ # TODO Make the group name configurable by the user
+ # https://github.com/diaspora/vines/issues/39
+ group_name = "External XMPP Contacts"
+ matches = Sql::Aspect.where(:name => group_name).count
+ if matches > 0
+ group_name = "#{group_name} (#{matches + 1})"
+ end
+ [ group_name ]
+ end
+
+ def fragment_by_jid(jid, node)
+ jid = JID.new(jid).bare.to_s
+ clause = 'user_id=(select id from users where jid=?) and root=? and namespace=?'
+ Sql::ChatFragment.where(clause, jid, node.name, node.namespace.href).first
+ end
+
+ def build_vcard(person)
+ builder = Nokogiri::XML::Builder.new
+ builder.vCard('xmlns' => 'vcard-temp') do |xml|
+ xml.send(:"FN", person.name) if person.name
+ xml.send(:"N") do |sub|
+ sub.send(:"FAMILY", person.profile.last_name) if person.profile.last_name
+ sub.send(:"GIVEN", person.profile.first_name) if person.profile.first_name
+ end if (person.profile.last_name? || person.profile.first_name?)
+ xml.send(:"URL", person.url) if person.url
+ xml.send(:"PHOTO") do |sub|
+ sub.send(:"EXTVAL", person.profile.image_url)
+ end if person.profile.image_url
+ end
+
+ builder.to_xml :save_with => Nokogiri::XML::Node::SaveOptions::NO_DECLARATION
+ end
+
+ def get_diaspora_flags(contact)
+ groups = Array.new
+ ask, subscription = 'none', 'none'
+ contact.aspects.each do |aspect|
+ groups.push(aspect.name)
+ end
+
+ if contact.sharing && contact.receiving
+ subscription = 'both'
+ elsif contact.sharing && !contact.receiving
+ ask = 'suscribe'
+ subscription = 'from'
+ elsif !contact.sharing && contact.receiving
+ subscription = 'to'
+ else
+ ask = 'suscribe'
+ end
+ return ask, subscription, groups
+ end
+ end
+ end
+end
diff --git a/lib/vines/store.rb b/lib/vines/store.rb
new file mode 100644
index 0000000..6b1fdb6
--- /dev/null
+++ b/lib/vines/store.rb
@@ -0,0 +1,152 @@
+# encoding: UTF-8
+
+module Vines
+ # An X509 certificate store that validates certificate trust chains.
+ # This uses the conf/certs/*.crt files as the list of trusted root
+ # CA certificates.
+ class Store
+ include Vines::Log
+ @@sources = nil
+
+ # Create a certificate store to read certificate files from the given
+ # directory.
+ #
+ # dir - The String directory name (absolute or relative).
+ def initialize(dir)
+ @dir = File.expand_path(dir)
+ @store = OpenSSL::X509::Store.new
+ certs.each {|cert| append(cert) }
+ end
+
+ # Return true if the certificate is signed by a CA certificate in the
+ # store. If the certificate can be trusted, it's added to the store so
+ # it can be used to trust other certs.
+ #
+ # pem - The PEM encoded certificate String.
+ #
+ # Returns true if the certificate is trusted.
+ def trusted?(pem)
+ if cert = OpenSSL::X509::Certificate.new(pem) rescue nil
+ @store.verify(cert).tap do |trusted|
+ append(cert) if trusted
+ end
+ end
+ end
+
+ # Return true if the domain name matches one of the names in the
+ # certificate. In other words, is the certificate provided to us really
+ # for the domain to which we think we're connected?
+ #
+ # pem - The PEM encoded certificate String.
+ # domain - The domain name String.
+ #
+ # Returns true if the certificate was issued for the domain.
+ def domain?(pem, domain)
+ if cert = OpenSSL::X509::Certificate.new(pem) rescue nil
+ OpenSSL::SSL.verify_certificate_identity(cert, domain) rescue false
+ end
+ end
+
+ # Return the trusted root CA certificates installed in conf/certs. These
+ # certificates are used to start the trust chain needed to validate certs
+ # we receive from clients and servers.
+ #
+ # Returns an Array of OpenSSL::X509::Certificate objects.
+ def certs
+ @@sources ||= begin
+ pattern = /-{5}BEGIN CERTIFICATE-{5}\n.*?-{5}END CERTIFICATE-{5}\n/m
+ files = Dir[File.join(@dir, '*.crt')]
+ if defined?(AppConfig)
+ chain = AppConfig.environment.certificate_authorities.get
+ files << chain unless chain.nil?
+ end
+ pairs = files.map do |name|
+ begin
+ File.open(name, "r:UTF-8") do |f|
+ pems = f.read.scan(pattern)
+ certs = pems.map {|pem| OpenSSL::X509::Certificate.new(pem) }
+ certs.reject! {|cert| cert.not_after < Time.now }
+ [name, certs]
+ end
+ rescue ArgumentError => e
+ log.error("Skipping '#{name}' cause of '#{e.message.to_s}'! "+
+ "Checkout https://wiki.diasporafoundation.org/Vines#FAQ "+
+ "for further instructions.")
+ end
+ end
+ Hash[pairs.compact]
+ end
+ @@sources.values.flatten
+ end
+
+ # Returns a pair of file names containing the public key certificate
+ # and matching private key for the given domain. This supports using
+ # wildcard certificate files to serve several subdomains.
+ #
+ # Finding the certificate and private key file for a domain follows these steps:
+ #
+ # - Look for <domain>.crt and <domain>.key files in the conf/certs
+ # directory. If found, return those file names, otherwise . . .
+ #
+ # - Inspect all conf/certs/*.crt files for certificates that contain the
+ # domain name either as the subject common name (CN) or as a DNS
+ # subjectAltName. The corresponding private key must be in a file of the
+ # same name as the certificate's, but with a .key extension.
+ #
+ # So in the simplest configuration, the tea.wonderland.lit encryption files
+ # would be named:
+ #
+ # - conf/certs/tea.wonderland.lit.crt
+ # - conf/certs/tea.wonderland.lit.key
+ #
+ # However, in the case of a wildcard certificate for *.wonderland.lit,
+ # the files would be:
+ #
+ # - conf/certs/wonderland.lit.crt
+ # - conf/certs/wonderland.lit.key
+ #
+ # These same two files would be returned for the subdomains of:
+ #
+ # - tea.wonderland.lit
+ # - crumpets.wonderland.lit
+ # - etc.
+ #
+ # domain - The String domain name.
+ #
+ # Returns a two element String array for the certificate and private key
+ # file names or nil if not found.
+ def files_for_domain(domain)
+ crt = File.expand_path("#{domain}.crt", @dir)
+ key = File.expand_path("#{domain}.key", @dir)
+ return [crt, key] if File.exists?(crt) && File.exists?(key)
+
+ # Might be a wildcard cert file.
+ @@sources.each do |file, certs|
+ certs.each do |cert|
+ if OpenSSL::SSL.verify_certificate_identity(cert, domain)
+ key = file.chomp(File.extname(file)) + '.key'
+ return [file, key] if File.exists?(file) && File.exists?(key)
+ end
+ end
+ end
+ log.error("Your're using vines without a certificate! "+
+ "Checkout https://wiki.diasporafoundation.org/Vines#Certificates "+
+ "for further instructions.")
+ nil
+ end
+
+ private
+
+ # Add a trusted certificate to the store, suppressing any OpenSSL errors
+ # caused by the certificate already being stored.
+ #
+ # cert - The OpenSSL::X509::Certificate to add.
+ #
+ # Returns nothing.
+ def append(cert)
+ @store.add_cert(cert)
+ rescue OpenSSL::X509::StoreError
+ # Already added to store.
+ end
+ end
+end
diff --git a/lib/vines/stream.rb b/lib/vines/stream.rb
new file mode 100644
index 0000000..bd322bd
--- /dev/null
+++ b/lib/vines/stream.rb
@@ -0,0 +1,309 @@
+# encoding: UTF-8
+
+module Vines
+ # The base class for various XMPP streams (c2s, s2s, component, http),
+ # containing behavior common to all streams like rate limiting, stanza
+ # parsing, and stream error handling.
+ class Stream < EventMachine::Connection
+ include Vines::Log
+
+ ERROR = 'error'.freeze
+ STREAM = 'stream'.freeze
+ PAD = 20
+
+ attr_reader :config, :domain, :id, :state
+ attr_accessor :user
+
+ def initialize(config)
+ @config = config
+ end
+
+ # Initialize the stream after its connection to the server has completed.
+ # EventMachine calls this method when an incoming connection is accepted
+ # into the event loop.
+ #
+ # Returns nothing.
+ def post_init
+ @remote_addr, @local_addr = addresses
+ @user, @closed, @stanza_size = nil, false, 0
+ @bucket = TokenBucket.new(100, 10)
+ @store = Store.new(@config.certs)
+ @nodes = EM::Queue.new
+ process_node_queue
+ create_parser
+ log.info { "%s %21s -> %s" %
+ ['Stream connected:'.ljust(PAD), @remote_addr, @local_addr] }
+ end
+
+ # Initialize a new XML parser for this connection. This is called when the
+ # stream is first connected as well as for stream restarts during
+ # negotiation. Subclasses can override this method to provide a different
+ # type of parser (e.g. HTTP).
+ #
+ # Returns nothing.
+ def create_parser
+ @parser = Parser.new.tap do |parser|
+ parser.stream_open {|node| @nodes.push(node) }
+ parser.stream_close { close_connection }
+ parser.stanza {|node| @nodes.push(node) }
+ end
+ end
+
+ # Advance the state machine into the `Closed` state so any remaining queued
+ # nodes are not processed while we're waiting for EM to actually close the
+ # connection.
+ #
+ # Returns nothing.
+ def close_connection(after_writing=false)
+ super
+ @closed = true
+ advance(Client::Closed.new(self))
+ end
+
+ # Read bytes off the stream and feed them into the XML parser. EventMachine
+ # is responsible for calling this method on its event loop as connections
+ # become readable.
+ #
+ # data - The byte String sent to the server from the client, hopefully XML.
+ #
+ # Returns nothing.
+ def receive_data(data)
+ return if @closed
+ @stanza_size += data.bytesize
+ if @stanza_size < max_stanza_size
+ @parser << data rescue error(StreamErrors::NotWellFormed.new)
+ else
+ error(StreamErrors::PolicyViolation.new('max stanza size reached'))
+ end
+ end
+
+ # Reset the connection's XML parser when a new <stream:stream> header
+ # is received.
+ #
+ # Returns nothing.
+ def reset
+ create_parser
+ end
+
+ # Returns the storage system for the domain. If no domain is given,
+ # the stream's storage mechanism is returned.
+ def storage(domain=nil)
+ @config.storage(domain || self.domain)
+ end
+
+ # Returns the Config::Host virtual host for the stream's domain.
+ def vhost
+ @config.vhost(domain)
+ end
+
+ # Reload the user's information into their active connections. Call this
+ # after storage.save_user() to sync the new user state with their other
+ # connections.
+ #
+ # user - The User whose connection info needs refreshing.
+ #
+ # Returns nothing.
+ def update_user_streams(user)
+ connected_resources(user.jid.bare).each do |stream|
+ stream.user.update_from(user)
+ end
+ end
+
+ def connected_resources(jid)
+ router.connected_resources(jid, user.jid)
+ end
+
+ def available_resources(*jid)
+ router.available_resources(*jid, user.jid)
+ end
+
+ def interested_resources(*jid)
+ router.interested_resources(*jid, user.jid)
+ end
+
+ def ssl_verify_peer(pem)
+ # Skip verifying if user accept self-signed certificates
+ return true if self.vhost.accept_self_signed?
+ # EM is supposed to close the connection when this returns false,
+ # but it only does that for inbound connections, not when we
+ # make a connection to another server.
+ @store.trusted?(pem).tap do |trusted|
+ close_connection unless trusted
+ end
+ end
+
+ def cert_domain_matches?(domain)
+ @store.domain?(get_peer_cert, domain)
+ end
+
+ # Send the data over the wire to this client.
+ #
+ # data - The XML String or XML::Node to write to the socket.
+ #
+ # Returns nothing.
+ def write(data)
+ log_node(data, :out)
+ if data.respond_to?(:to_xml)
+ data = data.to_xml(:indent => 0)
+ end
+ send_data(data)
+ end
+
+ def encrypt
+ cert, key = @store.files_for_domain(domain)
+ start_tls(cert_chain_file: cert, private_key_file: key, verify_peer: true)
+ end
+
+ # Returns true if the TLS certificate and private key files for this domain
+ # exist and can be used to encrypt this stream.
+ def encrypt?
+ !@store.files_for_domain(domain).nil?
+ end
+
+ def unbind
+ router.delete(self)
+ log.info { "%s %21s -> %s" %
+ ['Stream disconnected:'.ljust(PAD), @remote_addr, @local_addr] }
+ log.info { "Streams connected: #{router.size}" }
+ end
+
+ # Advance the stream's state machine to the new state. XML nodes received
+ # by the stream will be passed to this state's `node` method.
+ #
+ # state - The Stream::State to process the stanzas next.
+ #
+ # Returns the new Stream::State.
+ def advance(state)
+ @state = state
+ end
+
+ # Stream level errors close the stream while stanza and SASL errors are
+ # written to the client and leave the stream open. All exceptions should
+ # pass through this method for consistent handling.
+ #
+ # e - The StandardError, usually XmppError, that occurred.
+ #
+ # Returns nothing.
+ def error(e)
+ case e
+ when SaslError, StanzaError
+ write(e.to_xml)
+ when StreamError
+ send_stream_error(e)
+ close_stream
+ else
+ log.error(e)
+ send_stream_error(StreamErrors::InternalServerError.new)
+ close_stream
+ end
+ end
+
+ def router
+ @config.router
+ end
+
+ private
+
+ # Determine the remote and local socket addresses used by this connection.
+ #
+ # Returns a two-element Array of String addresses.
+ def addresses
+ [get_peername, get_sockname].map do |addr|
+ addr ? Socket.unpack_sockaddr_in(addr)[0, 2].reverse.join(':') : 'unknown'
+ end
+ end
+
+ # Write the StreamError's xml to the stream. Subclasses can override
+ # this method with custom error writing behavior.
+ #
+ # A call to `close_stream` should follow this method. Stream level errors
+ # are fatal to the connection.
+ #
+ # e - The StreamError that caused the connection to close.
+ #
+ # Returns nothing.
+ def send_stream_error(e)
+ write(e.to_xml)
+ end
+
+ # Write a closing stream tag and close the connection. Subclasses can
+ # override this method for custom close behavior.
+ #
+ # Returns nothing.
+ def close_stream
+ write('</stream:stream>')
+ close_connection_after_writing
+ end
+
+ def error?(node)
+ ns = node.namespace ? node.namespace.href : nil
+ node.name == ERROR && ns == NAMESPACES[:stream]
+ end
+
+ # Schedule a queue pop on the EM thread to handle the next element. This
+ # guarantees all stanzas received on this stream are processed in order.
+ #
+ # http://tools.ietf.org/html/rfc6120#section-10.1
+ #
+ # Once a node is processed, this method recursively schedules itself to pop
+ # the next node and so on. A single call to this method effectively begins
+ # an asynchronous node processing loop.
+ #
+ # Returns nothing.
+ def process_node_queue
+ @nodes.pop do |node|
+ Fiber.new do
+ process_node(node)
+ process_node_queue
+ end.resume unless @closed
+ end
+ end
+
+ def process_node(node)
+ log_node(node, :in)
+ @stanza_size = 0
+ enforce_rate_limit
+ if error?(node)
+ close_stream
+ else
+ update_stream_id(node)
+ state.node(node)
+ end
+ rescue => e
+ error(e)
+ end
+
+ def enforce_rate_limit
+ unless @bucket.take(1)
+ raise StreamErrors::PolicyViolation.new('rate limit exceeded')
+ end
+ end
+
+ def log_node(node, direction)
+ return unless log.debug?
+ from, to = @remote_addr, @local_addr
+ from, to = to, from if direction == :out
+ label = (direction == :out) ? 'Sent' : 'Received'
+ log.debug("%s %21s -> %s\n%s\n" %
+ ["#{label} stanza:".ljust(PAD), from, to, node])
+ end
+
+ # Determine if this is a valid domain-only JID that can be used in
+ # stream initiation stanza headers.
+ #
+ # jid - The String or JID to verify (e.g. 'wonderland.lit').
+ #
+ # Return true if the jid is domain-only.
+ def valid_address?(jid)
+ JID.new(jid).domain? rescue false
+ end
+
+ def update_stream_id(id_or_node)
+ if id_or_node.is_a? String
+ @id = id_or_node.freeze
+ elsif Node.stream?(id_or_node) # move stream? method somewhere else?
+ @id = id_or_node['id'].freeze
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/client.rb b/lib/vines/stream/client.rb
new file mode 100644
index 0000000..ead9709
--- /dev/null
+++ b/lib/vines/stream/client.rb
@@ -0,0 +1,88 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ # Implements the XMPP protocol for client-to-server (c2s) streams. This
+ # serves connected streams using the jabber:client namespace.
+ class Client < Stream
+ MECHANISMS = %w[PLAIN].freeze
+
+ def initialize(config)
+ super
+ @session = Client::Session.new(self)
+ end
+
+ # Delegate behavior to the session that's storing our stream state.
+ def method_missing(name, *args)
+ @session.send(name, *args)
+ end
+
+ %w[advance domain state user user=].each do |name|
+ define_method name do |*args|
+ @session.send(name, *args)
+ end
+ end
+
+ %w[max_stanza_size max_resources_per_account].each do |name|
+ define_method name do |*args|
+ config[:client].send(name, *args)
+ end
+ end
+
+ # Return an array of allowed authentication mechanisms advertised as
+ # client stream features.
+ def authentication_mechanisms
+ MECHANISMS
+ end
+
+ def ssl_handshake_completed
+ if get_peer_cert
+ close_connection unless cert_domain_matches?(@session.domain)
+ end
+ end
+
+ def unbind
+ @session.unbind!(self)
+ super
+ end
+
+ def start(node)
+ to, from = %w[to from].map {|a| node[a] }
+ @session.domain = to unless @session.domain
+ send_stream_header(from)
+ raise StreamErrors::NotAuthorized if domain_change?(to)
+ raise StreamErrors::UnsupportedVersion unless node['version'] == '1.0'
+ raise StreamErrors::ImproperAddressing unless valid_address?(@session.domain)
+ raise StreamErrors::HostUnknown unless config.vhost?(@session.domain)
+ raise StreamErrors::InvalidNamespace unless node.namespaces['xmlns'] == NAMESPACES[:client]
+ raise StreamErrors::InvalidNamespace unless node.namespaces['xmlns:stream'] == NAMESPACES[:stream]
+ end
+
+ private
+
+ # The `to` domain address set on the initial stream header must not change
+ # during stream restarts. This prevents a user from authenticating in one
+ # domain, then using a stream in a different domain.
+ #
+ # to - The String domain JID to verify (e.g. 'wonderland.lit').
+ #
+ # Returns true if the client connection is misbehaving and should be closed.
+ def domain_change?(to)
+ to != @session.domain
+ end
+
+ def send_stream_header(to)
+ attrs = {
+ 'xmlns' => NAMESPACES[:client],
+ 'xmlns:stream' => NAMESPACES[:stream],
+ 'xml:lang' => 'en',
+ 'id' => Kit.uuid,
+ 'from' => @session.domain,
+ 'version' => '1.0'
+ }
+ attrs['to'] = to if to
+ write "<stream:stream %s>" % attrs.to_a.map{|k,v| "#{k}='#{v}'"}.join(' ')
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/client/auth.rb b/lib/vines/stream/client/auth.rb
new file mode 100644
index 0000000..cdc2419
--- /dev/null
+++ b/lib/vines/stream/client/auth.rb
@@ -0,0 +1,74 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Client
+ class Auth < State
+ NS = NAMESPACES[:sasl]
+ MECHANISM = 'mechanism'.freeze
+ AUTH = 'auth'.freeze
+ PLAIN = 'PLAIN'.freeze
+ EXTERNAL = 'EXTERNAL'.freeze
+ SUCCESS = %Q{<success xmlns="#{NS}"/>}.freeze
+ MAX_AUTH_ATTEMPTS = 3
+
+ def initialize(stream, success=BindRestart)
+ super
+ @attempts = 0
+ @sasl = SASL.new(stream)
+ end
+
+ def node(node)
+ raise StreamErrors::NotAuthorized unless auth?(node)
+ if node.text.empty?
+ send_auth_fail(SaslErrors::MalformedRequest.new)
+ elsif stream.authentication_mechanisms.include?(node[MECHANISM])
+ case node[MECHANISM]
+ when PLAIN then plain_auth(node)
+ when EXTERNAL then external_auth(node)
+ end
+ else
+ send_auth_fail(SaslErrors::InvalidMechanism.new)
+ end
+ end
+
+ private
+
+ def auth?(node)
+ node.name == AUTH && namespace(node) == NS
+ end
+
+ def plain_auth(node)
+ stream.user = @sasl.plain_auth(node.text)
+ send_auth_success
+ rescue => e
+ send_auth_fail(e)
+ end
+
+ def external_auth(node)
+ @sasl.external_auth(node.text)
+ send_auth_success
+ rescue => e
+ send_auth_fail(e)
+ stream.write('</stream:stream>')
+ stream.close_connection_after_writing
+ end
+
+ def send_auth_success
+ stream.write(SUCCESS)
+ stream.reset
+ advance
+ end
+
+ def send_auth_fail(condition)
+ @attempts += 1
+ if @attempts >= MAX_AUTH_ATTEMPTS
+ stream.error(StreamErrors::PolicyViolation.new("max authentication attempts exceeded"))
+ else
+ stream.error(condition)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/client/auth_restart.rb b/lib/vines/stream/client/auth_restart.rb
new file mode 100644
index 0000000..c53f53e
--- /dev/null
+++ b/lib/vines/stream/client/auth_restart.rb
@@ -0,0 +1,29 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Client
+ class AuthRestart < State
+ def initialize(stream, success=Auth)
+ super
+ end
+
+ def node(node)
+ raise StreamErrors::NotAuthorized unless stream?(node)
+ stream.start(node)
+ doc = Document.new
+ features = doc.create_element('stream:features') do |el|
+ el << doc.create_element('mechanisms') do |parent|
+ parent.default_namespace = NAMESPACES[:sasl]
+ stream.authentication_mechanisms.each do |name|
+ parent << doc.create_element('mechanism', name)
+ end
+ end
+ end
+ stream.write(features)
+ advance
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/client/bind.rb b/lib/vines/stream/client/bind.rb
new file mode 100644
index 0000000..c90a952
--- /dev/null
+++ b/lib/vines/stream/client/bind.rb
@@ -0,0 +1,64 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Client
+ class Bind < State
+ NS = NAMESPACES[:bind]
+ MAX_ATTEMPTS = 5
+
+ def initialize(stream, success=Ready)
+ super
+ @attempts = 0
+ end
+
+ def node(node)
+ @attempts += 1
+ raise StreamErrors::NotAuthorized unless bind?(node)
+ raise StreamErrors::PolicyViolation.new('max bind attempts reached') if @attempts > MAX_ATTEMPTS
+ raise StanzaErrors::ResourceConstraint.new(node, 'wait') if resource_limit_reached?
+
+ stream.bind!(resource(node))
+ doc = Document.new
+ result = doc.create_element('iq', 'id' => node['id'], 'type' => 'result') do |el|
+ el << doc.create_element('bind') do |bind|
+ bind.default_namespace = NS
+ bind << doc.create_element('jid', stream.user.jid.to_s)
+ end
+ end
+ stream.write(result)
+ advance
+ end
+
+ private
+
+ def bind?(node)
+ node.name == 'iq' && node['type'] == 'set' && node.xpath('ns:bind', 'ns' => NS).any?
+ end
+
+ def resource(node)
+ el = node.xpath('ns:bind/ns:resource', 'ns' => NS).first
+ resource = el ? el.text.strip : ''
+ generate = resource.empty? || !resource_valid?(resource) || resource_used?(resource)
+ generate ? Kit.uuid : resource
+ end
+
+ def resource_limit_reached?
+ used = stream.connected_resources(stream.user.jid.bare).size
+ used >= stream.max_resources_per_account
+ end
+
+ def resource_used?(resource)
+ stream.available_resources(stream.user.jid).any? do |c|
+ c.user.jid.resource == resource
+ end
+ end
+
+ def resource_valid?(resource)
+ jid = stream.user.jid
+ JID.new(jid.node, jid.domain, resource) rescue false
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/client/bind_restart.rb b/lib/vines/stream/client/bind_restart.rb
new file mode 100644
index 0000000..3aa6445
--- /dev/null
+++ b/lib/vines/stream/client/bind_restart.rb
@@ -0,0 +1,30 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Client
+ class BindRestart < State
+ def initialize(stream, success=Bind)
+ super
+ end
+
+ def node(node)
+ raise StreamErrors::NotAuthorized unless stream?(node)
+ stream.start(node)
+ doc = Document.new
+ features = doc.create_element('stream:features') do |el|
+ # Session support is deprecated, but like we do it for Adium
+ # in the iq-session-stanza we have to serve the feature for Xabber.
+ # Otherwise it will disconnect after authentication!
+ el << doc.create_element('session', 'xmlns' => NAMESPACES[:session]) do |session|
+ session << doc.create_element('optional')
+ end
+ el << doc.create_element('bind', 'xmlns' => NAMESPACES[:bind])
+ end
+ stream.write(features)
+ advance
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/client/closed.rb b/lib/vines/stream/client/closed.rb
new file mode 100644
index 0000000..3882ecf
--- /dev/null
+++ b/lib/vines/stream/client/closed.rb
@@ -0,0 +1,13 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Client
+ class Closed < State
+ def node(node)
+ # ignore data received after close_connection
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/client/ready.rb b/lib/vines/stream/client/ready.rb
new file mode 100644
index 0000000..8dd5d82
--- /dev/null
+++ b/lib/vines/stream/client/ready.rb
@@ -0,0 +1,17 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Client
+ class Ready < State
+ def node(node)
+ stanza = to_stanza(node)
+ raise StreamErrors::UnsupportedStanzaType unless stanza
+ stanza.validate_to
+ stanza.validate_from
+ stanza.process
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/client/session.rb b/lib/vines/stream/client/session.rb
new file mode 100644
index 0000000..ac8456b
--- /dev/null
+++ b/lib/vines/stream/client/session.rb
@@ -0,0 +1,210 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Client
+ # A Session tracks the state of a client stream over its lifetime from
+ # negotiation to processing stanzas to shutdown. By disconnecting the
+ # stream's state from the stream, we can allow multiple TCP connections
+ # to access one logical session (e.g. HTTP streams).
+ class Session
+ include Comparable
+
+ attr_accessor :domain, :user
+ attr_reader :id, :last_broadcast_presence, :state
+
+ def initialize(stream)
+ @stream = stream
+ @id = Kit.uuid
+ @config = stream.config
+ @state = Client::Start.new(stream)
+ @available = false
+ @domain = nil
+ @last_broadcast_presence = nil
+ @requested_roster = false
+ @unbound = false
+ @user = nil
+ end
+
+ def <=>(session)
+ session.is_a?(Session) ? self.id <=> session.id : nil
+ end
+
+ alias :eql? :==
+
+ def hash
+ @id.hash
+ end
+
+ def advance(state)
+ @state = state
+ end
+
+ # Returns true if this client has properly authenticated with
+ # the server.
+ def authenticated?
+ !@user.nil?
+ end
+
+ # Notify the session that the client has sent an initial presence
+ # broadcast and is now considered to be an "available" resource.
+ # Available resources are sent presence subscription stanzas.
+ def available!
+ @available = true
+ save_to_cluster
+ end
+
+ # An available resource has sent initial presence and can
+ # receive presence subscription requests.
+ def available?
+ @available && connected?
+ end
+
+ # Complete resource binding with the given resource name, provided by the
+ # client or generated by the server. Once resource binding is completed,
+ # the stream is considered to be "connected" and ready for traffic.
+ def bind!(resource)
+ @user.jid.resource = resource
+ router << self
+ save_to_cluster
+ end
+
+ # A connected resource has authenticated and bound a resource
+ # identifier.
+ def connected?
+ !@unbound && authenticated? && !@user.jid.bare?
+ end
+
+ # An interested resource has requested its roster and can
+ # receive roster pushes.
+ def interested?
+ @requested_roster && connected?
+ end
+
+ def last_broadcast_presence=(node)
+ @last_broadcast_presence = node
+ save_to_cluster
+ end
+
+ def ready?
+ @state.class == Client::Ready
+ end
+
+ # Notify the session that the client has requested its roster and is now
+ # considered to be an "interested" resource. Interested resources are sent
+ # roster pushes when changes are made to their contacts.
+ def requested_roster!
+ @requested_roster = true
+ save_to_cluster
+ end
+
+ def stream_type
+ :client
+ end
+
+ def write(data)
+ @stream.write(data)
+ end
+
+ # Called by the stream when it's disconnected from the client. The stream
+ # passes itself to this method in case multiple streams are accessing this
+ # session (e.g. BOSH/HTTP).
+ def unbind!(stream)
+ router.delete(self)
+ delete_from_cluster
+ unsubscribe_pubsub
+ @unbound = true
+ @available = false
+ broadcast_unavailable
+ end
+
+ # Returns streams for available resources to which this user
+ # has successfully subscribed.
+ def available_subscribed_to_resources
+ subscribed = @user.subscribed_to_contacts.map {|c| c.jid }
+ router.available_resources(subscribed, @user.jid)
+ end
+
+ # Returns streams for available resources that are subscribed
+ # to this user's presence updates.
+ def available_subscribers
+ subscribed = @user.subscribed_from_contacts.map {|c| c.jid }
+ router.available_resources(subscribed, @user.jid)
+ end
+
+ # Returns contacts hosted at remote servers to which this user has
+ # successfully subscribed.
+ def remote_subscribed_to_contacts
+ @user.subscribed_to_contacts.reject do |c|
+ @config.local_jid?(c.jid)
+ end
+ end
+
+ # Returns contacts hosted at remote servers that are subscribed
+ # to this user's presence updates.
+ def remote_subscribers(to=nil)
+ jid = (to.nil? || to.empty?) ? nil : JID.new(to).bare
+ @user.subscribed_from_contacts.reject do |c|
+ @config.local_jid?(c.jid) || (jid && c.jid.bare != jid)
+ end
+ end
+
+ private
+
+ def broadcast_unavailable
+ return unless authenticated?
+ Fiber.new do
+ broadcast(unavailable, available_subscribers)
+ broadcast(unavailable, router.available_resources(@user.jid, @user.jid))
+ remote_subscribers.each do |contact|
+ node = unavailable
+ node['to'] = contact.jid.bare.to_s
+ router.route(node) rescue nil # ignore RemoteServerNotFound
+ end
+ end.resume
+ end
+
+ def unavailable
+ doc = Nokogiri::XML::Document.new
+ doc.create_element('presence',
+ 'from' => @user.jid.to_s,
+ 'type' => 'unavailable')
+ end
+
+ def broadcast(stanza, recipients)
+ recipients.each do |recipient|
+ stanza['to'] = recipient.user.jid.to_s
+ recipient.write(stanza)
+ end
+ end
+
+ def router
+ @config.router
+ end
+
+ def save_to_cluster
+ if @config.cluster?
+ @config.cluster.save_session(@user.jid, to_hash)
+ end
+ end
+
+ def delete_from_cluster
+ if connected? && @config.cluster?
+ @config.cluster.delete_session(@user.jid)
+ end
+ end
+
+ def unsubscribe_pubsub
+ if connected?
+ @config.vhost(@user.jid.domain).unsubscribe_pubsub(@user.jid)
+ end
+ end
+
+ def to_hash
+ presence = @last_broadcast_presence ? @last_broadcast_presence.to_s : nil
+ {available: @available, interested: @requested_roster, presence: presence.to_s}
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/client/start.rb b/lib/vines/stream/client/start.rb
new file mode 100644
index 0000000..6f18c30
--- /dev/null
+++ b/lib/vines/stream/client/start.rb
@@ -0,0 +1,27 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Client
+ class Start < State
+ def initialize(stream, success=TLS)
+ super
+ end
+
+ def node(node)
+ raise StreamErrors::NotAuthorized unless stream?(node)
+ stream.start(node)
+ doc = Document.new
+ features = doc.create_element('stream:features') do |el|
+ el << doc.create_element('starttls') do |tls|
+ tls.default_namespace = NAMESPACES[:tls]
+ tls << doc.create_element('required')
+ end
+ end
+ stream.write(features)
+ advance
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/client/tls.rb b/lib/vines/stream/client/tls.rb
new file mode 100644
index 0000000..bc9c371
--- /dev/null
+++ b/lib/vines/stream/client/tls.rb
@@ -0,0 +1,38 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Client
+ class TLS < State
+ NS = NAMESPACES[:tls]
+ PROCEED = %Q{<proceed xmlns="#{NS}"/>}.freeze
+ FAILURE = %Q{<failure xmlns="#{NS}"/>}.freeze
+ STARTTLS = 'starttls'.freeze
+
+ def initialize(stream, success=AuthRestart)
+ super
+ end
+
+ def node(node)
+ raise StreamErrors::NotAuthorized unless starttls?(node)
+ if stream.encrypt?
+ stream.write(PROCEED)
+ stream.encrypt
+ stream.reset
+ advance
+ else
+ stream.write(FAILURE)
+ stream.write('</stream:stream>')
+ stream.close_connection_after_writing
+ end
+ end
+
+ private
+
+ def starttls?(node)
+ node.name == STARTTLS && namespace(node) == NS
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/component.rb b/lib/vines/stream/component.rb
new file mode 100644
index 0000000..9978483
--- /dev/null
+++ b/lib/vines/stream/component.rb
@@ -0,0 +1,58 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+
+ # Implements the XMPP protocol for trusted, external component (XEP-0114)
+ # streams. This serves connected streams using the jabber:component:accept
+ # namespace.
+ class Component < Stream
+ attr_reader :remote_domain
+
+ def initialize(config)
+ super
+ @remote_domain = nil
+ @stream_id = Kit.uuid
+ advance(Start.new(self))
+ end
+
+ def max_stanza_size
+ config[:component].max_stanza_size
+ end
+
+ def ready?
+ state.class == Component::Ready
+ end
+
+ def stream_type
+ :component
+ end
+
+ def start(node)
+ @remote_domain = node['to']
+ send_stream_header
+ raise StreamErrors::ImproperAddressing unless valid_address?(@remote_domain)
+ raise StreamErrors::HostUnknown unless config.component?(@remote_domain)
+ raise StreamErrors::InvalidNamespace unless node.namespaces['xmlns'] == NAMESPACES[:component]
+ raise StreamErrors::InvalidNamespace unless node.namespaces['xmlns:stream'] == NAMESPACES[:stream]
+ end
+
+ def secret
+ password = config.component_password(@remote_domain)
+ Digest::SHA1.hexdigest(@stream_id + password)
+ end
+
+ private
+
+ def send_stream_header
+ attrs = {
+ 'xmlns' => NAMESPACES[:component],
+ 'xmlns:stream' => NAMESPACES[:stream],
+ 'id' => @stream_id,
+ 'from' => @remote_domain
+ }
+ write "<stream:stream %s>" % attrs.to_a.map{|k,v| "#{k}='#{v}'"}.join(' ')
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/component/handshake.rb b/lib/vines/stream/component/handshake.rb
new file mode 100644
index 0000000..93948cc
--- /dev/null
+++ b/lib/vines/stream/component/handshake.rb
@@ -0,0 +1,26 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Component
+ class Handshake < State
+ def initialize(stream, success=Ready)
+ super
+ end
+
+ def node(node)
+ raise StreamErrors::NotAuthorized unless handshake?(node)
+ stream.write('<handshake/>')
+ stream.router << stream
+ advance
+ end
+
+ private
+
+ def handshake?(node)
+ node.name == 'handshake' && node.text == stream.secret
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/component/ready.rb b/lib/vines/stream/component/ready.rb
new file mode 100644
index 0000000..fe2eac9
--- /dev/null
+++ b/lib/vines/stream/component/ready.rb
@@ -0,0 +1,23 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Component
+ class Ready < State
+ def node(node)
+ stanza = to_stanza(node)
+ raise StreamErrors::UnsupportedStanzaType unless stanza
+ to, from = stanza.validate_to, stanza.validate_from
+ raise StreamErrors::ImproperAddressing unless to && from
+ raise StreamErrors::InvalidFrom unless from.domain == stream.remote_domain
+ stream.user = User.new(jid: from)
+ if stanza.local? || stanza.to_pubsub_domain?
+ stanza.process
+ else
+ stanza.route
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/component/start.rb b/lib/vines/stream/component/start.rb
new file mode 100644
index 0000000..a948623
--- /dev/null
+++ b/lib/vines/stream/component/start.rb
@@ -0,0 +1,19 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Component
+ class Start < State
+ def initialize(stream, success=Handshake)
+ super
+ end
+
+ def node(node)
+ raise StreamErrors::NotAuthorized unless stream?(node)
+ stream.start(node)
+ advance
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/http.rb b/lib/vines/stream/http.rb
new file mode 100644
index 0000000..f3b4bb9
--- /dev/null
+++ b/lib/vines/stream/http.rb
@@ -0,0 +1,185 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Http < Client
+ attr_accessor :session
+
+ def initialize(config)
+ super
+ @session = Http::Session.new(self)
+ end
+
+ # Override Stream#create_parser to provide an HTTP parser rather than
+ # a Nokogiri XML parser.
+ #
+ # Returns nothing.
+ def create_parser
+ @parser = ::Http::Parser.new.tap do |parser|
+ body = ''
+ parser.on_body = proc {|data| body << data }
+ parser.on_message_complete = proc do
+ process_request(Request.new(self, @parser, body))
+ body = ''
+ end
+ end
+ end
+
+ # If the session ID is valid, switch this stream's session to the new
+ # ID and return true. Some clients, like Google Chrome, reuse one stream
+ # for multiple sessions.
+ #
+ # sid - The String session ID.
+ #
+ # Returns true if the server previously distributed this SID to a client.
+ def valid_session?(sid)
+ if session = Sessions[sid]
+ @session = session
+ end
+ !!session
+ end
+
+ %w[max_stanza_size max_resources_per_account bind root].each do |name|
+ define_method name do |*args|
+ config[:http].send(name, *args)
+ end
+ end
+
+ def process_request(request)
+ if request.method == 'POST'
+ if request.path == self.bind && request.options?
+ request.reply_to_options
+ elsif request.path == self.bind
+ body = Nokogiri::XML(request.body).root
+ if session = Sessions[body['sid']]
+ @session = session
+ else
+ @session = Http::Session.new(self)
+ end
+ @session.request(request)
+ @nodes.push(body)
+ end
+ else
+ request.reply('It works!', 'text/plain')
+ end
+ end
+
+ # Alias the Stream#write method before overriding it so we can call
+ # it later from a Session instance.
+ alias :stream_write :write
+
+ # Override Stream#write to queue stanzas rather than immediately writing
+ # to the stream. Stanza responses must be paired with a queued request.
+ #
+ # If a request is not waiting, the written stanzas will buffer until they
+ # can be sent in the next response.
+ #
+ # data - The XML String or XML::Node to write to the HTTP socket.
+ #
+ # Returns nothing.
+ def write(data)
+ @session.write(data)
+ end
+
+ # Parse the one or more stanzas from a single body element. BOSH clients
+ # buffer stanzas sent in quick succession, and send them as a bundle, to
+ # save on the request/response cycle.
+ #
+ # TODO This parses the XML again just to strip namespaces. Figure out
+ # Nokogiri namespace handling instead.
+ #
+ # body - The XML::Node containing the BOSH `body` element.
+ #
+ # Returns an Array of XML::Node stanzas.
+ def parse_body(body)
+ body.namespace = nil
+ body.elements.map do |node|
+ Nokogiri::XML(node.to_s.sub(' xmlns="jabber:client"', '')).root
+ end
+ end
+
+ def start(node)
+ domain, type, hold, wait, rid = %w[to content hold wait rid].map {|a| (node[a] || '').strip }
+ version = node.attribute_with_ns('version', NAMESPACES[:bosh]).value rescue nil
+
+ @session.inactivity = 20
+ @session.domain = domain
+ @session.content_type = type unless type.empty?
+ @session.hold = hold.to_i unless hold.empty?
+ @session.wait = wait.to_i unless wait.empty?
+
+ raise StreamErrors::UndefinedCondition.new('rid required') if rid.empty?
+ raise StreamErrors::UnsupportedVersion unless version == '1.0'
+ raise StreamErrors::ImproperAddressing unless valid_address?(domain)
+ raise StreamErrors::HostUnknown unless config.vhost?(domain)
+ raise StreamErrors::InvalidNamespace unless node.namespaces['xmlns'] == NAMESPACES[:http_bind]
+
+ Sessions[@session.id] = @session
+ send_stream_header
+ end
+
+ def terminate
+ doc = Nokogiri::XML::Document.new
+ node = doc.create_element('body',
+ 'type' => 'terminate',
+ 'xmlns' => NAMESPACES[:http_bind])
+ @session.reply(node)
+ close_stream
+ end
+
+ private
+
+ def send_stream_header
+ doc = Nokogiri::XML::Document.new
+ node = doc.create_element('body',
+ 'charsets' => 'UTF-8',
+ 'from' => @session.domain,
+ 'hold' => @session.hold,
+ 'inactivity' => @session.inactivity,
+ 'polling' => '5',
+ 'requests' => '2',
+ 'sid' => @session.id,
+ 'ver' => '1.6',
+ 'wait' => @session.wait,
+ 'xmpp:version' => '1.0',
+ 'xmlns' => NAMESPACES[:http_bind],
+ 'xmlns:xmpp' => NAMESPACES[:bosh],
+ 'xmlns:stream' => NAMESPACES[:stream])
+
+ node << doc.create_element('stream:features') do |el|
+ el << doc.create_element('mechanisms') do |mechanisms|
+ mechanisms.default_namespace = NAMESPACES[:sasl]
+ mechanisms << doc.create_element('mechanism', 'PLAIN')
+ end
+ end
+ @session.reply(node)
+ end
+
+ # Override Stream#send_stream_error to wrap the error XML in a BOSH
+ # terminate body tag.
+ #
+ # e - The StreamError that caused the connection to close.
+ #
+ # Returns nothing.
+ def send_stream_error(e)
+ doc = Nokogiri::XML::Document.new
+ node = doc.create_element('body',
+ 'condition' => 'remote-stream-error',
+ 'type' => 'terminate',
+ 'xmlns' => NAMESPACES[:http_bind],
+ 'xmlns:stream' => NAMESPACES[:stream])
+ node.inner_html = e.to_xml
+ @session.reply(node)
+ end
+
+ # Override Stream#close_stream to simply close the connection without
+ # writing a closing stream tag.
+ #
+ # Returns nothing.
+ def close_stream
+ close_connection_after_writing
+ @session.close
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/http/auth.rb b/lib/vines/stream/http/auth.rb
new file mode 100644
index 0000000..cbf1aa7
--- /dev/null
+++ b/lib/vines/stream/http/auth.rb
@@ -0,0 +1,22 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Http
+ class Auth < Client::Auth
+ def initialize(stream, success=BindRestart)
+ super
+ end
+
+ def node(node)
+ unless stream.valid_session?(node['sid']) && body?(node) && node['rid']
+ raise StreamErrors::NotAuthorized
+ end
+ nodes = stream.parse_body(node)
+ raise StreamErrors::NotAuthorized unless nodes.size == 1
+ super(nodes.first)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/http/bind.rb b/lib/vines/stream/http/bind.rb
new file mode 100644
index 0000000..8c6a4a7
--- /dev/null
+++ b/lib/vines/stream/http/bind.rb
@@ -0,0 +1,32 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Http
+ class Bind < Client::Bind
+ FEATURES = %Q{<stream:features xmlns:stream="#{NAMESPACES[:stream]}"/>}.freeze
+
+ def initialize(stream, success=Ready)
+ super
+ end
+
+ def node(node)
+ unless stream.valid_session?(node['sid']) && body?(node) && node['rid']
+ raise StreamErrors::NotAuthorized
+ end
+ nodes = stream.parse_body(node)
+ raise StreamErrors::NotAuthorized unless nodes.size == 1
+ super(nodes.first)
+ end
+
+ private
+
+ # Override Client::Bind#send_empty_features to properly namespace the
+ # empty features element.
+ def send_empty_features
+ stream.write(FEATURES)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/http/bind_restart.rb b/lib/vines/stream/http/bind_restart.rb
new file mode 100644
index 0000000..7e73073
--- /dev/null
+++ b/lib/vines/stream/http/bind_restart.rb
@@ -0,0 +1,37 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Http
+ class BindRestart < State
+ def initialize(stream, success=Bind)
+ super
+ end
+
+ def node(node)
+ raise StreamErrors::NotAuthorized unless restart?(node)
+
+ doc = Document.new
+ body = doc.create_element('body') do |el|
+ el.add_namespace(nil, NAMESPACES[:http_bind])
+ el.add_namespace('stream', NAMESPACES[:stream])
+ el << doc.create_element('stream:features') do |features|
+ features << doc.create_element('bind', 'xmlns' => NAMESPACES[:bind])
+ end
+ end
+ stream.reply(body)
+ advance
+ end
+
+ private
+
+ def restart?(node)
+ session = stream.valid_session?(node['sid'])
+ restart = node.attribute_with_ns('restart', NAMESPACES[:bosh]).value rescue nil
+ domain = node['to'] == stream.domain
+ session && body?(node) && domain && restart == 'true' && node['rid']
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/http/ready.rb b/lib/vines/stream/http/ready.rb
new file mode 100644
index 0000000..c379319
--- /dev/null
+++ b/lib/vines/stream/http/ready.rb
@@ -0,0 +1,29 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Http
+ class Ready < Client::Ready
+ RID, SID, TYPE, TERMINATE = %w[rid sid type terminate].map {|s| s.freeze }
+
+ def node(node)
+ unless stream.valid_session?(node[SID]) && body?(node) && node[RID]
+ raise StreamErrors::NotAuthorized
+ end
+ stream.parse_body(node).each do |child|
+ begin
+ super(child)
+ rescue StanzaError => e
+ stream.error(e)
+ end
+ end
+ stream.terminate if terminate?(node)
+ end
+
+ def terminate?(node)
+ node[TYPE] == TERMINATE
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/http/request.rb b/lib/vines/stream/http/request.rb
new file mode 100644
index 0000000..70f02f3
--- /dev/null
+++ b/lib/vines/stream/http/request.rb
@@ -0,0 +1,193 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Http
+ class Request
+ BUF_SIZE = 1024
+ MODIFIED = '%a, %d %b %Y %H:%M:%S GMT'.freeze
+ MOVED = 'Moved Permanently'.freeze
+ NOT_FOUND = 'Not Found'.freeze
+ NOT_MODIFIED = 'Not Modified'.freeze
+ IF_MODIFIED = 'If-Modified-Since'.freeze
+ TEXT_PLAIN = 'text/plain'.freeze
+ OPTIONS = 'OPTIONS'.freeze
+ CONTENT_TYPES = {
+ 'html' => 'text/html; charset="utf-8"',
+ 'js' => 'application/javascript; charset="utf-8"',
+ 'css' => 'text/css',
+ 'png' => 'image/png',
+ 'jpg' => 'image/jpeg',
+ 'jpeg' => 'image/jpeg',
+ 'gif' => 'image/gif',
+ 'manifest' => 'text/cache-manifest'
+ }.freeze
+
+ attr_reader :stream, :body, :headers, :method, :path, :url, :query
+
+ # Create a new request parsed from an HTTP client connection. We'll try
+ # to keep this request open until there are stanzas available to send
+ # as a response.
+ #
+ # stream - The Stream::Http client connection that received the request.
+ # parser - The Http::Parser that parsed the HTTP request.
+ # body - The String request body.
+ def initialize(stream, parser, body)
+ uri = URI(parser.request_url)
+ @stream = stream
+ @body = body
+ @headers = parser.headers
+ @method = parser.http_method
+ @url = parser.request_url
+ @path = uri.path
+ @query = uri.query
+ @received = Time.now
+ end
+
+ # Return the number of seconds since this request was received.
+ def age
+ Time.now - @received
+ end
+
+ # Write the requested file to the client out of the given document root
+ # directory. Take care to prevent directory traversal attacks with paths
+ # like ../../../etc/passwd. Use the If-Modified-Since request header
+ # to implement caching.
+ #
+ # Returns nothing.
+ def reply_with_file(dir)
+ path = File.expand_path(File.join(dir, @path))
+
+ # Redirect requests missing a slash so relative links work.
+ if File.directory?(path) && !@path.end_with?('/')
+ send_status(301, MOVED, "Location: #{redirect_uri}")
+ return
+ end
+
+ path = File.join(path, 'index.html') if File.directory?(path)
+
+ if path.start_with?(dir) && File.exist?(path)
+ modified?(path) ? send_file(path) : send_status(304, NOT_MODIFIED)
+ else
+ missing = File.join(dir, '404.html')
+ if File.exist?(missing)
+ send_file(missing, 404, NOT_FOUND)
+ else
+ send_status(404, NOT_FOUND)
+ end
+ end
+ end
+
+ # Send an HTTP 200 OK response wrapping the XMPP node content back
+ # to the client.
+ #
+ # Returns nothing.
+ def reply(node, content_type)
+ body = node.to_s
+ header = [
+ "HTTP/1.1 200 OK",
+ "Access-Control-Allow-Origin: *",
+ "Content-Type: #{content_type}",
+ "Content-Length: #{body.bytesize}",
+ vroute_cookie
+ ].compact.join("\r\n")
+ @stream.stream_write([header, body].join("\r\n\r\n"))
+ end
+
+ # Return true if the request method is OPTIONS, signaling a
+ # CORS preflight check.
+ def options?
+ @method == OPTIONS
+ end
+
+ # Send a 200 OK response, allowing any origin domain to connect to the
+ # server, in response to CORS preflight OPTIONS requests. This allows
+ # any web application using strophe.js to connect to our BOSH port.
+ #
+ # Returns nothing.
+ def reply_to_options
+ allow = @headers['Access-Control-Request-Headers']
+ headers = [
+ "Access-Control-Allow-Origin: *",
+ "Access-Control-Allow-Methods: POST, GET, OPTIONS",
+ "Access-Control-Allow-Headers: #{allow}",
+ "Access-Control-Max-Age: #{60 * 60 * 24 * 30}"
+ ]
+ send_status(200, 'OK', headers)
+ end
+
+ private
+
+ # Attempt to rebuild the full request URI from the Host header. If it
+ # wasn't sent by the client, just return the relative path that
+ # was requested. The Location response header must contain the fully
+ # qualified URI, but most browsers will accept relative paths as well.
+ #
+ # Returns the String URL.
+ def redirect_uri
+ host = headers['Host']
+ uri = "#{path}/"
+ uri = "#{uri}?#{query}" unless (query || '').empty?
+ uri = "http://#{host}#{uri}" if host
+ uri
+ end
+
+ # Return true if the file has been modified since the client last
+ # requested it with the If-Modified-Since header.
+ def modified?(path)
+ @headers[IF_MODIFIED] != mtime(path)
+ end
+
+ def mtime(path)
+ File.mtime(path).utc.strftime(MODIFIED)
+ end
+
+ def send_status(status, message, *headers)
+ header = [
+ "HTTP/1.1 #{status} #{message}",
+ "Content-Length: 0",
+ *headers
+ ].join("\r\n")
+ @stream.stream_write("#{header}\r\n\r\n")
+ end
+
+ # Stream the contents of the file to the client in a 200 OK response.
+ # Send a Last-Modified response header so clients can send us an
+ # If-Modified-Since request header for caching.
+ #
+ # Returns nothing.
+ def send_file(path, status=200, message='OK')
+ header = [
+ "HTTP/1.1 #{status} #{message}",
+ "Content-Type: #{content_type(path)}",
+ "Content-Length: #{File.size(path)}",
+ "Last-Modified: #{mtime(path)}"
+ ].join("\r\n")
+ @stream.stream_write("#{header}\r\n\r\n")
+
+ File.open(path) do |file|
+ while (buf = file.read(BUF_SIZE)) != nil
+ @stream.stream_write(buf)
+ end
+ end
+ end
+
+ def content_type(path)
+ ext = File.extname(path).sub('.', '')
+ CONTENT_TYPES[ext] || TEXT_PLAIN
+ end
+
+ # Provide a vroute cookie in each response that uniquely identifies this
+ # HTTP server. Reverse proxy servers (nginx/apache) can use this cookie
+ # to implement sticky sessions. Return nil if vroute was not set in
+ # config.rb and no cookie should be sent.
+ #
+ # Returns a String cookie value or nil if disabled.
+ def vroute_cookie
+ route = @stream.config[:http].vroute
+ route ? "Set-Cookie: vroute=#{route}; path=/; HttpOnly" : nil
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/http/session.rb b/lib/vines/stream/http/session.rb
new file mode 100644
index 0000000..a697900
--- /dev/null
+++ b/lib/vines/stream/http/session.rb
@@ -0,0 +1,128 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Http
+ class Session < Client::Session
+ include Nokogiri::XML
+
+ attr_accessor :content_type, :hold, :inactivity, :wait
+
+ CONTENT_TYPE = 'text/xml; charset=utf-8'.freeze
+
+ def initialize(stream)
+ super
+ @state = Http::Start.new(stream)
+ @inactivity, @wait, @hold = 20, 60, 1
+ @replied = Time.now
+ @requests, @responses = [], []
+ @content_type = CONTENT_TYPE
+ end
+
+ def close
+ Sessions.delete(@id)
+ router.delete(self)
+ delete_from_cluster
+ unsubscribe_pubsub
+ @requests.each {|req| req.stream.close_connection }
+ @requests.clear
+ @responses.clear
+ @state = Client::Closed.new(nil)
+ @unbound = true
+ @available = false
+ broadcast_unavailable
+ end
+
+ def ready?
+ @state.class == Http::Ready
+ end
+
+ def requests
+ @requests.clone
+ end
+
+ def expired?
+ respond_to_expired_requests
+ @requests.empty? && (Time.now - @replied > @inactivity)
+ end
+
+ # Resume this session from its most recent state with a new client
+ # stream and incoming node.
+ def resume(stream, node)
+ stream.session.requests.each do |req|
+ request(req)
+ end
+ stream.session = self
+ @state.stream = stream
+ @state.node(node)
+ end
+
+ def request(request)
+ if @responses.any?
+ request.reply(wrap_body(@responses.join), @content_type)
+ @replied = Time.now
+ @responses.clear
+ else
+ while @requests.size >= @hold
+ @requests.shift.reply(wrap_body(''), @content_type)
+ @replied = Time.now
+ end
+ @requests << request
+ end
+ end
+
+ # Send an HTTP 200 OK response wrapping the XMPP node content back
+ # to the client.
+ #
+ # node - The XML::Node to send to the client.
+ #
+ # Returns nothing.
+ def reply(node)
+ if request = @requests.shift
+ request.reply(node, @content_type)
+ @replied = Time.now
+ end
+ end
+
+ # Write the XMPP node to the client stream after wrapping it in a BOSH
+ # body tag. If there's a waiting request, the node is written
+ # immediately. If not, it's queued until the next request arrives.
+ #
+ # data - The XML String or XML::Node to send in the next HTTP response.
+ #
+ # Returns nothing.
+ def write(node)
+ if request = @requests.shift
+ request.reply(wrap_body(node), @content_type)
+ @replied = Time.now
+ else
+ @responses << node.to_s
+ end
+ end
+
+ def unbind!(stream)
+ @requests.reject! {|req| req.stream == stream }
+ end
+
+ private
+
+ def respond_to_expired_requests
+ expired = @requests.select {|req| req.age > @wait }
+ expired.each do |request|
+ request.reply(wrap_body(''), @content_type)
+ @requests.delete(request)
+ @replied = Time.now
+ end
+ end
+
+ def wrap_body(data)
+ doc = Document.new
+ doc.create_element('body') do |node|
+ node.add_namespace(nil, NAMESPACES[:http_bind])
+ node.inner_html = data.to_s
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/http/sessions.rb b/lib/vines/stream/http/sessions.rb
new file mode 100644
index 0000000..d31f8fe
--- /dev/null
+++ b/lib/vines/stream/http/sessions.rb
@@ -0,0 +1,65 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Http
+ # Sessions is a cache of Http::Session objects for transient HTTP
+ # connections. The cache is monitored for expired client connections.
+ class Sessions
+ include Vines::Log
+
+ @@instance = nil
+ def self.instance
+ @@instance ||= self.new
+ end
+
+ def self.[](sid)
+ instance[sid]
+ end
+
+ def self.[]=(sid, session)
+ instance[sid] = session
+ end
+
+ def self.delete(sid)
+ instance.delete(sid)
+ end
+
+ def initialize
+ @sessions = {}
+ start_timer
+ end
+
+ def []=(sid, session)
+ @sessions[sid] = session
+ end
+
+ def [](sid)
+ @sessions[sid]
+ end
+
+ def delete(sid)
+ @sessions.delete(sid)
+ end
+
+ private
+
+ # Check for expired clients to cleanup every second.
+ def start_timer
+ @timer ||= EventMachine::PeriodicTimer.new(1) { cleanup }
+ end
+
+ # Remove cached information for all expired connections. An expired
+ # HTTP client is one that has no queued requests and has had no activity
+ # for over 20 seconds.
+ def cleanup
+ @sessions.each_value do |session|
+ session.close if session.expired?
+ end
+ rescue => e
+ log.error("Expired session cleanup failed: #{e}")
+ end
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/lib/vines/stream/http/start.rb b/lib/vines/stream/http/start.rb
new file mode 100644
index 0000000..8db2622
--- /dev/null
+++ b/lib/vines/stream/http/start.rb
@@ -0,0 +1,23 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Http
+ class Start < State
+ def initialize(stream, success=Auth)
+ super
+ end
+
+ def node(node)
+ raise StreamErrors::NotAuthorized unless body?(node)
+ if session = Sessions[node['sid']]
+ session.resume(stream, node)
+ else
+ stream.start(node)
+ advance
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/parser.rb b/lib/vines/stream/parser.rb
new file mode 100644
index 0000000..a59aded
--- /dev/null
+++ b/lib/vines/stream/parser.rb
@@ -0,0 +1,79 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Parser < Nokogiri::XML::SAX::Document
+ include Nokogiri::XML
+ STREAM_NAME = 'stream'.freeze
+ STREAM_URI = 'http://etherx.jabber.org/streams'.freeze
+ IGNORE = NAMESPACES.values_at(:client, :component, :server)
+
+ def initialize(&block)
+ @listeners, @node = Hash.new {|h, k| h[k] = []}, nil
+ @parser = Nokogiri::XML::SAX::PushParser.new(self)
+ instance_eval(&block) if block
+ end
+
+ [:stream_open, :stream_close, :stanza].each do |name|
+ define_method(name) do |&block|
+ @listeners[name] << block
+ end
+ end
+
+ def <<(data)
+ @parser << data
+ self
+ end
+
+ def start_element_namespace(name, attrs=[], prefix=nil, uri=nil, ns=[])
+ el = node(name, attrs, prefix, uri, ns)
+ if stream?(name, uri)
+ notify(:stream_open, el)
+ else
+ @node << el if @node
+ @node = el
+ end
+ end
+
+ def end_element_namespace(name, prefix=nil, uri=nil)
+ if stream?(name, uri)
+ notify(:stream_close)
+ elsif @node.parent != @node.document
+ @node = @node.parent
+ else
+ notify(:stanza, @node)
+ @node = nil
+ end
+ end
+
+ def characters(chars)
+ @node << Text.new(chars, @node.document) if @node
+ end
+ alias :cdata_block :characters
+
+ private
+
+ def notify(msg, node=nil)
+ @listeners[msg].each do |b|
+ (node ? b.call(node) : b.call) rescue nil
+ end
+ end
+
+ def stream?(name, uri)
+ name == STREAM_NAME && uri == STREAM_URI
+ end
+
+ def node(name, attrs=[], prefix=nil, uri=nil, ns=[])
+ ignore = stream?(name, uri) ? [] : IGNORE
+ doc = @node ? @node.document : Document.new
+ node = doc.create_element(name) do |node|
+ attrs.each {|attr| node[attr.localname] = attr.value }
+ ns.each {|prefix, uri| node.add_namespace(prefix, uri) unless ignore.include?(uri) }
+ doc << node unless @node
+ end
+ node.namespace = node.add_namespace(prefix, uri) unless ignore.include?(uri)
+ node
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/sasl.rb b/lib/vines/stream/sasl.rb
new file mode 100644
index 0000000..2f46d02
--- /dev/null
+++ b/lib/vines/stream/sasl.rb
@@ -0,0 +1,128 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ # Provides plain (username/password) and external (TLS certificate) SASL
+ # authentication to client and server streams.
+ class SASL
+ include Vines::Log
+ EMPTY = '='.freeze
+
+ def initialize(stream)
+ @stream = stream
+ end
+
+ # Authenticate server-to-server streams, comparing their domain to their
+ # SSL certificate.
+ #
+ # http://xmpp.org/extensions/xep-0178.html#s2s
+ #
+ # encoded - The Base64 encoded remote domain name String sent by the
+ # server stream.
+ #
+ # Returns true if the Base64 encoded domain matches the TLS certificate
+ # presented earlier in stream negotiation.
+ #
+ # Raises a SaslError if authentication failed.
+ def external_auth(encoded)
+ unless encoded == EMPTY
+ authzid = decode64(encoded)
+ matches_from = (authzid == @stream.remote_domain)
+ raise SaslErrors::InvalidAuthzid unless matches_from
+ end
+ matches_from = @stream.cert_domain_matches?(@stream.remote_domain)
+ matches_from or raise SaslErrors::NotAuthorized
+ end
+
+ # Authenticate client-to-server streams using a username and password.
+ #
+ # encoded - The Base64 encoded jid and password String sent by the
+ # client stream.
+ #
+ # Returns the authenticated User or raises SaslError if authentication failed.
+ def plain_auth(encoded)
+ jid, password = decode_credentials(encoded)
+ user = authenticate(jid, password)
+ user or raise SaslErrors::NotAuthorized
+ end
+
+ private
+
+ # Storage backends should not raise errors, but if an unexpected error
+ # occurs during authentication, convert it to a temporary-auth-failure.
+ #
+ # jid - The user's jid String.
+ # password - The String password.
+ #
+ # Returns the authenticated User or nil if authentication failed.
+ #
+ # Raises TemoraryAuthFailure if the storage system failed.
+ def authenticate(jid, password)
+ log.info("Authenticating user: %s" % jid)
+ @stream.storage.authenticate(jid, password).tap do |user|
+ log.info("Authentication succeeded: %s" % user.jid) if user
+ end
+ rescue => e
+ log.error("Failed to authenticate: #{e.to_s}")
+ raise SaslErrors::TemporaryAuthFailure
+ end
+
+ # Return the JID and password decoded from the Base64 encoded SASL PLAIN
+ # credentials formatted as authzid\0authcid\0password.
+ #
+ # http://tools.ietf.org/html/rfc6120#section-6.3.8
+ # http://tools.ietf.org/html/rfc4616
+ #
+ # encoded - The Base64 encoded String from which to extract jid and password.
+ #
+ # Returns an Array of jid String and password String.
+ def decode_credentials(encoded)
+ authzid, node, password = decode64(encoded).split("\x00")
+ raise SaslErrors::NotAuthorized if node.nil? || node.empty? || password.nil? || password.empty?
+ jid = JID.new(node, @stream.domain) rescue (raise SaslErrors::NotAuthorized)
+ validate_authzid!(authzid, jid)
+ [jid, password]
+ end
+
+ # An optional SASL authzid allows a user to authenticate with one
+ # user name and password and then have their connection authorized as a
+ # different ID (the authzid). We don't support that, so raise an error if
+ # the authzid is provided and different than the authcid.
+ #
+ # Most clients don't send an authzid at all because it's optional and not
+ # widely supported. However, Strophe and Blather send a bare JID, in
+ # compliance with RFC 6120, but Smack sends just the user name as the
+ # authzid. So, take care to handle non-compliant clients here.
+ #
+ # http://tools.ietf.org/html/rfc6120#section-6.3.8
+ #
+ # authzid - The authzid String (may be nil).
+ # jid - The username String.
+ #
+ # Returns nothing.
+ def validate_authzid!(authzid, jid)
+ return if authzid.nil? || authzid.empty?
+ authzid.downcase!
+ smack = authzid == jid.node
+ compliant = authzid == jid.to_s
+ raise SaslErrors::InvalidAuthzid unless compliant || smack
+ end
+
+ # Decode the Base64 encoded string, raising an error for invalid data.
+ #
+ # http://tools.ietf.org/html/rfc6120#section-13.9.1
+ #
+ # encoded - The Base64 encoded String.
+ #
+ # Returns a UTF-8 String.
+ def decode64(encoded)
+ Base64.strict_decode64(encoded).tap do |decoded|
+ decoded.force_encoding(Encoding::UTF_8)
+ raise SaslErrors::IncorrectEncoding unless decoded.valid_encoding?
+ end
+ rescue
+ raise SaslErrors::IncorrectEncoding
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/server.rb b/lib/vines/stream/server.rb
new file mode 100644
index 0000000..5ae4efe
--- /dev/null
+++ b/lib/vines/stream/server.rb
@@ -0,0 +1,207 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ # Implements the XMPP protocol for server-to-server (s2s) streams. This
+ # serves connected streams using the jabber:server namespace. This handles
+ # both accepting incoming s2s streams and initiating outbound s2s streams
+ # to other servers.
+ class Server < Stream
+ MECHANISMS, FROM, TO = %w(EXTERNAL from to).map(&:freeze)
+
+ # Starts the connection to the remote server. When the stream is
+ # connected and ready to send stanzas it will yield to the callback
+ # block. The callback is run on the EventMachine reactor thread. The
+ # yielded stream will be nil if the remote connection failed. We need to
+ # use a background thread to avoid blocking the server on DNS SRV
+ # lookups.
+ def self.start(config, to, from, dialback_verify_key = false, &callback)
+ op = proc do
+ Resolv::DNS.open do |dns|
+ dns.getresources("_xmpp-server._tcp.#{to}", Resolv::DNS::Resource::IN::SRV)
+ end.sort! {|a,b| a.priority == b.priority ? b.weight <=> a.weight : a.priority <=> b.priority }
+ end
+ cb = proc do |srv|
+ if srv.empty?
+ srv << {target: to, port: 5269}
+ class << srv.first
+ def method_missing(name); self[name]; end
+ end
+ end
+ Server.connect(config, to, from, srv, dialback_verify_key, callback)
+ end
+ EM.defer(proc { op.call rescue [] }, cb)
+ end
+
+ def self.connect(config, to, from, srv, dialback_verify_key = false, callback)
+ if srv.empty?
+ # fiber so storage calls work properly
+ Fiber.new { callback.call(nil) }.resume
+ else
+ begin
+ rr = srv.shift
+ opts = {to: to, from: from, srv: srv, dialback_verify_key: dialback_verify_key, callback: callback}
+ EM.connect(rr.target.to_s, rr.port, Server, config, opts)
+ rescue => e
+ connect(config, to, from, srv, dialback_verify_key, callback)
+ end
+ end
+ end
+
+ attr_reader :domain
+ attr_accessor :remote_domain
+
+ def initialize(config, options={})
+ super(config)
+ @outbound_tls_required = false
+ @peer_trusted = nil
+ @connected = false
+ @remote_domain = options[:to]
+ @domain = options[:from]
+ @srv = options[:srv]
+ @dialback_verify_key = options[:dialback_verify_key]
+ @callback = options[:callback]
+ @outbound = @remote_domain && @domain
+ start = @outbound ? Outbound::Start.new(self) : Start.new(self)
+ advance(start)
+ end
+
+ def post_init
+ super
+ send_stream_header if @outbound
+ end
+
+ def max_stanza_size
+ config[:server].max_stanza_size
+ end
+
+ def ssl_verify_peer(pem)
+ @peer_trusted = @store.trusted?(pem)
+ true
+ end
+
+ def peer_trusted?
+ !@peer_trusted.nil? && @peer_trusted
+ end
+
+ def dialback_retry?
+ return false if @peer_trusted.nil? || @peer_trusted
+ true
+ end
+
+ def ssl_handshake_completed
+ @peer_trusted = cert_domain_matches?(@remote_domain) && peer_trusted?
+ end
+
+ def outbound_tls_required?
+ @outbound_tls_required
+ end
+
+ def outbound_tls_required(required)
+ @outbound_tls_required = required
+ end
+
+ # Return an array of allowed authentication mechanisms advertised as
+ # server stream features.
+ def authentication_mechanisms
+ MECHANISMS
+ end
+
+ def stream_type
+ :server
+ end
+
+ def unbind
+ super
+ if @outbound && !@connected
+ Server.connect(config, @remote_domain, @domain, @srv, @callback)
+ end
+ end
+
+ def vhost?(domain)
+ config.vhost?(domain)
+ end
+
+ def notify_connected
+ @connected = true
+ self.callback!
+ @callback = nil
+ end
+
+ def callback!
+ @callback.call(self) if @callback
+ end
+
+ def dialback_verify_key?
+ @dialback_verify_key
+ end
+
+ def ready?
+ state.class == Server::Ready
+ end
+
+ def authoritative_dialback(node)
+ stream = self
+ Server.start(stream.config, node[FROM], node[TO], true) do |authoritative|
+ if authoritative
+ # will be closed in outbound/authoritative.rb
+ authoritative.write("<db:verify from='#{node[TO]}' id='#{stream.id}' " \
+ "to='#{node[FROM]}'>#{node.text}</db:verify>")
+ end
+ end
+ # We need to be discoverable for the dialback connection
+ router << stream
+ rescue StanzaErrors::RemoteServerNotFound
+ write("<db:result from='#{node[TO]}' to='#{node[FROM]}' " \
+ "type='error'><error type='cancel'><item-not-found " \
+ "xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></error></db:result>")
+ close_connection_after_writing
+ end
+
+ def start(node)
+ if @outbound then send_stream_header; return end
+ to, from = %w[to from].map {|a| node[a] }
+ @domain, @remote_domain = to, from unless @domain
+ send_stream_header
+ raise StreamErrors::NotAuthorized if domain_change?(to, from)
+ raise StreamErrors::ImproperAddressing unless valid_address?(@domain) && valid_address?(@remote_domain)
+ raise StreamErrors::HostUnknown unless config.vhost?(@domain) || config.pubsub?(@domain) || config.component?(@domain)
+ raise StreamErrors::NotAuthorized unless config.s2s?(@remote_domain) && config.allowed?(@domain, @remote_domain)
+ raise StreamErrors::InvalidNamespace unless node.namespaces['xmlns'] == NAMESPACES[:server]
+ raise StreamErrors::InvalidNamespace unless node.namespaces['xmlns:stream'] == NAMESPACES[:stream]
+ end
+
+ private
+
+ # The `to` and `from` domain addresses set on the initial stream header
+ # must not change during stream restarts. This prevents a server from
+ # authenticating as one domain, then sending stanzas from users in a
+ # different domain.
+ #
+ # to - The String domain the other server thinks we are.
+ # from - The String domain the other server is asserting as its identity.
+ #
+ # Returns true if the other server is misbehaving and its connection
+ # should be closed.
+ def domain_change?(to, from)
+ to != @domain || from != @remote_domain
+ end
+
+ def send_stream_header
+ stream_id = Kit.uuid
+ update_stream_id(stream_id)
+ attrs = {
+ 'xmlns' => NAMESPACES[:server],
+ 'xmlns:stream' => NAMESPACES[:stream],
+ 'xmlns:db' => NAMESPACES[:legacy_dialback],
+ 'xml:lang' => 'en',
+ 'id' => stream_id,
+ 'version' => '1.0',
+ 'from' => @domain,
+ 'to' => @remote_domain,
+ }
+ write "<stream:stream %s>" % attrs.to_a.map{|k,v| "#{k}='#{v}'"}.join(' ')
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/server/auth.rb b/lib/vines/stream/server/auth.rb
new file mode 100644
index 0000000..0198541
--- /dev/null
+++ b/lib/vines/stream/server/auth.rb
@@ -0,0 +1,30 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Server
+ class Auth < Client::Auth
+ RESULT = "result".freeze
+
+ def initialize(stream, success=FinalRestart)
+ super
+ end
+
+ def node(node)
+ if dialback_result?(node)
+ # open a new connection and verify the dialback key
+ stream.authoritative_dialback(node)
+ else
+ super
+ end
+ end
+
+ private
+
+ def dialback_result?(node)
+ node.name == RESULT && namespace(node) == NAMESPACES[:legacy_dialback]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/server/auth_method.rb b/lib/vines/stream/server/auth_method.rb
new file mode 100644
index 0000000..22331d3
--- /dev/null
+++ b/lib/vines/stream/server/auth_method.rb
@@ -0,0 +1,66 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Server
+ class AuthMethod < State
+ VERIFY, VALID_TYPE, INVALID_TYPE = %w[verify valid invalid].map {|t| t.freeze }
+ STARTTLS, RESULT, FROM, TO = %w[starttls result from to].map {|s| s.freeze }
+ PROCEED = %Q{<proceed xmlns="#{NAMESPACES[:tls]}"/>}.freeze
+ FAILURE = %Q{<failure xmlns="#{NAMESPACES[:tls]}"/>}.freeze
+
+ def initialize(stream, success=AuthRestart)
+ super
+ end
+
+ def node(node)
+ if dialback_verify?(node)
+ id, from, to = %w[id from to].map {|a| node[a] }
+ key = node.text
+ outbound_stream = stream.router.stream_by_id(id)
+
+ unless outbound_stream && outbound_stream.state.is_a?(Stream::Server::Outbound::AuthDialbackResult)
+ stream.write(%Q{<db:verify from="#{to}" to=#{from} id=#{id} type="error"><error type="cancel"><item-not-found xmlns="#{NAMESPACES[:stanzas]}" /></error></db:verify>})
+ return
+ end
+
+ secret = outbound_stream.state.dialback_secret
+ type = Kit.dialback_key(secret, from, to, id) == key ? VALID_TYPE : INVALID_TYPE
+ stream.write(%Q{<db:verify from="#{to}" to="#{from}" id="#{id}" type="#{type}" />})
+ stream.close_connection_after_writing
+ elsif starttls?(node)
+ if stream.encrypt?
+ stream.write(PROCEED)
+ stream.encrypt
+ stream.reset
+ advance
+ else
+ stream.write(FAILURE)
+ stream.write('</stream:stream>')
+ stream.close_connection_after_writing
+ end
+ elsif dialback_result?(node)
+ # open a new connection and verify the dialback key
+ stream.authoritative_dialback(node)
+ else
+ raise StreamErrors::NotAuthorized
+ end
+ end
+
+ private
+
+ def starttls?(node)
+ node.name == STARTTLS && namespace(node) == NAMESPACES[:tls]
+ end
+
+ def dialback_verify?(node)
+ node.name == VERIFY && namespace(node) == NAMESPACES[:legacy_dialback]
+ end
+
+ def dialback_result?(node)
+ node.name == RESULT && namespace(node) == NAMESPACES[:legacy_dialback]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/server/auth_restart.rb b/lib/vines/stream/server/auth_restart.rb
new file mode 100644
index 0000000..f5703af
--- /dev/null
+++ b/lib/vines/stream/server/auth_restart.rb
@@ -0,0 +1,39 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Server
+ class AuthRestart < State
+ def initialize(stream, success=Auth)
+ super
+ end
+
+ def node(node)
+ raise StreamErrors::NotAuthorized unless stream?(node)
+ stream.start(node)
+ doc = Document.new
+ features = doc.create_element('stream:features')
+ if stream.dialback_retry?
+ if stream.vhost.force_s2s_encryption?
+ stream.close_connection
+ return
+ end
+ @success = AuthMethod
+ features << doc.create_element('dialback') do |db|
+ db.default_namespace = NAMESPACES[:dialback]
+ end
+ else
+ features << doc.create_element('mechanisms') do |parent|
+ parent.default_namespace = NAMESPACES[:sasl]
+ stream.authentication_mechanisms.each do |name|
+ parent << doc.create_element('mechanism', name)
+ end
+ end
+ end
+ stream.write(features)
+ advance
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/server/final_restart.rb b/lib/vines/stream/server/final_restart.rb
new file mode 100644
index 0000000..1c96f45
--- /dev/null
+++ b/lib/vines/stream/server/final_restart.rb
@@ -0,0 +1,21 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Server
+ class FinalRestart < State
+ def initialize(stream, success=Ready)
+ super
+ end
+
+ def node(node)
+ raise StreamErrors::NotAuthorized unless stream?(node)
+ stream.start(node)
+ stream.write('<stream:features/>')
+ stream.router << stream
+ advance
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/server/outbound/auth.rb b/lib/vines/stream/server/outbound/auth.rb
new file mode 100644
index 0000000..b0567db
--- /dev/null
+++ b/lib/vines/stream/server/outbound/auth.rb
@@ -0,0 +1,65 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Server
+ class Outbound
+ class Auth < State
+ REQUIRED = 'required'.freeze
+ FEATURES = 'features'.freeze
+
+ def initialize(stream, success=AuthDialbackResult)
+ super
+ end
+
+ def node(node)
+ # We have to remember tls_require for
+ # closing or restarting the stream
+ stream.outbound_tls_required(tls_required?(node))
+
+ if stream.dialback_verify_key?
+ @success = Authoritative
+ stream.callback!
+ advance
+ elsif dialback?(node)
+ secret = Kit.auth_token
+ dialback_key = Kit.dialback_key(secret, stream.remote_domain, stream.domain, stream.id)
+ stream.write("<db:result xmlns:db='#{NAMESPACES[:legacy_dialback]}' " \
+ "from='#{stream.domain}' to='#{stream.remote_domain}'>#{dialback_key}</db:result>")
+ advance
+ stream.router << stream # We need to be discoverable for the dialback connection
+ stream.state.dialback_secret = secret
+ elsif tls?(node)
+ @success = TLSResult
+ stream.write("<starttls xmlns='#{NAMESPACES[:tls]}'/>")
+ advance
+ else
+ raise StreamErrors::NotAuthorized
+ end
+ end
+
+ private
+
+ def tls_required?(node)
+ child = node.xpath('ns:starttls', 'ns' => NAMESPACES[:tls]).children.first
+ child && child.name == REQUIRED
+ end
+
+ def dialback?(node)
+ dialback = node.xpath('ns:dialback', 'ns' => NAMESPACES[:dialback]).any?
+ features?(node) && dialback && !tls_required?(node)
+ end
+
+ def tls?(node)
+ tls = node.xpath('ns:starttls', 'ns' => NAMESPACES[:tls]).any?
+ features?(node) && tls
+ end
+
+ def features?(node)
+ node.name == FEATURES && namespace(node) == NAMESPACES[:stream]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/server/outbound/auth_dialback_result.rb b/lib/vines/stream/server/outbound/auth_dialback_result.rb
new file mode 100644
index 0000000..0e2848b
--- /dev/null
+++ b/lib/vines/stream/server/outbound/auth_dialback_result.rb
@@ -0,0 +1,39 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Server
+ class Outbound
+ class AuthDialbackResult < State
+ RESULT, VALID, INVALID, TYPE = %w[result valid invalid type].map {|s| s.freeze }
+
+ attr_accessor :dialback_secret
+
+ def initialize(stream, success=Ready)
+ super
+ end
+
+ def node(node)
+ raise StreamErrors::NotAuthorized unless result?(node)
+
+ case node[TYPE]
+ when VALID
+ advance
+ stream.notify_connected
+ when INVALID
+ stream.close_connection
+ else
+ raise StreamErrors::NotAuthorized
+ end
+ end
+
+ private
+
+ def result?(node)
+ node.name == RESULT && namespace(node) == NAMESPACES[:legacy_dialback]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/server/outbound/auth_external.rb b/lib/vines/stream/server/outbound/auth_external.rb
new file mode 100644
index 0000000..1575fdb
--- /dev/null
+++ b/lib/vines/stream/server/outbound/auth_external.rb
@@ -0,0 +1,33 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Server
+ class Outbound
+ class AuthExternal < State
+ def initialize(stream, success=AuthExternalResult)
+ super
+ end
+
+ def node(node)
+ raise StreamErrors::NotAuthorized unless external?(node)
+ authzid = Base64.strict_encode64(stream.domain)
+ stream.write(%Q{<auth xmlns="#{NAMESPACES[:sasl]}" mechanism="EXTERNAL">#{authzid}</auth>})
+ advance
+ end
+
+ private
+
+ def external?(node)
+ external = node.xpath("ns:mechanisms/ns:mechanism[text()='EXTERNAL']", 'ns' => NAMESPACES[:sasl]).any?
+ features?(node) && external
+ end
+
+ def features?(node)
+ node.name == 'features' && namespace(node) == NAMESPACES[:stream]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/server/outbound/auth_external_result.rb b/lib/vines/stream/server/outbound/auth_external_result.rb
new file mode 100644
index 0000000..795c935
--- /dev/null
+++ b/lib/vines/stream/server/outbound/auth_external_result.rb
@@ -0,0 +1,32 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Server
+ class Outbound
+ class AuthExternalResult < State
+ SUCCESS = 'success'.freeze
+ FAILURE = 'failure'.freeze
+
+ def initialize(stream, success=FinalRestart)
+ super
+ end
+
+ def node(node)
+ raise StreamErrors::NotAuthorized unless namespace(node) == NAMESPACES[:sasl]
+ case node.name
+ when SUCCESS
+ stream.start(node)
+ stream.reset
+ advance
+ when FAILURE
+ stream.close_connection
+ else
+ raise StreamErrors::NotAuthorized
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/server/outbound/auth_restart.rb b/lib/vines/stream/server/outbound/auth_restart.rb
new file mode 100644
index 0000000..9a04b62
--- /dev/null
+++ b/lib/vines/stream/server/outbound/auth_restart.rb
@@ -0,0 +1,27 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Server
+ class Outbound
+ class AuthRestart < State
+ def initialize(stream, success=AuthExternal)
+ super
+ end
+
+ def node(node)
+ raise StreamErrors::NotAuthorized unless stream?(node)
+ if stream.dialback_retry?
+ if stream.outbound_tls_required?
+ stream.close_connection
+ return
+ end
+ @success = Auth
+ end
+ advance
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/server/outbound/authoritative.rb b/lib/vines/stream/server/outbound/authoritative.rb
new file mode 100644
index 0000000..05c5422
--- /dev/null
+++ b/lib/vines/stream/server/outbound/authoritative.rb
@@ -0,0 +1,48 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Server
+ class Outbound
+ class Authoritative < State
+ VALID, INVALID, ERROR, TYPE = %w[valid invalid error type]
+ VERIFY, ID, FROM, TO = %w[verify id from to].map {|s| s.freeze }
+
+ def initialize(stream, success=nil)
+ super
+ end
+
+ def node(node)
+ raise StreamErrors::NotAuthorized unless authoritative?(node)
+
+ case node[TYPE]
+ when VALID
+ @inbound.write("<db:result xmlns:db='#{NAMESPACES[:legacy_dialback]}' " \
+ "from='#{node[TO]}' to='#{node[FROM]}' type='#{node[TYPE]}'/>")
+ @inbound.advance(Server::Ready.new(@inbound))
+ @inbound.notify_connected
+ when INVALID
+ @inbound.write("<db:result xmlns:db='#{NAMESPACES[:legacy_dialback]}' " \
+ "from='#{node[TO]}' to='#{node[FROM]}' type='#{node[TYPE]}'/>")
+ @inbound.close_connection_after_writing
+ else
+ @inbound.write("<db:result xmlns:db='#{NAMESPACES[:legacy_dialback]}' " \
+ "from='#{node[TO]}' to='#{node[FROM]}' type='#{ERROR}'>" \
+ "<error type='cancel'><item-not-found xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>" \
+ "</error></db:result>")
+ @inbound.close_connection_after_writing
+ end
+ stream.close_connection
+ end
+
+ private
+
+ def authoritative?(node)
+ @inbound = stream.router.stream_by_id(node[ID])
+ node.name == VERIFY && namespace(node) == NAMESPACES[:legacy_dialback] && !@inbound.nil?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/server/outbound/final_features.rb b/lib/vines/stream/server/outbound/final_features.rb
new file mode 100644
index 0000000..0848533
--- /dev/null
+++ b/lib/vines/stream/server/outbound/final_features.rb
@@ -0,0 +1,28 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Server
+ class Outbound
+ class FinalFeatures < State
+ def initialize(stream, success=Server::Ready)
+ super
+ end
+
+ def node(node)
+ raise StreamErrors::NotAuthorized unless empty_features?(node)
+ stream.router << stream
+ advance
+ stream.notify_connected
+ end
+
+ private
+
+ def empty_features?(node)
+ node.name == 'features' && namespace(node) == NAMESPACES[:stream] && node.elements.empty?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/server/outbound/final_restart.rb b/lib/vines/stream/server/outbound/final_restart.rb
new file mode 100644
index 0000000..46ba30b
--- /dev/null
+++ b/lib/vines/stream/server/outbound/final_restart.rb
@@ -0,0 +1,20 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Server
+ class Outbound
+ class FinalRestart < State
+ def initialize(stream, success=FinalFeatures)
+ super
+ end
+
+ def node(node)
+ raise StreamErrors::NotAuthorized unless stream?(node)
+ advance
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/server/outbound/start.rb b/lib/vines/stream/server/outbound/start.rb
new file mode 100644
index 0000000..4677278
--- /dev/null
+++ b/lib/vines/stream/server/outbound/start.rb
@@ -0,0 +1,20 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Server
+ class Outbound
+ class Start < State
+ def initialize(stream, success=Auth)
+ super
+ end
+
+ def node(node)
+ raise StreamErrors::NotAuthorized unless stream?(node)
+ advance
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/server/outbound/tls_result.rb b/lib/vines/stream/server/outbound/tls_result.rb
new file mode 100644
index 0000000..6467a2d
--- /dev/null
+++ b/lib/vines/stream/server/outbound/tls_result.rb
@@ -0,0 +1,34 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Server
+ class Outbound
+ class TLSResult < State
+ NS = NAMESPACES[:tls]
+ PROCEED = 'proceed'.freeze
+ FAILURE = 'failure'.freeze
+
+ def initialize(stream, success=AuthRestart)
+ super
+ end
+
+ def node(node)
+ raise StreamErrors::NotAuthorized unless namespace(node) == NS
+ case node.name
+ when PROCEED
+ stream.encrypt
+ stream.start(node)
+ stream.reset
+ advance
+ when FAILURE
+ stream.close_connection
+ else
+ raise StreamErrors::NotAuthorized
+ end
+ end
+ end
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/lib/vines/stream/server/ready.rb b/lib/vines/stream/server/ready.rb
new file mode 100644
index 0000000..aa538e5
--- /dev/null
+++ b/lib/vines/stream/server/ready.rb
@@ -0,0 +1,24 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Server
+ class Ready < State
+ def node(node)
+ stanza = to_stanza(node)
+ raise StreamErrors::UnsupportedStanzaType unless stanza
+ to, from = stanza.validate_to, stanza.validate_from
+ raise StreamErrors::ImproperAddressing unless to && from
+ raise StreamErrors::InvalidFrom unless from.domain == stream.remote_domain
+ raise StreamErrors::HostUnknown unless to.domain == stream.domain
+ stream.user = User.new(jid: from)
+ if stanza.local? || stanza.to_pubsub_domain?
+ stanza.process
+ else
+ stanza.route
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/server/start.rb b/lib/vines/stream/server/start.rb
new file mode 100644
index 0000000..876f5de
--- /dev/null
+++ b/lib/vines/stream/server/start.rb
@@ -0,0 +1,40 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+ class Server
+ class Start < State
+ FROM = "from".freeze
+
+ def initialize(stream, success=AuthMethod)
+ super
+ end
+
+ def node(node)
+ raise StreamErrors::NotAuthorized unless stream?(node)
+ stream.start(node)
+ doc = Document.new
+ features = doc.create_element('stream:features', 'xmlns:stream' => NAMESPACES[:stream]) do |el|
+ unless stream.dialback_retry?
+ el << doc.create_element('starttls') do |tls|
+ tls.default_namespace = NAMESPACES[:tls]
+ tls << doc.create_element('required') if force_s2s_encryption?
+ end
+ end
+ el << doc.create_element('dialback') do |db|
+ db.default_namespace = NAMESPACES[:dialback]
+ end
+ end
+ stream.write(features)
+ advance
+ end
+
+ private
+
+ def force_s2s_encryption?
+ stream.vhost.force_s2s_encryption?
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/stream/state.rb b/lib/vines/stream/state.rb
new file mode 100644
index 0000000..167ff59
--- /dev/null
+++ b/lib/vines/stream/state.rb
@@ -0,0 +1,46 @@
+# encoding: UTF-8
+
+module Vines
+ class Stream
+
+ # The base class of Stream state machines. States know how to process XML
+ # nodes and advance to their next valid state or fail the stream.
+ class State
+ include Nokogiri::XML
+ include Vines::Log
+ include Vines::Node
+
+ attr_accessor :stream
+
+ def initialize(stream, success=nil)
+ @stream, @success = stream, success
+ end
+
+ def node(node)
+ raise 'subclass must implement'
+ end
+
+ def ==(state)
+ self.class == state.class
+ end
+
+ def eql?(state)
+ state.is_a?(State) && self == state
+ end
+
+ def hash
+ self.class.hash
+ end
+
+ private
+
+ def advance
+ stream.advance(@success.new(stream))
+ end
+
+ def to_stanza(node)
+ super(node, stream)
+ end
+ end
+ end
+end
diff --git a/lib/vines/token_bucket.rb b/lib/vines/token_bucket.rb
new file mode 100644
index 0000000..87becf3
--- /dev/null
+++ b/lib/vines/token_bucket.rb
@@ -0,0 +1,55 @@
+# encoding: UTF-8
+
+module Vines
+
+ # The token bucket algorithm is useful for rate limiting.
+ # Before an operation can be completed, a token is taken from
+ # the bucket. If no tokens are available, the operation fails.
+ # The bucket is refilled with tokens at the maximum allowed rate
+ # of operations.
+ class TokenBucket
+
+ # Create a full bucket with `capacity` number of tokens to be filled
+ # at the given rate of tokens/second.
+ #
+ # capacity - The Fixnum maximum number of tokens the bucket can hold.
+ # rate - The Fixnum number of tokens per second at which the bucket is
+ # refilled.
+ def initialize(capacity, rate)
+ raise ArgumentError.new('capacity must be > 0') unless capacity > 0
+ raise ArgumentError.new('rate must be > 0') unless rate > 0
+ @capacity = capacity
+ @tokens = capacity
+ @rate = rate
+ @timestamp = Time.new
+ end
+
+ # Remove tokens from the bucket if it's full enough. There's no way, or
+ # need, to add tokens to the bucket. It refills over time.
+ #
+ # tokens - The Fixnum number of tokens to attempt to take from the bucket.
+ #
+ # Returns true if the bucket contains enough tokens to take, false if the
+ # bucket isn't full enough to satisy the request.
+ def take(tokens)
+ raise ArgumentError.new('tokens must be > 0') unless tokens > 0
+ tokens <= fill ? @tokens -= tokens : false
+ end
+
+ private
+
+ # Add tokens to the bucket at the `rate` provided in the constructor. This
+ # fills the bucket slowly over time.
+ #
+ # Returns the Fixnum number of tokens left in the bucket.
+ def fill
+ if @tokens < @capacity
+ now = Time.new
+ @tokens += (@rate * (now - @timestamp)).round
+ @tokens = @capacity if @tokens > @capacity
+ @timestamp = now
+ end
+ @tokens
+ end
+ end
+end
diff --git a/lib/vines/user.rb b/lib/vines/user.rb
new file mode 100644
index 0000000..2d0253d
--- /dev/null
+++ b/lib/vines/user.rb
@@ -0,0 +1,125 @@
+# encoding: UTF-8
+
+module Vines
+ class User
+ include Comparable
+
+ attr_accessor :name, :token, :password, :roster
+ attr_reader :jid
+
+ def initialize(args={})
+ @jid = JID.new(args[:jid])
+ raise ArgumentError, 'invalid jid' if @jid.empty?
+
+ @name = args[:name]
+ @password = args[:password]
+ @token = args[:token]
+ @roster = args[:roster] || []
+ end
+
+ def <=>(user)
+ user.is_a?(User) ? self.jid.to_s <=> user.jid.to_s : nil
+ end
+
+ alias :eql? :==
+
+ def hash
+ jid.to_s.hash
+ end
+
+ # Update this user's information from the given user object.
+ def update_from(user)
+ @name = user.name
+ @password = user.password
+ @token = user.token
+ @roster = user.roster.map {|c| c.clone }
+ end
+
+ # Return true if the jid is on this user's roster.
+ def contact?(jid)
+ !contact(jid).nil?
+ end
+
+ # Returns the contact with this jid or nil if not found.
+ def contact(jid)
+ bare = JID.new(jid).bare
+ @roster.find {|c| c.jid.bare == bare }
+ end
+
+ # Returns true if the user is subscribed to this contact's
+ # presence updates.
+ def subscribed_to?(jid)
+ contact = contact(jid)
+ contact && contact.subscribed_to?
+ end
+
+ # Returns true if the user has a presence subscription from this contact.
+ # The contact is subscribed to this user's presence.
+ def subscribed_from?(jid)
+ contact = contact(jid)
+ contact && contact.subscribed_from?
+ end
+
+ # Removes the contact with this jid from the user's roster.
+ def remove_contact(jid)
+ bare = JID.new(jid).bare
+ @roster.reject! {|c| c.jid.bare == bare }
+ end
+
+ # Returns a list of the contacts to which this user has
+ # successfully subscribed.
+ def subscribed_to_contacts
+ @roster.select {|c| c.subscribed_to? }
+ end
+
+ # Returns a list of the contacts that are subscribed to this user's
+ # presence updates.
+ def subscribed_from_contacts
+ @roster.select {|c| c.subscribed_from? }
+ end
+
+ # Update the contact's jid on this user's roster to signal that this user
+ # has requested the contact's permission to receive their presence updates.
+ def request_subscription(jid)
+ unless contact = contact(jid)
+ contact = Contact.new(:jid => jid)
+ @roster << contact
+ end
+ contact.ask = 'subscribe' if %w[none from].include?(contact.subscription)
+ end
+
+ # Add the user's jid to this contact's roster with a subscription state of
+ # 'from.' This signals that this contact has approved a user's subscription.
+ def add_subscription_from(jid)
+ unless contact = contact(jid)
+ contact = Contact.new(:jid => jid)
+ @roster << contact
+ end
+ contact.subscribe_from
+ end
+
+ def remove_subscription_to(jid)
+ if contact = contact(jid)
+ contact.unsubscribe_to
+ end
+ end
+
+ def remove_subscription_from(jid)
+ if contact = contact(jid)
+ contact.unsubscribe_from
+ end
+ end
+
+ # Returns this user's roster contacts as an iq query element.
+ def to_roster_xml(id)
+ doc = Nokogiri::XML::Document.new
+ doc.create_element('iq', 'id' => id, 'type' => 'result') do |el|
+ el << doc.create_element('query', 'xmlns' => 'jabber:iq:roster') do |query|
+ @roster.sort!.each do |contact|
+ query << contact.to_roster_xml
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/vines/version.rb b/lib/vines/version.rb
new file mode 100644
index 0000000..422c464
--- /dev/null
+++ b/lib/vines/version.rb
@@ -0,0 +1,6 @@
+# encoding: UTF-8
+
+module Vines
+ # vines forked version 0.4.10
+ VERSION = '0.2.0.develop.4'
+end
diff --git a/lib/vines/xmpp_server.rb b/lib/vines/xmpp_server.rb
new file mode 100644
index 0000000..b59c3a5
--- /dev/null
+++ b/lib/vines/xmpp_server.rb
@@ -0,0 +1,25 @@
+# encoding: UTF-8
+
+module Vines
+
+ # The main starting point for the XMPP server process. Starts the
+ # EventMachine processing loop and registers the XMPP protocol handler
+ # with the ports defined in the server configuration file.
+ class XmppServer
+ include Vines::Log
+
+ def initialize(config)
+ @config = config
+ end
+
+ def start
+ log.info('XMPP server started')
+ at_exit { log.fatal('XMPP server stopped') }
+ EM.epoll
+ EM.kqueue
+ EM.run do
+ @config.ports.each {|port| port.start }
+ end
+ end
+ end
+end
diff --git a/metadata.yml b/metadata.yml
new file mode 100644
index 0000000..3cf544c
--- /dev/null
+++ b/metadata.yml
@@ -0,0 +1,456 @@
+--- !ruby/object:Gem::Specification
+name: diaspora-vines
+version: !ruby/object:Gem::Version
+ version: 0.2.0.develop.4
+platform: ruby
+authors:
+- David Graham
+- Lukas Matt
+autorequire:
+bindir: bin
+cert_chain: []
+date: 2015-10-10 00:00:00.000000000 Z
+dependencies:
+- !ruby/object:Gem::Dependency
+ name: bcrypt
+ requirement: !ruby/object:Gem::Requirement
+ requirements:
+ - - "~>"
+ - !ruby/object:Gem::Version
+ version: '3.1'
+ type: :runtime
+ prerelease: false
+ version_requirements: !ruby/object:Gem::Requirement
+ requirements:
+ - - "~>"
+ - !ruby/object:Gem::Version
+ version: '3.1'
+- !ruby/object:Gem::Dependency
+ name: em-hiredis
+ requirement: !ruby/object:Gem::Requirement
+ requirements:
+ - - "~>"
+ - !ruby/object:Gem::Version
+ version: 0.3.0
+ type: :runtime
+ prerelease: false
+ version_requirements: !ruby/object:Gem::Requirement
+ requirements:
+ - - "~>"
+ - !ruby/object:Gem::Version
+ version: 0.3.0
+- !ruby/object:Gem::Dependency
+ name: eventmachine
+ requirement: !ruby/object:Gem::Requirement
+ requirements:
+ - - "~>"
+ - !ruby/object:Gem::Version
+ version: 1.0.8
+ type: :runtime
+ prerelease: false
+ version_requirements: !ruby/object:Gem::Requirement
+ requirements:
+ - - "~>"
+ - !ruby/object:Gem::Version
+ version: 1.0.8
+- !ruby/object:Gem::Dependency
+ name: http_parser.rb
+ requirement: !ruby/object:Gem::Requirement
+ requirements:
+ - - "~>"
+ - !ruby/object:Gem::Version
+ version: '0.6'
+ type: :runtime
+ prerelease: false
+ version_requirements: !ruby/object:Gem::Requirement
+ requirements:
+ - - "~>"
+ - !ruby/object:Gem::Version
+ version: '0.6'
+- !ruby/object:Gem::Dependency
+ name: nokogiri
+ requirement: !ruby/object:Gem::Requirement
+ requirements:
+ - - "~>"
+ - !ruby/object:Gem::Version
+ version: '1.6'
+ type: :runtime
+ prerelease: false
+ version_requirements: !ruby/object:Gem::Requirement
+ requirements:
+ - - "~>"
+ - !ruby/object:Gem::Version
+ version: '1.6'
+- !ruby/object:Gem::Dependency
+ name: activerecord
+ requirement: !ruby/object:Gem::Requirement
+ requirements:
+ - - "~>"
+ - !ruby/object:Gem::Version
+ version: '4.1'
+ type: :runtime
+ prerelease: false
+ version_requirements: !ruby/object:Gem::Requirement
+ requirements:
+ - - "~>"
+ - !ruby/object:Gem::Version
+ version: '4.1'
+- !ruby/object:Gem::Dependency
+ name: pronto
+ requirement: !ruby/object:Gem::Requirement
+ requirements:
+ - - "~>"
+ - !ruby/object:Gem::Version
+ version: 0.4.2
+ type: :development
+ prerelease: false
+ version_requirements: !ruby/object:Gem::Requirement
+ requirements:
+ - - "~>"
+ - !ruby/object:Gem::Version
+ version: 0.4.2
+- !ruby/object:Gem::Dependency
+ name: pronto-rubocop
+ requirement: !ruby/object:Gem::Requirement
+ requirements:
+ - - "~>"
+ - !ruby/object:Gem::Version
+ version: 0.4.4
+ type: :development
+ prerelease: false
+ version_requirements: !ruby/object:Gem::Requirement
+ requirements:
+ - - "~>"
+ - !ruby/object:Gem::Version
+ version: 0.4.4
+- !ruby/object:Gem::Dependency
+ name: rails
+ requirement: !ruby/object:Gem::Requirement
+ requirements:
+ - - "~>"
+ - !ruby/object:Gem::Version
+ version: '4.1'
+ type: :development
+ prerelease: false
+ version_requirements: !ruby/object:Gem::Requirement
+ requirements:
+ - - "~>"
+ - !ruby/object:Gem::Version
+ version: '4.1'
+- !ruby/object:Gem::Dependency
+ name: sqlite3
+ requirement: !ruby/object:Gem::Requirement
+ requirements:
+ - - "~>"
+ - !ruby/object:Gem::Version
+ version: 1.3.9
+ type: :development
+ prerelease: false
+ version_requirements: !ruby/object:Gem::Requirement
+ requirements:
+ - - "~>"
+ - !ruby/object:Gem::Version
+ version: 1.3.9
+- !ruby/object:Gem::Dependency
+ name: minitest
+ requirement: !ruby/object:Gem::Requirement
+ requirements:
+ - - "~>"
+ - !ruby/object:Gem::Version
+ version: '5.8'
+ type: :development
+ prerelease: false
+ version_requirements: !ruby/object:Gem::Requirement
+ requirements:
+ - - "~>"
+ - !ruby/object:Gem::Version
+ version: '5.8'
+- !ruby/object:Gem::Dependency
+ name: rake
+ requirement: !ruby/object:Gem::Requirement
+ requirements:
+ - - "~>"
+ - !ruby/object:Gem::Version
+ version: '10.3'
+ type: :development
+ prerelease: false
+ version_requirements: !ruby/object:Gem::Requirement
+ requirements:
+ - - "~>"
+ - !ruby/object:Gem::Version
+ version: '10.3'
+description: Diaspora-vines is a Vines fork build for diaspora integration. DO NOT
+ use it unless you know what you are doing!
+email:
+- david at negativecode.com
+- lukas at zauberstuhl.de
+executables:
+- vines
+extensions: []
+extra_rdoc_files: []
+files:
+- Gemfile
+- LICENSE
+- README.md
+- Rakefile
+- bin/vines
+- conf/certs/README
+- conf/certs/ca-bundle.crt
+- conf/config.rb
+- lib/vines.rb
+- lib/vines/cli.rb
+- lib/vines/cluster.rb
+- lib/vines/cluster/connection.rb
+- lib/vines/cluster/publisher.rb
+- lib/vines/cluster/pubsub.rb
+- lib/vines/cluster/sessions.rb
+- lib/vines/cluster/subscriber.rb
+- lib/vines/command/cert.rb
+- lib/vines/command/restart.rb
+- lib/vines/command/start.rb
+- lib/vines/command/stop.rb
+- lib/vines/config.rb
+- lib/vines/config/diaspora.rb
+- lib/vines/config/host.rb
+- lib/vines/config/port.rb
+- lib/vines/config/pubsub.rb
+- lib/vines/contact.rb
+- lib/vines/daemon.rb
+- lib/vines/error.rb
+- lib/vines/jid.rb
+- lib/vines/kit.rb
+- lib/vines/log.rb
+- lib/vines/node.rb
+- lib/vines/router.rb
+- lib/vines/stanza.rb
+- lib/vines/stanza/dialback.rb
+- lib/vines/stanza/iq.rb
+- lib/vines/stanza/iq/auth.rb
+- lib/vines/stanza/iq/disco_info.rb
+- lib/vines/stanza/iq/disco_items.rb
+- lib/vines/stanza/iq/error.rb
+- lib/vines/stanza/iq/ping.rb
+- lib/vines/stanza/iq/private_storage.rb
+- lib/vines/stanza/iq/query.rb
+- lib/vines/stanza/iq/result.rb
+- lib/vines/stanza/iq/roster.rb
+- lib/vines/stanza/iq/session.rb
+- lib/vines/stanza/iq/vcard.rb
+- lib/vines/stanza/iq/version.rb
+- lib/vines/stanza/message.rb
+- lib/vines/stanza/presence.rb
+- lib/vines/stanza/presence/error.rb
+- lib/vines/stanza/presence/probe.rb
+- lib/vines/stanza/presence/subscribe.rb
+- lib/vines/stanza/presence/subscribed.rb
+- lib/vines/stanza/presence/unavailable.rb
+- lib/vines/stanza/presence/unsubscribe.rb
+- lib/vines/stanza/presence/unsubscribed.rb
+- lib/vines/stanza/pubsub.rb
+- lib/vines/stanza/pubsub/create.rb
+- lib/vines/stanza/pubsub/delete.rb
+- lib/vines/stanza/pubsub/publish.rb
+- lib/vines/stanza/pubsub/subscribe.rb
+- lib/vines/stanza/pubsub/unsubscribe.rb
+- lib/vines/storage.rb
+- lib/vines/storage/local.rb
+- lib/vines/storage/null.rb
+- lib/vines/storage/sql.rb
+- lib/vines/store.rb
+- lib/vines/stream.rb
+- lib/vines/stream/client.rb
+- lib/vines/stream/client/auth.rb
+- lib/vines/stream/client/auth_restart.rb
+- lib/vines/stream/client/bind.rb
+- lib/vines/stream/client/bind_restart.rb
+- lib/vines/stream/client/closed.rb
+- lib/vines/stream/client/ready.rb
+- lib/vines/stream/client/session.rb
+- lib/vines/stream/client/start.rb
+- lib/vines/stream/client/tls.rb
+- lib/vines/stream/component.rb
+- lib/vines/stream/component/handshake.rb
+- lib/vines/stream/component/ready.rb
+- lib/vines/stream/component/start.rb
+- lib/vines/stream/http.rb
+- lib/vines/stream/http/auth.rb
+- lib/vines/stream/http/bind.rb
+- lib/vines/stream/http/bind_restart.rb
+- lib/vines/stream/http/ready.rb
+- lib/vines/stream/http/request.rb
+- lib/vines/stream/http/session.rb
+- lib/vines/stream/http/sessions.rb
+- lib/vines/stream/http/start.rb
+- lib/vines/stream/parser.rb
+- lib/vines/stream/sasl.rb
+- lib/vines/stream/server.rb
+- lib/vines/stream/server/auth.rb
+- lib/vines/stream/server/auth_method.rb
+- lib/vines/stream/server/auth_restart.rb
+- lib/vines/stream/server/final_restart.rb
+- lib/vines/stream/server/outbound/auth.rb
+- lib/vines/stream/server/outbound/auth_dialback_result.rb
+- lib/vines/stream/server/outbound/auth_external.rb
+- lib/vines/stream/server/outbound/auth_external_result.rb
+- lib/vines/stream/server/outbound/auth_restart.rb
+- lib/vines/stream/server/outbound/authoritative.rb
+- lib/vines/stream/server/outbound/final_features.rb
+- lib/vines/stream/server/outbound/final_restart.rb
+- lib/vines/stream/server/outbound/start.rb
+- lib/vines/stream/server/outbound/tls_result.rb
+- lib/vines/stream/server/ready.rb
+- lib/vines/stream/server/start.rb
+- lib/vines/stream/state.rb
+- lib/vines/token_bucket.rb
+- lib/vines/user.rb
+- lib/vines/version.rb
+- lib/vines/xmpp_server.rb
+- test/cluster/publisher_test.rb
+- test/cluster/sessions_test.rb
+- test/cluster/subscriber_test.rb
+- test/config/host_test.rb
+- test/config/pubsub_test.rb
+- test/config_test.rb
+- test/contact_test.rb
+- test/error_test.rb
+- test/ext/nokogiri.rb
+- test/jid_test.rb
+- test/kit_test.rb
+- test/router_test.rb
+- test/stanza/iq/disco_info_test.rb
+- test/stanza/iq/disco_items_test.rb
+- test/stanza/iq/private_storage_test.rb
+- test/stanza/iq/roster_test.rb
+- test/stanza/iq/session_test.rb
+- test/stanza/iq/vcard_test.rb
+- test/stanza/iq/version_test.rb
+- test/stanza/iq_test.rb
+- test/stanza/message_test.rb
+- test/stanza/presence/probe_test.rb
+- test/stanza/presence/subscribe_test.rb
+- test/stanza/pubsub/create_test.rb
+- test/stanza/pubsub/delete_test.rb
+- test/stanza/pubsub/publish_test.rb
+- test/stanza/pubsub/subscribe_test.rb
+- test/stanza/pubsub/unsubscribe_test.rb
+- test/stanza_test.rb
+- test/storage/local_test.rb
+- test/storage/mock_redis.rb
+- test/storage/null_test.rb
+- test/storage/sql_schema.rb
+- test/storage/sql_test.rb
+- test/storage/storage_tests.rb
+- test/store_test.rb
+- test/stream/client/auth_test.rb
+- test/stream/client/ready_test.rb
+- test/stream/client/session_test.rb
+- test/stream/component/handshake_test.rb
+- test/stream/component/ready_test.rb
+- test/stream/component/start_test.rb
+- test/stream/http/auth_test.rb
+- test/stream/http/ready_test.rb
+- test/stream/http/request_test.rb
+- test/stream/http/sessions_test.rb
+- test/stream/http/start_test.rb
+- test/stream/parser_test.rb
+- test/stream/sasl_test.rb
+- test/stream/server/auth_method_test.rb
+- test/stream/server/auth_test.rb
+- test/stream/server/outbound/auth_dialback_result_test.rb
+- test/stream/server/outbound/auth_external_test.rb
+- test/stream/server/outbound/auth_restart_test.rb
+- test/stream/server/outbound/auth_test.rb
+- test/stream/server/outbound/authoritative_test.rb
+- test/stream/server/outbound/start_test.rb
+- test/stream/server/ready_test.rb
+- test/stream/server/start_test.rb
+- test/test_helper.rb
+- test/token_bucket_test.rb
+- test/user_test.rb
+homepage: https://diasporafoundation.org
+licenses:
+- MIT
+metadata: {}
+post_install_message:
+rdoc_options: []
+require_paths:
+- lib
+required_ruby_version: !ruby/object:Gem::Requirement
+ requirements:
+ - - ">="
+ - !ruby/object:Gem::Version
+ version: 1.9.3
+required_rubygems_version: !ruby/object:Gem::Requirement
+ requirements:
+ - - ">"
+ - !ruby/object:Gem::Version
+ version: 1.3.1
+requirements: []
+rubyforge_project:
+rubygems_version: 2.4.5.1
+signing_key:
+specification_version: 4
+summary: Diaspora-vines is a Vines fork build for diaspora integration.
+test_files:
+- test/error_test.rb
+- test/test_helper.rb
+- test/storage/local_test.rb
+- test/storage/mock_redis.rb
+- test/storage/sql_schema.rb
+- test/storage/sql_test.rb
+- test/storage/null_test.rb
+- test/storage/storage_tests.rb
+- test/ext/nokogiri.rb
+- test/contact_test.rb
+- test/store_test.rb
+- test/cluster/sessions_test.rb
+- test/cluster/publisher_test.rb
+- test/cluster/subscriber_test.rb
+- test/config_test.rb
+- test/stream/parser_test.rb
+- test/stream/client/ready_test.rb
+- test/stream/client/auth_test.rb
+- test/stream/client/session_test.rb
+- test/stream/sasl_test.rb
+- test/stream/http/sessions_test.rb
+- test/stream/http/start_test.rb
+- test/stream/http/ready_test.rb
+- test/stream/http/auth_test.rb
+- test/stream/http/request_test.rb
+- test/stream/component/start_test.rb
+- test/stream/component/ready_test.rb
+- test/stream/component/handshake_test.rb
+- test/stream/server/start_test.rb
+- test/stream/server/ready_test.rb
+- test/stream/server/auth_test.rb
+- test/stream/server/auth_method_test.rb
+- test/stream/server/outbound/start_test.rb
+- test/stream/server/outbound/auth_restart_test.rb
+- test/stream/server/outbound/auth_external_test.rb
+- test/stream/server/outbound/auth_test.rb
+- test/stream/server/outbound/auth_dialback_result_test.rb
+- test/stream/server/outbound/authoritative_test.rb
+- test/token_bucket_test.rb
+- test/router_test.rb
+- test/user_test.rb
+- test/stanza/message_test.rb
+- test/stanza/presence/subscribe_test.rb
+- test/stanza/presence/probe_test.rb
+- test/stanza/pubsub/publish_test.rb
+- test/stanza/pubsub/delete_test.rb
+- test/stanza/pubsub/create_test.rb
+- test/stanza/pubsub/unsubscribe_test.rb
+- test/stanza/pubsub/subscribe_test.rb
+- test/stanza/iq_test.rb
+- test/stanza/iq/roster_test.rb
+- test/stanza/iq/vcard_test.rb
+- test/stanza/iq/disco_info_test.rb
+- test/stanza/iq/private_storage_test.rb
+- test/stanza/iq/session_test.rb
+- test/stanza/iq/version_test.rb
+- test/stanza/iq/disco_items_test.rb
+- test/jid_test.rb
+- test/stanza_test.rb
+- test/config/host_test.rb
+- test/config/pubsub_test.rb
+- test/kit_test.rb
diff --git a/test/cluster/publisher_test.rb b/test/cluster/publisher_test.rb
new file mode 100644
index 0000000..8e18a95
--- /dev/null
+++ b/test/cluster/publisher_test.rb
@@ -0,0 +1,57 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Cluster::Publisher do
+ subject { Vines::Cluster::Publisher.new(cluster) }
+ let(:connection) { MiniTest::Mock.new }
+ let(:cluster) { MiniTest::Mock.new }
+
+ before do
+ cluster.expect :id, 'abc'
+ cluster.expect :connection, connection
+ end
+
+ describe '#broadcast' do
+ before do
+ msg = {from: 'abc', type: 'online', time: Time.now.to_i}.to_json
+ connection.expect :publish, nil, ["cluster:nodes:all", msg]
+ end
+
+ it 'publishes the message to every cluster node' do
+ subject.broadcast(:online)
+ connection.verify
+ cluster.verify
+ end
+ end
+
+ describe '#route' do
+ let(:stanza) { "<message>hello</message>" }
+
+ before do
+ msg = {from: 'abc', type: 'stanza', stanza: stanza}.to_json
+ connection.expect :publish, nil, ["cluster:nodes:node-42", msg]
+ end
+
+ it 'publishes the message to just one cluster node' do
+ subject.route(stanza, "node-42")
+ connection.verify
+ cluster.verify
+ end
+ end
+
+ describe '#update_user' do
+ let(:jid) { Vines::JID.new('alice at wonderland.lit') }
+
+ before do
+ msg = {from: 'abc', type: 'user', jid: jid.to_s}.to_json
+ connection.expect :publish, nil, ["cluster:nodes:node-42", msg]
+ end
+
+ it 'publishes the new user to just one cluster node' do
+ subject.update_user(jid, "node-42")
+ connection.verify
+ cluster.verify
+ end
+ end
+end
diff --git a/test/cluster/sessions_test.rb b/test/cluster/sessions_test.rb
new file mode 100644
index 0000000..f1f5116
--- /dev/null
+++ b/test/cluster/sessions_test.rb
@@ -0,0 +1,47 @@
+# encoding: UTF-8
+
+require 'test_helper'
+require 'storage/storage_tests'
+require 'storage/mock_redis'
+
+describe Vines::Cluster::Sessions do
+ subject { Vines::Cluster::Sessions.new(cluster) }
+ let(:connection) { MockRedis.new }
+ let(:cluster) { OpenStruct.new(id: 'abc', connection: connection) }
+ let(:jid1) { 'alice at wonderland.lit/tea' }
+ let(:jid2) { 'alice at wonderland.lit/cake' }
+
+ describe 'when saving to the cluster' do
+ it 'writes to a redis hash' do
+ StorageTests::EMLoop.new do
+ subject.save(jid1, {available: true, interested: true})
+ subject.save(jid2, {available: false, interested: false})
+ EM.next_tick do
+ session1 = {node: 'abc', available: true, interested: true}
+ session2 = {node: 'abc', available: false, interested: false}
+ connection.db["sessions:alice at wonderland.lit"].size.must_equal 2
+ connection.db["sessions:alice at wonderland.lit"]['tea'].must_equal session1.to_json
+ connection.db["sessions:alice at wonderland.lit"]['cake'].must_equal session2.to_json
+ connection.db["cluster:nodes:abc"].to_a.must_equal [jid1, jid2]
+ end
+ end
+ end
+ end
+
+ describe 'when deleting from the cluster' do
+ it 'removes from a redis hash' do
+ StorageTests::EMLoop.new do
+ connection.db["sessions:alice at wonderland.lit"] = {}
+ connection.db["sessions:alice at wonderland.lit"]['tea'] = {node: 'abc', available: true}.to_json
+ connection.db["sessions:alice at wonderland.lit"]['cake'] = {node: 'abc', available: true}.to_json
+ connection.db["cluster:nodes:abc"] = Set.new([jid1, jid2])
+
+ subject.delete(jid1)
+ EM.next_tick do
+ connection.db["sessions:alice at wonderland.lit"].size.must_equal 1
+ connection.db["cluster:nodes:abc"].to_a.must_equal [jid2]
+ end
+ end
+ end
+ end
+end
diff --git a/test/cluster/subscriber_test.rb b/test/cluster/subscriber_test.rb
new file mode 100644
index 0000000..3b313bf
--- /dev/null
+++ b/test/cluster/subscriber_test.rb
@@ -0,0 +1,111 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Cluster::Subscriber do
+ subject { Vines::Cluster::Subscriber.new(cluster) }
+ let(:connection) { MiniTest::Mock.new }
+ let(:cluster) { MiniTest::Mock.new }
+ let(:now) { Time.now.to_i }
+
+ before do
+ cluster.expect :id, 'abc'
+ end
+
+ describe '#subscribe' do
+ before do
+ cluster.expect :connect, connection
+ connection.expect :subscribe, nil, ['cluster:nodes:all']
+ connection.expect :subscribe, nil, ['cluster:nodes:abc']
+ connection.expect :on, nil, [:message]
+ end
+
+ it 'subscribes to its own channel and the broadcast channel' do
+ subject.subscribe
+ connection.verify
+ cluster.verify
+ end
+ end
+
+ describe 'when receiving a heartbeat broadcast message' do
+ before do
+ cluster.expect :poke, nil, ['node-42', now]
+ end
+
+ it 'pokes the session manager for the broadcasting node' do
+ msg = {from: 'node-42', type: 'heartbeat', time: now}.to_json
+ subject.send(:on_message, 'cluster:nodes:all', msg)
+ connection.verify
+ cluster.verify
+ end
+ end
+
+ describe 'when receiving an initial online broadcast message' do
+ before do
+ cluster.expect :poke, nil, ['node-42', now]
+ end
+
+ it 'pokes the session manager for the broadcasting node' do
+ msg = {from: 'node-42', type: 'online', time: now}.to_json
+ subject.send(:on_message, 'cluster:nodes:all', msg)
+ connection.verify
+ cluster.verify
+ end
+ end
+
+ describe 'when receiving an offline broadcast message' do
+ before do
+ cluster.expect :delete_sessions, nil, ['node-42']
+ end
+
+ it 'deletes the sessions for the broadcasting node' do
+ msg = {from: 'node-42', type: 'offline', time: now}.to_json
+ subject.send(:on_message, 'cluster:nodes:all', msg)
+ connection.verify
+ cluster.verify
+ end
+ end
+
+ describe 'when receiving a stanza routed to my node' do
+ let(:stream) { MiniTest::Mock.new }
+ let(:stanza) { "<message to='alice at wonderland.lit/tea'>hello</message>" }
+ let(:xml) { Nokogiri::XML(stanza).root }
+
+ before do
+ stream.expect :write, nil, [xml]
+ cluster.expect :connected_resources, [stream], ['alice at wonderland.lit/tea']
+ end
+
+ it 'writes the stanza to the connected user streams' do
+ # NOTE https://github.com/diaspora/vines/issues/68
+ skip "This fails randomly! Skipping it for later investigations."
+ msg = {from: 'node-42', type: 'stanza', stanza: stanza}.to_json
+ subject.send(:on_message, 'cluster:nodes:abc', msg)
+ stream.verify
+ connection.verify
+ cluster.verify
+ end
+ end
+
+ describe 'when receiving a user update message to my node' do
+ let(:alice) { Vines::User.new(jid: 'alice at wonderland.lit/tea') }
+ let(:storage) { MiniTest::Mock.new }
+ let(:stream) { MiniTest::Mock.new }
+
+ before do
+ storage.expect :find_user, alice, [alice.jid.bare]
+ stream.expect :user, alice
+ cluster.expect :storage, storage, ['wonderland.lit']
+ cluster.expect :connected_resources, [stream], [alice.jid.bare]
+ end
+
+ it 'reloads the user from storage and updates their connected streams' do
+ msg = {from: 'node-42', type: 'user', jid: alice.jid.to_s}.to_json
+ subject.send(:on_message, 'cluster:nodes:abc', msg)
+ storage.verify
+ stream.verify
+ connection.verify
+ cluster.verify
+ end
+ end
+end
diff --git a/test/config/host_test.rb b/test/config/host_test.rb
new file mode 100644
index 0000000..a15b136
--- /dev/null
+++ b/test/config/host_test.rb
@@ -0,0 +1,358 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Config::Host do
+ def test_missing_storage
+ assert_raises(RuntimeError) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ # missing storage
+ end
+ end
+ end
+ end
+
+ def test_bad_storage
+ assert_raises(RuntimeError) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage 'bogus' do
+ # no bogus storage implementation
+ end
+ end
+ end
+ end
+ end
+
+ def test_duplicate_storage
+ assert_raises(RuntimeError) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage('fs') { dir Dir.tmpdir }
+ storage('fs') { dir Dir.tmpdir }
+ end
+ end
+ end
+ end
+
+ def test_good_storage_raises_no_errors
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ storage 'fs' do
+ dir Dir.tmpdir
+ end
+ end
+ end
+ refute_nil config.vhost('wonderland.lit').storage
+ end
+
+ def test_empty_component_name_raises
+ assert_raises(RuntimeError) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ components '' => 'secr3t'
+ end
+ end
+ end
+ end
+
+ def test_nil_component_name_raises
+ assert_raises(RuntimeError) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ components nil => 'secr3t'
+ end
+ end
+ end
+ end
+
+ def test_empty_component_password_raises
+ assert_raises(RuntimeError) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ components 'tea' => ''
+ end
+ end
+ end
+ end
+
+ def test_nil_component_password_raises
+ assert_raises(RuntimeError) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ components 'tea' => nil
+ end
+ end
+ end
+ end
+
+ def test_duplicate_component_raises
+ assert_raises(RuntimeError) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ components 'tea' => 'one'
+ components 'TEA' => 'two'
+ end
+ end
+ end
+ end
+
+ def test_duplicate_component_in_one_call_raises
+ assert_raises(RuntimeError) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ components 'tea' => 'one', 'TEA' => 'two'
+ end
+ end
+ end
+ end
+
+ def test_duplicate_component_symbol_raises
+ assert_raises(RuntimeError) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ components 'tea' => 'one'
+ components :TEA => 'two'
+ end
+ end
+ end
+ end
+
+ def test_invalid_host_domain_raises
+ assert_raises(ArgumentError) do
+ Vines::Config.new do
+ host 'wonderland.lit ' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ end
+ end
+
+ def test_invalid_jid_host_domain_raises
+ assert_raises(RuntimeError) do
+ Vines::Config.new do
+ host 'alice at wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ end
+ end
+
+ def test_invalid_component_domain_raises
+ assert_raises(ArgumentError) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ components 'exam ple' => 'one'
+ end
+ end
+ end
+ end
+
+ def test_invalid_jid_component_domain_raises
+ assert_raises(RuntimeError) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ components 'alice at example' => 'one'
+ end
+ end
+ end
+ end
+
+ def test_multi_subdomain_component_raises
+ assert_raises(RuntimeError) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ components 'exam.ple' => 'one'
+ end
+ end
+ end
+ end
+
+ def test_case_insensitive_component_name
+ config = Vines::Config.new do
+ host 'WONDERLAND.LIT' do
+ storage(:fs) { dir Dir.tmpdir }
+ components 'TEA' => 'secr3t', CAKE: 'Passw0rd'
+ end
+ end
+ host = config.vhost('wonderland.lit')
+ refute_nil host
+ assert_equal 2, host.components.size
+ assert_equal host.components['tea.wonderland.lit'], 'secr3t'
+ assert_equal host.components['cake.wonderland.lit'], 'Passw0rd'
+ end
+
+ def test_component?
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ components 'tea' => 'secr3t', cake: 'passw0rd'
+ end
+ end
+ host = config.vhost('wonderland.lit')
+ refute_nil host
+ refute host.component?(nil)
+ refute host.component?('tea')
+ refute host.component?(:cake)
+ assert host.component?('tea.wonderland.lit')
+ assert host.component?(Vines::JID.new('tea.wonderland.lit'))
+ assert host.component?('cake.wonderland.lit')
+ assert_nil host.password(nil)
+ assert_nil host.password('bogus')
+ assert_equal 'secr3t', host.password('tea.wonderland.lit')
+ assert_equal 'passw0rd', host.password('cake.wonderland.lit')
+ expected = {'tea.wonderland.lit' => 'secr3t', 'cake.wonderland.lit' => 'passw0rd'}
+ assert_equal expected, host.components
+
+ refute config.component?(nil)
+ refute config.component?('tea')
+ refute config.component?('bogus')
+ assert config.component?('tea.wonderland.lit')
+ assert config.component?(Vines::JID.new('tea.wonderland.lit'))
+ assert config.component?('cake.wonderland.lit')
+ assert config.component?('tea.wonderland.lit', 'cake.wonderland.lit')
+ refute config.component?('tea.wonderland.lit', 'bogus.wonderland.lit')
+
+ assert_nil config.component_password(nil)
+ assert_nil config.component_password('bogus')
+ assert_equal 'secr3t', config.component_password('tea.wonderland.lit')
+ assert_equal 'passw0rd', config.component_password('cake.wonderland.lit')
+ end
+
+ def test_invalid_pubsub_domain_raises
+ assert_raises(ArgumentError) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ pubsub 'exam ple'
+ end
+ end
+ end
+ end
+
+ def test_invalid_jid_pubsub_domain_raises
+ assert_raises(RuntimeError) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ pubsub 'alice at example'
+ end
+ end
+ end
+ end
+
+ def test_multi_subdomain_pubsub_raises
+ assert_raises(RuntimeError) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ pubsub 'exam.ple'
+ end
+ end
+ end
+ end
+
+ def test_case_insensitive_pubsub_name
+ config = Vines::Config.new do
+ host 'WONDERLAND.LIT' do
+ storage(:fs) { dir Dir.tmpdir }
+ pubsub 'TEA', :CAKE
+ end
+ end
+ host = config.vhost('wonderland.lit')
+ refute_nil host
+ assert_equal 2, host.pubsubs.size
+ refute_nil host.pubsubs['tea.wonderland.lit']
+ refute_nil host.pubsubs['cake.wonderland.lit']
+ end
+
+ def test_pubsub?
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ pubsub 'tea', :cake
+ end
+ end
+ host = config.vhost('wonderland.lit')
+ refute_nil host
+ refute host.pubsub?(nil)
+ refute host.pubsub?('tea')
+ refute host.pubsub?(:cake)
+ assert host.pubsub?('tea.wonderland.lit')
+ assert host.pubsub?(Vines::JID.new('tea.wonderland.lit'))
+ assert host.pubsub?('cake.wonderland.lit')
+ assert_equal ['tea.wonderland.lit', 'cake.wonderland.lit'], host.pubsubs.keys
+
+ refute config.pubsub?(nil)
+ refute config.pubsub?('tea')
+ refute config.pubsub?('bogus')
+ assert config.pubsub?('tea.wonderland.lit')
+ assert config.pubsub?(Vines::JID.new('tea.wonderland.lit'))
+ assert config.pubsub?('cake.wonderland.lit')
+ refute config.pubsub?('alice at cake.wonderland.lit')
+ end
+
+ def test_default_private_storage_is_off
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ host = config.vhost('wonderland.lit')
+ refute_nil host
+ refute host.private_storage?
+ end
+
+ def test_enable_private_storage
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ private_storage true
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ host = config.vhost('wonderland.lit')
+ refute_nil host
+ assert host.private_storage?
+ assert config.private_storage?('wonderland.lit')
+ assert config.private_storage?(Vines::JID.new('wonderland.lit'))
+ refute config.private_storage?(Vines::JID.new('alice at wonderland.lit'))
+ refute config.private_storage?(nil)
+ end
+
+ def test_enabled_blacklisting
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ server '0.0.0.0', 5269 do
+ max_stanza_size 131072
+ blacklist ['wonderland.lit']
+ end
+ end
+ refute config.s2s?('wonderland.lit')
+ end
+
+ def test_disabled_blacklisting
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ server '0.0.0.0', 5269 do
+ max_stanza_size 131072
+ blacklist []
+ end
+ end
+ assert config.s2s?('wonderland.lit')
+ end
+end
diff --git a/test/config/pubsub_test.rb b/test/config/pubsub_test.rb
new file mode 100644
index 0000000..88cf857
--- /dev/null
+++ b/test/config/pubsub_test.rb
@@ -0,0 +1,187 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Config::PubSub do
+ subject { config.pubsub('topics.wonderland.lit') }
+ let(:alice) { Vines::JID.new('alice at wonderland.lit') }
+ let(:romeo) { Vines::JID.new('romeo at verona.lit') }
+ let(:config) do
+ @config = Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ pubsub 'topics'
+ end
+ end
+ end
+
+ it 'adds and deletes a topic node' do
+ topic = 'rhode_island_is_neither_a_road_nor_an_island'
+ refute subject.node?(topic)
+ subject.add_node(topic)
+ assert subject.node?(topic)
+ subject.delete_node(topic)
+ refute subject.node?(topic)
+ end
+
+ it 'ignores deleting a missing topic node' do
+ topic = 'kittens_vs_puppies'
+ refute subject.node?(topic)
+ subject.delete_node(topic)
+ refute subject.node?(topic)
+ end
+
+ it 'subscribes a jid to a node' do
+ topic = 'with_jid'
+ jid = Vines::JID.new('alice at wonderland.lit')
+ subject.add_node(topic)
+ subject.subscribe(topic, jid)
+ assert subject.subscribed?(topic, jid.to_s)
+ assert subject.subscribed?(topic, jid)
+ end
+
+ it 'does not allow remote jids to subscribe to a node by default' do
+ topic = 'remote_jids_failed'
+ jid = 'romeo at verona.lit'
+ subject.add_node(topic)
+ subject.subscribe(topic, jid)
+ refute subject.subscribed?(topic, jid)
+ end
+
+ it 'allows remote jid subscriptions when cross domain messages are enabled' do
+ topic = 'remote_jids_allowed'
+ jid = 'romeo at verona.lit'
+ config.vhost('wonderland.lit').cross_domain_messages true
+ subject.add_node(topic)
+ subject.subscribe(topic, jid)
+ assert subject.subscribed?(topic, jid)
+ end
+
+ it 'ignores subscribing to a missing node' do
+ topic = 'bogus'
+ jid = 'alice at wonderland.lit'
+ refute subject.node?(topic)
+ subject.subscribe(topic, jid)
+ refute subject.node?(topic)
+ refute subject.subscribed?(topic, jid)
+ end
+
+ it 'deletes the node after unsubscribing' do
+ topic = 'delete_me'
+ jid = 'alice at wonderland.lit/tea'
+ subject.add_node(topic)
+ subject.subscribe(topic, jid)
+ assert subject.subscribed?(topic, jid)
+ subject.unsubscribe(topic, jid)
+ refute subject.subscribed?(topic, jid)
+ refute subject.node?(topic)
+ end
+
+ it 'unsubscribes a jid from all topics' do
+ topic = 'pirates_vs_ninjas'
+ topic2 = 'pirates_vs_ninjas_2'
+ jid = 'alice at wonderland.lit'
+ jid2 = 'hatter at wonderland.lit'
+ subject.add_node(topic)
+ subject.add_node(topic2)
+
+ subject.subscribe(topic, jid)
+ subject.subscribe(topic, jid2)
+ subject.subscribe(topic2, jid)
+ assert subject.subscribed?(topic, jid)
+ assert subject.subscribed?(topic, jid2)
+ assert subject.subscribed?(topic2, jid)
+
+ subject.unsubscribe_all(jid)
+ refute subject.node?(topic2)
+ refute subject.subscribed?(topic, jid)
+ refute subject.subscribed?(topic2, jid)
+ assert subject.subscribed?(topic, jid2)
+ end
+
+ describe 'when publishing a message to a topic node' do
+ let(:xml) do
+ node(%q{
+ <iq type='set' to='topics.wonderland.lit'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <publish node='pirates_vs_ninjas'>
+ <item id='item_42'>
+ <entry xmlns='http://www.w3.org/2005/Atom'>
+ <title>Test</title>
+ <summary>This is a summary.</summary>
+ </entry>
+ </item>
+ </publish>
+ </pubsub>
+ </iq>})
+ end
+
+ let(:recipient) do
+ recipient = MiniTest::Mock.new
+ recipient.expect :user, Vines::User.new(jid: alice)
+ class << recipient
+ attr_accessor :nodes
+ def write(node)
+ @nodes ||= []
+ @nodes << node
+ end
+ end
+ recipient
+ end
+
+ before do
+ router = MiniTest::Mock.new
+ router.expect :connected_resources, [recipient], [alice, 'topics.wonderland.lit']
+ class << router
+ attr_accessor :nodes
+ def route(node)
+ @nodes ||= []
+ @nodes << node
+ end
+ end
+
+ class << config
+ attr_accessor :router
+ end
+ config.router = router
+ config.vhost('wonderland.lit').cross_domain_messages true
+
+ subject.add_node(topic)
+ subject.subscribe(topic, alice)
+ subject.subscribe(topic, romeo)
+ end
+
+ let(:topic) { 'pirates_vs_ninjas' }
+
+ it 'writes the message to local connected resource streams' do
+ expected = xml.clone
+ expected['to'] = 'alice at wonderland.lit'
+ expected['from'] = 'topics.wonderland.lit'
+
+ subject.publish(topic, xml)
+ config.router.verify
+ recipient.verify
+
+ # id is random
+ received = recipient.nodes.first
+ received['id'].wont_be_nil
+ received.remove_attribute('id')
+ received.must_equal expected
+ end
+
+ it 'routes the message to remote jids' do
+ expected = xml.clone
+ expected['to'] = 'romeo at verona.lit'
+ expected['from'] = 'topics.wonderland.lit'
+
+ subject.publish(topic, xml)
+ config.router.verify
+
+ # id is random
+ routed = config.router.nodes.first
+ routed['id'].wont_be_nil
+ routed.remove_attribute('id')
+ routed.must_equal expected
+ end
+ end
+end
diff --git a/test/config_test.rb b/test/config_test.rb
new file mode 100644
index 0000000..2eaa1bd
--- /dev/null
+++ b/test/config_test.rb
@@ -0,0 +1,753 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Config do
+ def test_missing_host
+ assert_raises(RuntimeError) do
+ Vines::Config.new do
+ # missing hosts
+ end
+ end
+ end
+
+ def test_duplicate_host
+ assert_raises(RuntimeError) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage 'fs' do
+ dir Dir.tmpdir
+ end
+ end
+ host 'WONDERLAND.LIT' do
+ storage 'fs' do
+ dir Dir.tmpdir
+ end
+ end
+ end
+ end
+ end
+
+ def test_duplicate_host_in_one_call
+ assert_raises(RuntimeError) do
+ Vines::Config.new do
+ host 'wonderland.lit', 'wonderland.lit' do
+ storage 'fs' do
+ dir Dir.tmpdir
+ end
+ end
+ end
+ end
+ end
+
+ def test_configure
+ config = Vines::Config.configure do
+ host 'wonderland.lit' do
+ storage :fs do
+ dir Dir.tmpdir
+ end
+ end
+ end
+ refute_nil config
+ assert_same config, Vines::Config.instance
+ end
+
+ def test_vhost
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ refute_nil config.vhost('wonderland.lit')
+ refute_nil config.vhost(Vines::JID.new('wonderland.lit'))
+ assert config.vhost?('wonderland.lit')
+ assert config.vhost?(Vines::JID.new('wonderland.lit'))
+ refute config.vhost?('alice at wonderland.lit')
+ refute config.vhost?('tea.wonderland.lit')
+ refute config.vhost?('bogus')
+ end
+
+ def test_port_lookup
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ client
+ end
+ refute_nil config[:client]
+ assert_raises(ArgumentError) { config[:server] }
+ assert_raises(ArgumentError) { config[:bogus] }
+ end
+
+ def test_duplicate_client
+ assert_raises(RuntimeError) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ client
+ client
+ end
+ end
+ end
+
+ def test_duplicate_server
+ assert_raises(RuntimeError) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ server
+ server
+ end
+ end
+ end
+
+ def test_duplicate_http
+ assert_raises(RuntimeError) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ http
+ http
+ end
+ end
+ end
+
+ def test_duplicate_component
+ assert_raises(RuntimeError) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ component
+ component
+ end
+ end
+ end
+
+ def test_duplicate_cluster
+ assert_raises(RuntimeError) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ cluster {}
+ cluster {}
+ end
+ end
+ end
+
+ def test_missing_cluster
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ assert_nil config.cluster
+ refute config.cluster?
+ end
+
+ def test_cluster
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ cluster do
+ host 'redis.wonderland.lit'
+ port 12345
+ database 8
+ password 'secr3t'
+ end
+ end
+ refute_nil config.cluster
+ assert config.cluster?
+ assert_equal 'redis.wonderland.lit', config.cluster.host
+ assert_equal 12345, config.cluster.port
+ assert_equal 8, config.cluster.database
+ assert_equal 'secr3t', config.cluster.password
+ end
+
+ def test_default_client
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ client
+ end
+ port = config.ports.first
+ refute_nil port
+ assert_equal Vines::Config::ClientPort, port.class
+ assert_equal '0.0.0.0', port.host
+ assert_equal 5222, port.port
+ assert_equal 131_072, port.max_stanza_size
+ assert_equal 5, port.max_resources_per_account
+ assert_equal Vines::Stream::Client, port.stream
+ assert_same config, port.config
+ assert_equal 1, config.ports.size
+ end
+
+ def test_configured_client
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ client '0.0.0.1', 42 do
+ max_stanza_size 60_000
+ max_resources_per_account 1
+ end
+ end
+ port = config.ports.first
+ refute_nil port
+ assert_equal Vines::Config::ClientPort, port.class
+ assert_equal '0.0.0.1', port.host
+ assert_equal 42, port.port
+ assert_equal 60_000, port.max_stanza_size
+ assert_equal 1, port.max_resources_per_account
+ assert_equal Vines::Stream::Client, port.stream
+ assert_same config, port.config
+ assert_equal 1, config.ports.size
+ end
+
+ def test_max_stanza_size
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ client do
+ max_stanza_size 0
+ end
+ end
+ assert_equal 10_000, config.ports.first.max_stanza_size
+ end
+
+ def test_default_server
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ server
+ end
+ port = config.ports.first
+ refute_nil port
+ assert_equal Vines::Config::ServerPort, port.class
+ assert_equal '0.0.0.0', port.host
+ assert_equal 5269, port.port
+ assert_equal 131_072, port.max_stanza_size
+ assert_equal Vines::Stream::Server, port.stream
+ assert_same config, port.config
+ assert_equal 1, config.ports.size
+ end
+
+ def test_configured_server
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ server '0.0.0.1', 42 do
+ max_stanza_size 60_000
+ end
+ end
+ port = config.ports.first
+ refute_nil port
+ assert_equal Vines::Config::ServerPort, port.class
+ assert_equal '0.0.0.1', port.host
+ assert_equal 42, port.port
+ assert_equal 60_000, port.max_stanza_size
+ assert_equal Vines::Stream::Server, port.stream
+ assert_same config, port.config
+ assert_equal 1, config.ports.size
+ end
+
+ def test_default_http
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ http
+ end
+ port = config.ports.first
+ refute_nil port
+ assert_equal Vines::Config::HttpPort, port.class
+ assert_equal '0.0.0.0', port.host
+ assert_equal 5280, port.port
+ assert_equal 131_072, port.max_stanza_size
+ assert_equal 5, port.max_resources_per_account
+ assert_equal File.join(Dir.pwd, 'web'), port.root
+ assert_equal '/xmpp', port.bind
+ assert_equal Vines::Stream::Http, port.stream
+ assert_same config, port.config
+ assert_equal 1, config.ports.size
+ end
+
+ def test_configured_http
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ http '0.0.0.1', 42 do
+ bind '/custom'
+ max_stanza_size 60_000
+ max_resources_per_account 1
+ root '/var/www/html'
+ end
+ end
+ port = config.ports.first
+ refute_nil port
+ assert_equal Vines::Config::HttpPort, port.class
+ assert_equal '0.0.0.1', port.host
+ assert_equal 42, port.port
+ assert_equal 60_000, port.max_stanza_size
+ assert_equal 1, port.max_resources_per_account
+ assert_equal '/var/www/html', port.root
+ assert_equal '/custom', port.bind
+ assert_equal Vines::Stream::Http, port.stream
+ assert_same config, port.config
+ assert_equal 1, config.ports.size
+ end
+
+ def test_default_component
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ component
+ end
+ port = config.ports.first
+ refute_nil port
+ assert_equal Vines::Config::ComponentPort, port.class
+ assert_equal '0.0.0.0', port.host
+ assert_equal 5347, port.port
+ assert_equal 131_072, port.max_stanza_size
+ assert_equal Vines::Stream::Component, port.stream
+ assert_same config, port.config
+ assert_equal 1, config.ports.size
+ end
+
+ def test_configured_component
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ component '0.0.0.1', 42 do
+ max_stanza_size 60_000
+ end
+ end
+ port = config.ports.first
+ refute_nil port
+ assert_equal Vines::Config::ComponentPort, port.class
+ assert_equal '0.0.0.1', port.host
+ assert_equal 42, port.port
+ assert_equal 60_000, port.max_stanza_size
+ assert_equal Vines::Stream::Component, port.stream
+ assert_same config, port.config
+ assert_equal 1, config.ports.size
+ end
+
+ def test_not_existing_file_path
+ assert_raises(RuntimeError) do
+ config = Vines::Config.new do
+ log 'not/existing/path.log'
+ end
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ end
+
+ def test_invalid_log_level
+ assert_raises(RuntimeError) do
+ config = Vines::Config.new do
+ log 'vines.log' do
+ level 'bogus'
+ end
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ end
+ end
+
+ def test_valid_log_level
+ config = Vines::Config.new do
+ log 'vines.log' do
+ level :error
+ end
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ assert_equal Logger::ERROR, Class.new.extend(Vines::Log).log.level
+ end
+
+ def test_null_storage
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ assert_equal Vines::Storage::Local, config.storage('wonderland.lit').class
+ assert_equal Vines::Storage::Null, config.storage('bogus').class
+ end
+
+ def test_cross_domain_messages
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ host 'verona.lit' do
+ cross_domain_messages true
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ refute config.vhost('wonderland.lit').cross_domain_messages?
+ assert config.vhost('verona.lit').cross_domain_messages?
+ end
+
+ def test_accept_self_signed
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ host 'verona.lit' do
+ accept_self_signed true
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ refute config.vhost('wonderland.lit').accept_self_signed?
+ assert config.vhost('verona.lit').accept_self_signed?
+ end
+
+ def test_local_jid?
+ config = Vines::Config.new do
+ host 'wonderland.lit', 'verona.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ refute config.local_jid?(nil)
+ refute config.local_jid?('alice at wonderland.lit', nil)
+ assert config.local_jid?('alice at wonderland.lit')
+ assert config.local_jid?(Vines::JID.new('alice at wonderland.lit'))
+ assert config.local_jid?(Vines::JID.new('wonderland.lit'))
+ assert config.local_jid?('alice at wonderland.lit', 'romeo at verona.lit')
+ refute config.local_jid?('alice at wonderland.lit', 'romeo at bogus.lit')
+ refute config.local_jid?('alice at tea.wonderland.lit')
+ refute config.local_jid?('alice at bogus.lit')
+ end
+
+ def test_missing_addresses_not_allowed
+ config = Vines::Config.new do
+ host 'wonderland.lit', 'verona.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ refute config.allowed?(nil, nil)
+ refute config.allowed?('', '')
+ end
+
+ def test_same_domain_allowed
+ config = Vines::Config.new do
+ host 'wonderland.lit', 'verona.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ alice = Vines::JID.new('alice at wonderland.lit')
+ hatter = Vines::JID.new('hatter at wonderland.lit')
+ assert config.allowed?(alice, hatter)
+ assert config.allowed?(hatter, alice)
+ assert config.allowed?('wonderland.lit', alice)
+ end
+
+ def test_both_vhosts_with_cross_domain_allowed
+ config = Vines::Config.new do
+ host 'wonderland.lit', 'verona.lit' do
+ cross_domain_messages true
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ alice = Vines::JID.new('alice at wonderland.lit')
+ romeo = Vines::JID.new('romeo at verona.lit')
+ assert config.allowed?(alice, romeo)
+ assert config.allowed?(romeo, alice)
+ end
+
+ def test_one_vhost_with_cross_domain_not_allowed
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ cross_domain_messages true
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ host 'verona.lit' do
+ cross_domain_messages false
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ alice = Vines::JID.new('alice at wonderland.lit')
+ romeo = Vines::JID.new('romeo at verona.lit')
+ refute config.allowed?(alice, romeo)
+ refute config.allowed?(romeo, alice)
+ end
+
+ def test_same_domain_component_to_pubsub_allowed
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ cross_domain_messages false
+ storage(:fs) { dir Dir.tmpdir }
+ components 'tea' => 'secr3t', cake: 'passw0rd'
+ pubsub 'games'
+ end
+ end
+ alice = Vines::JID.new('alice at tea.wonderland.lit')
+ tea = Vines::JID.new('tea.wonderland.lit')
+ pubsub = Vines::JID.new('games.wonderland.lit')
+ assert config.allowed?(alice, pubsub)
+ assert config.allowed?(pubsub, alice)
+ assert config.allowed?(tea, pubsub)
+ assert config.allowed?(pubsub, tea)
+ end
+
+ def test_cross_domain_component_to_pubsub_allowed
+ config = Vines::Config.new do
+ host 'wonderland.lit', 'verona.lit' do
+ cross_domain_messages true
+ storage(:fs) { dir Dir.tmpdir }
+ components 'tea' => 'secr3t', cake: 'passw0rd'
+ pubsub 'games'
+ end
+ end
+ alice = Vines::JID.new('alice at tea.wonderland.lit')
+ tea = Vines::JID.new('tea.wonderland.lit')
+ pubsub = Vines::JID.new('games.verona.lit')
+ assert config.allowed?(alice, pubsub)
+ assert config.allowed?(pubsub, alice)
+ assert config.allowed?(tea, pubsub)
+ assert config.allowed?(pubsub, tea)
+ end
+
+ def test_cross_domain_component_to_pubsub_not_allowed
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ cross_domain_messages true
+ storage(:fs) { dir Dir.tmpdir }
+ components 'tea' => 'secr3t', cake: 'passw0rd'
+ pubsub 'games'
+ end
+ host 'verona.lit' do
+ cross_domain_messages false
+ storage(:fs) { dir Dir.tmpdir }
+ components 'party' => 'secr3t'
+ end
+ end
+ romeo = Vines::JID.new('romeo at party.verona.lit')
+ party = Vines::JID.new('party.verona.lit')
+ pubsub = Vines::JID.new('games.wonderland.lit')
+ refute config.allowed?(romeo, pubsub)
+ refute config.allowed?(pubsub, romeo)
+ refute config.allowed?(party, pubsub)
+ refute config.allowed?(pubsub, party)
+ end
+
+ def test_same_domain_component_to_component_allowed
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ cross_domain_messages false
+ storage(:fs) { dir Dir.tmpdir }
+ components 'tea' => 'secr3t', cake: 'passw0rd'
+ end
+ end
+ alice = Vines::JID.new('alice at tea.wonderland.lit')
+ hatter = Vines::JID.new('hatter at cake.wonderland.lit')
+ assert config.allowed?(alice, alice)
+ assert config.allowed?(alice, hatter)
+ assert config.allowed?(hatter, alice)
+ end
+
+ def test_cross_domain_component_to_component_allowed
+ config = Vines::Config.new do
+ host 'wonderland.lit', 'verona.lit' do
+ cross_domain_messages true
+ storage(:fs) { dir Dir.tmpdir }
+ components 'tea' => 'secr3t', cake: 'passw0rd'
+ end
+ end
+ alice = Vines::JID.new('alice at tea.wonderland.lit')
+ romeo = Vines::JID.new('romeo at cake.verona.lit')
+ assert config.allowed?(alice, romeo)
+ assert config.allowed?(romeo, alice)
+ end
+
+ def test_cross_domain_component_to_component_not_allowed
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ cross_domain_messages true
+ storage(:fs) { dir Dir.tmpdir }
+ components 'tea' => 'secr3t', cake: 'passw0rd'
+ end
+ host 'verona.lit' do
+ cross_domain_messages false
+ storage(:fs) { dir Dir.tmpdir }
+ components 'party' => 'secr3t'
+ end
+ end
+ alice = Vines::JID.new('alice at tea.wonderland.lit')
+ romeo = Vines::JID.new('romeo at party.verona.lit')
+ refute config.allowed?(alice, romeo)
+ refute config.allowed?(romeo, alice)
+ end
+
+ def test_same_domain_user_to_component_allowed
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ cross_domain_messages false
+ storage(:fs) { dir Dir.tmpdir }
+ components 'tea' => 'secr3t', cake: 'passw0rd'
+ end
+ end
+ alice = Vines::JID.new('alice at wonderland.lit')
+ comp = Vines::JID.new('hatter at cake.wonderland.lit')
+ assert config.allowed?(alice, comp)
+ assert config.allowed?(comp, alice)
+ end
+
+ def test_cross_domain_user_to_component_allowed
+ config = Vines::Config.new do
+ host 'wonderland.lit', 'verona.lit' do
+ cross_domain_messages true
+ storage(:fs) { dir Dir.tmpdir }
+ components 'tea' => 'secr3t', cake: 'passw0rd'
+ end
+ end
+ alice = Vines::JID.new('alice at tea.wonderland.lit')
+ romeo = Vines::JID.new('romeo at verona.lit')
+ assert config.allowed?(alice, romeo)
+ assert config.allowed?(romeo, alice)
+ end
+
+ def test_cross_domain_user_to_component_not_allowed
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ cross_domain_messages true
+ storage(:fs) { dir Dir.tmpdir }
+ components 'tea' => 'secr3t', cake: 'passw0rd'
+ end
+ host 'verona.lit' do
+ cross_domain_messages false
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ alice = Vines::JID.new('alice at tea.wonderland.lit')
+ romeo = Vines::JID.new('romeo at verona.lit')
+ refute config.allowed?(alice, romeo)
+ refute config.allowed?(romeo, alice)
+ end
+
+ def test_remote_user_to_component_allowed
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ cross_domain_messages true
+ storage(:fs) { dir Dir.tmpdir }
+ components 'tea' => 'secr3t', cake: 'passw0rd'
+ end
+ host 'verona.lit' do
+ cross_domain_messages false
+ storage(:fs) { dir Dir.tmpdir }
+ components 'tea' => 'secr3t', cake: 'passw0rd'
+ end
+ end
+ alice = Vines::JID.new('alice at tea.wonderland.lit')
+ romeo = Vines::JID.new('romeo at tea.verona.lit')
+ hamlet = Vines::JID.new('hamlet at denmark.lit')
+ assert config.allowed?(alice, hamlet)
+ assert config.allowed?(hamlet, alice)
+ refute config.allowed?(romeo, hamlet)
+ refute config.allowed?(hamlet, romeo)
+ end
+
+ def test_same_domain_user_to_pubsub_allowed
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ cross_domain_messages false
+ storage(:fs) { dir Dir.tmpdir }
+ pubsub 'games'
+ end
+ end
+ alice = Vines::JID.new('alice at wonderland.lit')
+ pubsub = Vines::JID.new('games.wonderland.lit')
+ assert config.allowed?(alice, pubsub)
+ assert config.allowed?(pubsub, alice)
+ end
+
+ def test_cross_domain_user_to_pubsub_not_allowed
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ cross_domain_messages true
+ storage(:fs) { dir Dir.tmpdir }
+ pubsub 'games'
+ end
+ host 'verona.lit' do
+ cross_domain_messages false
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ pubsub = Vines::JID.new('games.wonderland.lit')
+ romeo = Vines::JID.new('romeo at verona.lit')
+ refute config.allowed?(pubsub, romeo)
+ refute config.allowed?(romeo, pubsub)
+ end
+
+ def test_remote_user_to_pubsub_allowed
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ cross_domain_messages true
+ storage(:fs) { dir Dir.tmpdir }
+ pubsub 'games'
+ end
+ host 'verona.lit' do
+ cross_domain_messages false
+ storage(:fs) { dir Dir.tmpdir }
+ pubsub 'games'
+ end
+ end
+ wonderland = Vines::JID.new('games.wonderland.lit')
+ verona = Vines::JID.new('games.verona.lit')
+ hamlet = Vines::JID.new('hamlet at denmark.lit')
+ assert config.allowed?(wonderland, hamlet)
+ assert config.allowed?(hamlet, wonderland)
+ refute config.allowed?(verona, hamlet)
+ refute config.allowed?(hamlet, verona)
+ end
+
+ def test_remote_user_to_local_user_allowed
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ cross_domain_messages true
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ host 'verona.lit' do
+ cross_domain_messages false
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ alice = Vines::JID.new('alice at wonderland.lit')
+ romeo = Vines::JID.new('romeo at verona.lit')
+ hamlet = Vines::JID.new('hamlet at denmark.lit')
+ assert config.allowed?(alice, hamlet)
+ assert config.allowed?(hamlet, alice)
+ refute config.allowed?(romeo, hamlet)
+ refute config.allowed?(hamlet, romeo)
+ end
+
+ def test_remote_user_to_remote_user_not_allowed
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ cross_domain_messages true
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ romeo = Vines::JID.new('romeo at verona.lit')
+ hamlet = Vines::JID.new('hamlet at denmark.lit')
+ refute config.allowed?(romeo, hamlet)
+ refute config.allowed?(hamlet, romeo)
+ end
+end
diff --git a/test/contact_test.rb b/test/contact_test.rb
new file mode 100644
index 0000000..cbda9da
--- /dev/null
+++ b/test/contact_test.rb
@@ -0,0 +1,102 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Contact do
+ subject do
+ Vines::Contact.new(
+ jid: 'alice at wonderland.lit',
+ name: "Alice",
+ groups: %w[Friends Buddies],
+ subscription: 'from')
+ end
+
+ describe 'contact equality checks' do
+ let(:alice) { Vines::Contact.new(jid: 'alice at wonderland.lit') }
+ let(:hatter) { Vines::Contact.new(jid: 'hatter at wonderland.lit') }
+
+ it 'uses class in equality check' do
+ (subject <=> 42).must_be_nil
+ end
+
+ it 'is equal to itself' do
+ assert subject == subject
+ assert subject.eql?(subject)
+ assert subject.hash == subject.hash
+ end
+
+ it 'is equal to another contact with the same jid' do
+ assert subject == alice
+ assert subject.eql?(alice)
+ assert subject.hash == alice.hash
+ end
+
+ it 'is not equal to a different jid' do
+ refute subject == hatter
+ refute subject.eql?(hatter)
+ refute subject.hash == hatter.hash
+ end
+ end
+
+ describe 'initialize' do
+ it 'raises when not given a jid' do
+ -> { Vines::Contact.new }.must_raise ArgumentError
+ -> { Vines::Contact.new(jid: '') }.must_raise ArgumentError
+ end
+
+ it 'accepts a domain-only jid' do
+ contact = Vines::Contact.new(jid: 'tea.wonderland.lit')
+ contact.jid.to_s.must_equal 'tea.wonderland.lit'
+ end
+ end
+
+ describe '#to_roster_xml' do
+ let(:expected) do
+ node(%q{
+ <item jid="alice at wonderland.lit" name="Alice" subscription="from" from_diaspora="false">
+ <group>Buddies</group>
+ <group>Friends</group>
+ </item>
+ })
+ end
+
+ it 'sorts group names' do
+ subject.to_roster_xml.must_equal expected
+ end
+ end
+
+ describe '#send_roster_push' do
+ let(:recipient) { MiniTest::Mock.new }
+ let(:expected) do
+ node(%q{
+ <iq to="hatter at wonderland.lit" type="set">
+ <query xmlns="jabber:iq:roster">
+ <item jid="alice at wonderland.lit" name="Alice" subscription="from" from_diaspora="false">
+ <group>Buddies</group>
+ <group>Friends</group>
+ </item>
+ </query>
+ </iq>
+ })
+ end
+
+ before do
+ recipient.expect :user, Vines::User.new(jid: 'hatter at wonderland.lit')
+ class << recipient
+ attr_accessor :nodes
+ def write(node)
+ @nodes ||= []
+ @nodes << node
+ end
+ end
+ end
+
+ it '' do
+ subject.send_roster_push(recipient)
+ recipient.verify
+ recipient.nodes.size.must_equal 1
+ recipient.nodes.first.remove_attribute('id') # id is random
+ recipient.nodes.first.must_equal expected
+ end
+ end
+end
diff --git a/test/error_test.rb b/test/error_test.rb
new file mode 100644
index 0000000..0238e4d
--- /dev/null
+++ b/test/error_test.rb
@@ -0,0 +1,58 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::XmppError do
+ describe Vines::SaslErrors do
+ it 'does not require a text element' do
+ expected = %q{<failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><temporary-auth-failure/></failure>}
+ Vines::SaslErrors::TemporaryAuthFailure.new.to_xml.must_equal expected
+ end
+
+ it 'includes a text element when message is given' do
+ text = %q{<text xml:lang="en">busted</text>}
+ expected = %q{<failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><temporary-auth-failure/>%s</failure>} % text
+ Vines::SaslErrors::TemporaryAuthFailure.new('busted').to_xml.must_equal expected
+ end
+ end
+
+ describe Vines::StreamErrors do
+ it 'does not require a text element' do
+ expected = %q{<stream:error><internal-server-error xmlns="urn:ietf:params:xml:ns:xmpp-streams"/></stream:error>}
+ Vines::StreamErrors::InternalServerError.new.to_xml.must_equal expected
+ end
+
+ it 'includes a text element when message is given' do
+ text = %q{<text xmlns="urn:ietf:params:xml:ns:xmpp-streams" xml:lang="en">busted</text>}
+ expected = %q{<stream:error><internal-server-error xmlns="urn:ietf:params:xml:ns:xmpp-streams"/>%s</stream:error>} % text
+ Vines::StreamErrors::InternalServerError.new('busted').to_xml.must_equal expected
+ end
+ end
+
+ describe Vines::StanzaErrors do
+ it 'raises when given a bad type' do
+ node = node('<message/>')
+ -> { Vines::StanzaErrors::BadRequest.new(node, 'bogus') }.must_raise RuntimeError
+ end
+
+ it 'raises when given a bad stanza' do
+ node = node('<bogus/>')
+ -> { Vines::StanzaErrors::BadRequest.new(node, 'modify') }.must_raise RuntimeError
+ end
+
+ it 'does not require a text element' do
+ error = %q{<error type="modify"><bad-request xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/></error>}
+ expected = %q{<message from="hatter at wonderland.lit" to="alice at wonderland.lit" type="error">%s</message>} % error
+ node = node(%Q{<message from="alice at wonderland.lit" to="hatter at wonderland.lit"/>})
+ Vines::StanzaErrors::BadRequest.new(node, 'modify').to_xml.must_equal expected
+ end
+
+ it 'includes a text element when message is given' do
+ text = %q{<text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" xml:lang="en">busted</text>}
+ error = %q{<error type="modify"><bad-request xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>%s</error>} % text
+ expected = %q{<message id="42" type="error">%s</message>} % error
+ node = node(%Q{<message id="42"/>})
+ Vines::StanzaErrors::BadRequest.new(node, 'modify', 'busted').to_xml.must_equal expected
+ end
+ end
+end
diff --git a/test/ext/nokogiri.rb b/test/ext/nokogiri.rb
new file mode 100644
index 0000000..80641f8
--- /dev/null
+++ b/test/ext/nokogiri.rb
@@ -0,0 +1,14 @@
+# encoding: UTF-8
+
+module Nokogiri
+ module XML
+ class Node
+ # Override equality testing so we can use MiniTest::Mock#expect with
+ # Nokogiri::XML::Node arguments. Node's default behavior considers
+ # all nodes unequal.
+ def ==(node)
+ self.to_s == node.to_s
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/test/jid_test.rb b/test/jid_test.rb
new file mode 100644
index 0000000..479af44
--- /dev/null
+++ b/test/jid_test.rb
@@ -0,0 +1,147 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::JID do
+ it 'handles empty input' do
+ [nil, ''].each do |text|
+ Vines::JID.new(text) # shouldn't raise an error
+ jid = Vines::JID.new(text)
+ assert_nil jid.node
+ assert_nil jid.resource
+ assert_equal '', jid.domain
+ assert_equal '', jid.to_s
+ assert_equal '', jid.bare.to_s
+ assert jid.empty?
+ refute jid.domain?
+ end
+ end
+
+ it 'raises when a jid part is too long' do
+ Vines::JID.new('n' * 1023) # shouldn't raise an error
+ assert_raises(ArgumentError) { Vines::JID.new('n' * 1024) }
+ assert_raises(ArgumentError) { Vines::JID.new('n', 'd' * 1024) }
+ assert_raises(ArgumentError) { Vines::JID.new('n', 'd', 'r' * 1024) }
+ Vines::JID.new('n' * 1023, 'd' * 1023, 'r' * 1023) # shouldn't raise an error
+ end
+
+ it 'correctly handles domain only jids' do
+ jid = Vines::JID.new('wonderland.lit')
+ assert_equal 'wonderland.lit', jid.to_s
+ assert_equal 'wonderland.lit', jid.domain
+ assert_nil jid.node
+ assert_nil jid.resource
+ assert_equal jid, jid.bare
+ assert jid.domain?
+ refute jid.empty?
+ end
+
+ it 'correctly handles bare jid components' do
+ jid = Vines::JID.new('alice', 'wonderland.lit')
+ assert_equal 'alice at wonderland.lit', jid.to_s
+ assert_equal 'wonderland.lit', jid.domain
+ assert_equal 'alice', jid.node
+ assert_nil jid.resource
+ assert_equal jid, jid.bare
+ refute jid.domain?
+ refute jid.empty?
+ end
+
+ it 'correctly parses bare jids' do
+ jid = Vines::JID.new('alice at wonderland.lit')
+ assert_equal 'alice at wonderland.lit', jid.to_s
+ assert_equal 'wonderland.lit', jid.domain
+ assert_equal 'alice', jid.node
+ assert_nil jid.resource
+ assert_equal jid, jid.bare
+ refute jid.domain?
+ refute jid.empty?
+ end
+
+ it 'correctly handles full jid components' do
+ jid = Vines::JID.new('alice', 'wonderland.lit', 'tea')
+ assert_equal 'alice at wonderland.lit/tea', jid.to_s
+ assert_equal 'wonderland.lit', jid.domain
+ assert_equal 'alice', jid.node
+ assert_equal 'tea', jid.resource
+ refute_equal jid, jid.bare
+ refute jid.domain?
+ refute jid.empty?
+ end
+
+ it 'correctly parses full jids' do
+ jid = Vines::JID.new('alice at wonderland.lit/tea')
+ assert_equal 'alice at wonderland.lit/tea', jid.to_s
+ assert_equal 'wonderland.lit', jid.domain
+ assert_equal 'alice', jid.node
+ assert_equal 'tea', jid.resource
+ refute_equal jid, jid.bare
+ refute jid.domain?
+ refute jid.empty?
+ end
+
+ it 'accepts separator characters in resource part' do
+ jid = Vines::JID.new('alice at wonderland.lit/foo/bar at blarg test')
+ assert_equal 'alice', jid.node
+ assert_equal 'wonderland.lit', jid.domain
+ assert_equal 'foo/bar at blarg test', jid.resource
+ end
+
+ it 'accepts separator characters in resource part with missing node part' do
+ jid = Vines::JID.new('wonderland.lit/foo/bar at blarg')
+ assert_nil jid.node
+ assert_equal 'wonderland.lit', jid.domain
+ assert_equal 'foo/bar at blarg', jid.resource
+ refute jid.domain?
+ end
+
+ it 'accepts strange characters in node part' do
+ jid = Vines::JID.new(%q{nasty!#$%()*+,-.;=?[\]^_`{|}~node at example.com})
+ jid.node.must_equal %q{nasty!#$%()*+,-.;=?[\]^_`{|}~node}
+ jid.domain.must_equal 'example.com'
+ jid.resource.must_be_nil
+ end
+
+ it 'accepts strange characters in resource part' do
+ jid = Vines::JID.new(%q{node at example.com/repulsive !#"$%&'()*+,-./:;<=>?@[\]^_`{|}~resource})
+ jid.node.must_equal 'node'
+ jid.domain.must_equal 'example.com'
+ jid.resource.must_equal %q{repulsive !#"$%&'()*+,-./:;<=>?@[\]^_`{|}~resource}
+ end
+
+ it 'rejects empty jid parts' do
+ assert_raises(ArgumentError) { Vines::JID.new('@wonderland.lit') }
+ assert_raises(ArgumentError) { Vines::JID.new('wonderland.lit/') }
+ assert_raises(ArgumentError) { Vines::JID.new('@') }
+ assert_raises(ArgumentError) { Vines::JID.new('alice@') }
+ assert_raises(ArgumentError) { Vines::JID.new('/') }
+ assert_raises(ArgumentError) { Vines::JID.new('/res') }
+ assert_raises(ArgumentError) { Vines::JID.new('@/') }
+ end
+
+ it 'rejects invalid characters' do
+ assert_raises(ArgumentError) { Vines::JID.new(%q{alice"s at wonderland.lit}) }
+ assert_raises(ArgumentError) { Vines::JID.new(%q{alice&s at wonderland.lit}) }
+ assert_raises(ArgumentError) { Vines::JID.new(%q{alice's at wonderland.lit}) }
+ assert_raises(ArgumentError) { Vines::JID.new(%q{alice:s at wonderland.lit}) }
+ assert_raises(ArgumentError) { Vines::JID.new(%q{alice<s at wonderland.lit}) }
+ assert_raises(ArgumentError) { Vines::JID.new(%q{alice>s at wonderland.lit}) }
+ assert_raises(ArgumentError) { Vines::JID.new("alice\u0000s at wonderland.lit") }
+ assert_raises(ArgumentError) { Vines::JID.new("alice\ts at wonderland.lit") }
+ assert_raises(ArgumentError) { Vines::JID.new("alice\rs at wonderland.lit") }
+ assert_raises(ArgumentError) { Vines::JID.new("alice\ns at wonderland.lit") }
+ assert_raises(ArgumentError) { Vines::JID.new("alice\vs at wonderland.lit") }
+ assert_raises(ArgumentError) { Vines::JID.new("alice\fs at wonderland.lit") }
+ assert_raises(ArgumentError) { Vines::JID.new(" alice at wonderland.lit") }
+ assert_raises(ArgumentError) { Vines::JID.new("alice at wonderland.lit ") }
+ assert_raises(ArgumentError) { Vines::JID.new("alice s at wonderland.lit") }
+ assert_raises(ArgumentError) { Vines::JID.new("alice at w onderland.lit") }
+ assert_raises(ArgumentError) { Vines::JID.new("alice at w\tonderland.lit") }
+ assert_raises(ArgumentError) { Vines::JID.new("alice at w\ronderland.lit") }
+ assert_raises(ArgumentError) { Vines::JID.new("alice at w\nonderland.lit") }
+ assert_raises(ArgumentError) { Vines::JID.new("alice at w\vonderland.lit") }
+ assert_raises(ArgumentError) { Vines::JID.new("alice at w\fonderland.lit") }
+ assert_raises(ArgumentError) { Vines::JID.new("alice at w\u0000onderland.lit") }
+ assert_raises(ArgumentError) { Vines::JID.new("alice at wonderland.lit/\u0000res") }
+ end
+end
diff --git a/test/kit_test.rb b/test/kit_test.rb
new file mode 100644
index 0000000..ae72d71
--- /dev/null
+++ b/test/kit_test.rb
@@ -0,0 +1,31 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Kit do
+ describe '#hmac' do
+ it 'generates a SHA-512 HMAC' do
+ Vines::Kit.hmac('secret', 'username').length.must_equal 128
+ assert_equal Vines::Kit.hmac('s1', 'u1'), Vines::Kit.hmac('s1', 'u1')
+ refute_equal Vines::Kit.hmac('s1', 'u1'), Vines::Kit.hmac('s2', 'u1')
+ refute_equal Vines::Kit.hmac('s1', 'u1'), Vines::Kit.hmac('s1', 'u2')
+ end
+ end
+
+ describe '#uuid' do
+ it 'returns a random uuid' do
+ ids = Array.new(1000) { Vines::Kit.uuid }
+ assert ids.all? {|id| !id.nil? }
+ assert ids.all? {|id| id.length == 36 }
+ assert ids.all? {|id| id.match(/\w{8}-\w{4}-[4]\w{3}-[89ab]\w{3}-\w{12}/) }
+ ids.uniq.length.must_equal ids.length
+ end
+ end
+
+ describe '#auth_token' do
+ it 'returns a random 128 character token' do
+ Vines::Kit.auth_token.wont_equal Vines::Kit.auth_token
+ Vines::Kit.auth_token.length.must_equal 128
+ end
+ end
+end
diff --git a/test/router_test.rb b/test/router_test.rb
new file mode 100644
index 0000000..d681256
--- /dev/null
+++ b/test/router_test.rb
@@ -0,0 +1,243 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Router do
+ subject { Vines::Router.new(config) }
+ let(:alice) { Vines::JID.new('alice at wonderland.lit/tea') }
+ let(:hatter) { 'hatter at wonderland.lit/cake' }
+ let(:romeo) { 'romeo at verona.lit/party' }
+ let(:config) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ components 'tea' => 'secr3t'
+ end
+ end
+ end
+
+ describe '#connected_resources' do
+ let(:cake) { 'alice at wonderland.lit/cake' }
+ let(:stream1) { stream(alice) }
+ let(:stream2) { stream(cake) }
+
+ it 'is empty before any streams are connected' do
+ subject.connected_resources(alice, alice).size.must_equal 0
+ subject.connected_resources(cake, alice).size.must_equal 0
+ subject.size.must_equal 0
+ end
+
+ it 'returns only one stream matching full jid' do
+ subject << stream1
+ subject << stream2
+
+ streams = subject.connected_resources(alice, alice)
+ streams.size.must_equal 1
+ streams.first.user.jid.must_equal alice
+
+ streams = subject.connected_resources(cake, alice)
+ streams.size.must_equal 1
+ streams.first.user.jid.to_s.must_equal cake
+ end
+
+ it 'returns all streams matching bare jid' do
+ subject << stream1
+ subject << stream2
+
+ streams = subject.connected_resources(alice.bare, alice)
+ streams.size.must_equal 2
+ subject.size.must_equal 2
+ end
+ end
+
+ describe '#connected_resources with permissions' do
+ let(:stream1) { stream(alice) }
+ let(:stream2) { stream(romeo) }
+
+ before do
+ subject << stream1
+ subject << stream2
+ end
+
+ it 'denies access when cross domain messages is off' do
+ subject.connected_resources(alice, romeo).size.must_equal 0
+ end
+
+ it 'allows access when cross domain messages is on' do
+ config.vhost('wonderland.lit').cross_domain_messages true
+ subject.connected_resources(alice, romeo).size.must_equal 1
+ end
+ end
+
+ describe '#available_resources' do
+ let(:cake) { 'alice at wonderland.lit/cake' }
+ let(:stream1) { stream(alice) }
+ let(:stream2) { stream(cake) }
+
+ before do
+ stream1.send 'available?=', true
+ stream2.send 'available?=', false
+ end
+
+ it 'is empty before any streams are connected' do
+ subject.available_resources(alice, alice).size.must_equal 0
+ subject.available_resources(cake, alice).size.must_equal 0
+ subject.size.must_equal 0
+ end
+
+ it 'returns available streams based on bare jid, not full jid' do
+ subject << stream1
+ subject << stream2
+
+ streams = [alice, cake, alice.bare].map do |jid|
+ subject.available_resources(jid, alice)
+ end.flatten
+
+ # should only have found alice's stream
+ streams.size.must_equal 3
+ streams.uniq.size.must_equal 1
+ streams.first.user.jid.must_equal alice
+
+ subject.size.must_equal 2
+ end
+ end
+
+ describe '#interested_resources with no streams' do
+ it 'is empty before any streams are connected' do
+ subject.interested_resources(alice, alice).size.must_equal 0
+ subject.interested_resources(hatter, alice).size.must_equal 0
+ subject.interested_resources(alice, hatter, alice).size.must_equal 0
+ subject.size.must_equal 0
+ end
+ end
+
+ describe '#interested_resources' do
+ let(:stream1) { stream(alice) }
+ let(:stream2) { stream(hatter) }
+
+ before do
+ stream1.send 'interested?=', true
+ stream2.send 'interested?=', false
+ subject << stream1
+ subject << stream2
+ end
+
+ it 'does not find streams for unauthenticated jids' do
+ subject.interested_resources('bogus at wonderland.lit', alice).size.must_equal 0
+ end
+
+ it 'finds interested streams for full jids' do
+ subject.interested_resources(alice, hatter, alice).size.must_equal 1
+ subject.interested_resources([alice, hatter], alice).size.must_equal 1
+ subject.interested_resources(alice, hatter, alice)[0].user.jid.must_equal alice
+ end
+
+ it 'does not find streams for uninterested jids' do
+ subject.interested_resources(hatter, alice).size.must_equal 0
+ subject.interested_resources([hatter], alice).size.must_equal 0
+ end
+
+ it 'finds interested streams for bare jids' do
+ subject.interested_resources(alice.bare, alice).size.must_equal 1
+ subject.interested_resources(alice.bare, alice)[0].user.jid.must_equal alice
+ end
+ end
+
+ describe '#delete' do
+ let(:stream1) { stream(alice) }
+ let(:stream2) { stream(hatter) }
+
+ it 'correctly adds and removes streams' do
+ subject.size.must_equal 0
+
+ subject << stream1
+ subject << stream2
+ subject.size.must_equal 2
+
+ subject.delete(stream2)
+ subject.size.must_equal 1
+
+ subject.delete(stream2)
+ subject.size.must_equal 1
+
+ subject.delete(stream1)
+ subject.size.must_equal 0
+ end
+ end
+
+ describe 'load balanced component streams' do
+ let(:stream1) { component('tea.wonderland.lit') }
+ let(:stream2) { component('tea.wonderland.lit') }
+ let(:stanza) { node('<message from="alice at wonderland.lit" to="tea.wonderland.lit">test</message>')}
+
+ before do
+ subject << stream1
+ subject << stream2
+ end
+
+ it 'must evenly distribute routed stanzas to both streams' do
+ 100.times { subject.route(stanza) }
+
+ (stream1.count + stream2.count).must_equal 100
+ stream1.count.must_be :>, 33
+ stream2.count.must_be :>, 33
+ end
+ end
+
+ describe 'load balanced s2s streams' do
+ let(:stream1) { s2s('wonderland.lit', 'verona.lit') }
+ let(:stream2) { s2s('wonderland.lit', 'verona.lit') }
+ let(:stanza) { node('<message from="alice at wonderland.lit" to="romeo at verona.lit">test</message>') }
+
+ before do
+ config.vhost('wonderland.lit').cross_domain_messages true
+ subject << stream1
+ subject << stream2
+ end
+
+ it 'must evenly distribute routed stanzas to both streams' do
+ 100.times { subject.route(stanza) }
+
+ (stream1.count + stream2.count).must_equal 100
+ stream1.count.must_be :>, 33
+ stream2.count.must_be :>, 33
+ end
+ end
+
+ private
+
+ def stream(jid)
+ OpenStruct.new.tap do |stream|
+ stream.send('connected?=', true)
+ stream.stream_type = :client
+ stream.user = Vines::User.new(jid: jid)
+ end
+ end
+
+ def component(jid)
+ OpenStruct.new.tap do |stream|
+ stream.stream_type = :component
+ stream.remote_domain = jid
+ stream.send('ready?=', true)
+ def stream.count; @count || 0; end
+ def stream.write(stanza)
+ @count ||= 0
+ @count += 1
+ end
+ end
+ end
+
+ def s2s(domain, remote_domain)
+ OpenStruct.new.tap do |stream|
+ stream.stream_type = :server
+ stream.domain = domain
+ stream.remote_domain = remote_domain
+ stream.send('ready?=', true)
+ def stream.count; @count || 0; end
+ def stream.write(stanza)
+ @count ||= 0
+ @count += 1
+ end
+ end
+ end
+end
diff --git a/test/stanza/iq/disco_info_test.rb b/test/stanza/iq/disco_info_test.rb
new file mode 100644
index 0000000..ad7896a
--- /dev/null
+++ b/test/stanza/iq/disco_info_test.rb
@@ -0,0 +1,80 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stanza::Iq::DiscoInfo do
+ subject { Vines::Stanza::Iq::DiscoInfo.new(xml, stream) }
+ let(:stream) { MiniTest::Mock.new }
+ let(:alice) { Vines::User.new(jid: 'alice at wonderland.lit/home') }
+ let(:config) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ end
+
+ let(:xml) do
+ query = %q{<query xmlns="http://jabber.org/protocol/disco#info"/>}
+ node(%Q{<iq id="42" to="wonderland.lit" type="get">#{query}</iq>})
+ end
+
+ before do
+ class << stream
+ attr_accessor :config, :user
+ end
+ stream.config = config
+ stream.user = alice
+ end
+
+ describe 'when private storage is disabled' do
+ let(:expected) do
+ node(%Q{
+ <iq from="wonderland.lit" id="42" to="#{alice.jid}" type="result">
+ <query xmlns="http://jabber.org/protocol/disco#info">
+ <identity category="server" type="im"/>
+ <feature var="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/disco#items"/>
+ <feature var="msgoffline"/>
+ <feature var="urn:xmpp:ping"/>
+ <feature var="vcard-temp"/>
+ <feature var="jabber:iq:version"/>
+ </query>
+ </iq>
+ })
+ end
+
+ it 'returns info stanza without the private storage feature' do
+ config.vhost('wonderland.lit').private_storage false
+ stream.expect :write, nil, [expected]
+ subject.process
+ stream.verify
+ end
+ end
+
+ describe 'when private storage is enabled' do
+ let(:expected) do
+ node(%Q{
+ <iq from="wonderland.lit" id="42" to="#{alice.jid}" type="result">
+ <query xmlns="http://jabber.org/protocol/disco#info">
+ <identity category="server" type="im"/>
+ <feature var="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/disco#items"/>
+ <feature var="msgoffline"/>
+ <feature var="urn:xmpp:ping"/>
+ <feature var="vcard-temp"/>
+ <feature var="jabber:iq:version"/>
+ <feature var="jabber:iq:private"/>
+ </query>
+ </iq>
+ })
+ end
+
+ it 'announces private storage feature in info stanza result' do
+ config.vhost('wonderland.lit').private_storage true
+ stream.expect :write, nil, [expected]
+ subject.process
+ stream.verify
+ end
+ end
+end
diff --git a/test/stanza/iq/disco_items_test.rb b/test/stanza/iq/disco_items_test.rb
new file mode 100644
index 0000000..baaa52f
--- /dev/null
+++ b/test/stanza/iq/disco_items_test.rb
@@ -0,0 +1,49 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stanza::Iq::DiscoItems do
+ subject { Vines::Stanza::Iq::DiscoItems.new(xml, stream) }
+ let(:stream) { MiniTest::Mock.new }
+ let(:alice) { Vines::User.new(jid: 'alice at wonderland.lit/home') }
+ let(:config) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ components 'tea' => 'secr3t', 'cake' => 'passw0rd'
+ end
+ end
+ end
+
+ before do
+ class << stream
+ attr_accessor :config, :user
+ end
+ stream.config = config
+ stream.user = alice
+ end
+
+ describe 'when querying server items' do
+ let(:xml) do
+ query = %q{<query xmlns="http://jabber.org/protocol/disco#items"/>}
+ node(%Q{<iq id="42" to="wonderland.lit" type="get">#{query}</iq>})
+ end
+
+ let(:result) do
+ node(%q{
+ <iq from="wonderland.lit" id="42" to="alice at wonderland.lit/home" type="result">
+ <query xmlns="http://jabber.org/protocol/disco#items">
+ <item jid="cake.wonderland.lit"/>
+ <item jid="tea.wonderland.lit"/>
+ </query>
+ </iq>
+ })
+ end
+
+ it 'includes component domains in output' do
+ stream.expect :write, nil, [result]
+ subject.process
+ stream.verify
+ end
+ end
+end
diff --git a/test/stanza/iq/private_storage_test.rb b/test/stanza/iq/private_storage_test.rb
new file mode 100644
index 0000000..7029c88
--- /dev/null
+++ b/test/stanza/iq/private_storage_test.rb
@@ -0,0 +1,184 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stanza::Iq::PrivateStorage do
+ subject { Vines::Stanza::Iq::PrivateStorage.new(xml, stream) }
+ let(:alice) { Vines::User.new(jid: 'alice at wonderland.lit/tea') }
+ let(:storage) { MiniTest::Mock.new }
+ let(:stream) { MiniTest::Mock.new }
+ let(:config) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ private_storage true
+ end
+ end
+ end
+
+ before do
+ class << stream
+ attr_accessor :config, :domain, :user
+ end
+ stream.config = config
+ stream.user = alice
+ stream.domain = 'wonderland.lit'
+ end
+
+ describe 'when private storage feature is disabled' do
+ let(:xml) do
+ query = %q{<query xmlns="jabber:iq:private"><one xmlns="a"/></query>}
+ node(%Q{<iq id="42" type="get">#{query}</iq>})
+ end
+
+ before do
+ config.vhost('wonderland.lit').private_storage false
+ end
+
+ it 'raises a service-unavailable stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::ServiceUnavailable
+ stream.verify
+ end
+ end
+
+ describe 'when retrieving a fragment for another user jid' do
+ let(:xml) do
+ query = %q{<query xmlns="jabber:iq:private"><one xmlns="a"/></query>}
+ node(%Q{<iq id="42" to="hatter at wonderland.lit" type="get">#{query}</iq>})
+ end
+
+ it 'raises a forbidden stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::Forbidden
+ stream.verify
+ end
+ end
+
+ describe 'when get stanza contains zero child elements' do
+ let(:xml) do
+ query = %q{<query xmlns="jabber:iq:private"></query>}
+ node(%Q{<iq id="42" type="get">#{query}</iq>})
+ end
+
+ it 'raises a not-acceptable stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::NotAcceptable
+ stream.verify
+ end
+ end
+
+ describe 'when get stanza contains more than one child element' do
+ let(:xml) do
+ query = %q{<query xmlns="jabber:iq:private"><one xmlns="a"/><two xmlns="b"/></query>}
+ node(%Q{<iq id="42" type="get">#{query}</iq>})
+ end
+
+ it 'raises a not-acceptable stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::NotAcceptable
+ stream.verify
+ end
+ end
+
+ describe 'when get stanza is missing a namespace' do
+ let(:xml) do
+ query = %q{<query xmlns="jabber:iq:private"><one/></query>}
+ node = node(%Q{<iq id="42" type="get">#{query}</iq>})
+ end
+
+ it 'raises a not-acceptable stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::NotAcceptable
+ stream.verify
+ end
+ end
+
+ describe 'when get stanza is missing fragment' do
+ let(:xml) do
+ query = %q{<query xmlns="jabber:iq:private"><one xmlns="a"/></query>}
+ node(%Q{<iq id="42" type="get">#{query}</iq>})
+ end
+
+ before do
+ storage.expect :find_fragment, nil, [alice.jid, xml.elements[0].elements[0]]
+ stream.expect :storage, storage, ['wonderland.lit']
+ end
+
+ it 'raises an item-not-found stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::ItemNotFound
+ stream.verify
+ storage.verify
+ end
+ end
+
+ describe 'when get finds fragment successfully' do
+ let(:xml) do
+ query = %q{<query xmlns="jabber:iq:private"><one xmlns="a"/></query>}
+ node = node(%Q{<iq id="42" type="get">#{query}</iq>})
+ end
+
+ before do
+ data = %q{<one xmlns="a"><child>data</child></one>}
+ query = %Q{<query xmlns="jabber:iq:private">#{data}</query>}
+ expected = node(%Q{<iq from="#{alice.jid}" id="42" to="#{alice.jid}" type="result">#{query}</iq>})
+
+ storage.expect :find_fragment, node(data), [alice.jid, xml.elements[0].elements[0]]
+ stream.expect :storage, storage, ['wonderland.lit']
+ stream.expect :write, nil, [expected]
+ end
+
+ it 'writes a response to the stream' do
+ subject.process
+ stream.verify
+ storage.verify
+ end
+ end
+
+ describe 'when saving a fragment' do
+ let(:result) { node(%Q{<iq from="#{alice.jid}" id="42" to="#{alice.jid}" type="result"/>}) }
+
+ before do
+ storage.expect :save_fragment, nil, [alice.jid, xml.elements[0].elements[0]]
+ stream.expect :storage, storage, ['wonderland.lit']
+ stream.expect :write, nil, [result]
+ end
+
+ describe 'and stanza contains zero child elements' do
+ let(:xml) do
+ query = %q{<query xmlns="jabber:iq:private"></query>}
+ node(%Q{<iq id="42" type="set">#{query}</iq>})
+ end
+
+ it 'raises a not-acceptable stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::NotAcceptable
+ end
+ end
+
+ describe 'and a single single fragment saves successfully' do
+ let(:xml) do
+ query = %q{<query xmlns="jabber:iq:private"><one xmlns="a"/></query>}
+ node(%Q{<iq id="42" type="set">#{query}</iq>})
+ end
+
+ it 'writes a result to the stream' do
+ subject.process
+ stream.verify
+ storage.verify
+ end
+ end
+
+ describe 'and two fragments save successfully' do
+ let(:xml) do
+ query = %q{<query xmlns="jabber:iq:private"><one xmlns="a"/><two xmlns="a"/></query>}
+ node(%Q{<iq id="42" type="set">#{query}</iq>})
+ end
+
+ before do
+ storage.expect :save_fragment, nil, [alice.jid, xml.elements[0].elements[1]]
+ stream.expect :storage, storage, ['wonderland.lit']
+ end
+
+ it 'writes a result to the stream' do
+ subject.process
+ stream.verify
+ storage.verify
+ end
+ end
+ end
+end
diff --git a/test/stanza/iq/roster_test.rb b/test/stanza/iq/roster_test.rb
new file mode 100644
index 0000000..2fedc37
--- /dev/null
+++ b/test/stanza/iq/roster_test.rb
@@ -0,0 +1,229 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stanza::Iq::Roster do
+ subject { Vines::Stanza::Iq::Roster.new(xml, stream) }
+ let(:stream) { MiniTest::Mock.new }
+ let(:alice) { Vines::User.new(jid: 'alice at wonderland.lit/tea') }
+
+ before do
+ class << stream
+ attr_accessor :domain, :user
+ end
+ stream.user = alice
+ stream.domain = 'wonderland.lit'
+ end
+
+ describe 'when retrieving an empty roster' do
+ let(:xml) { node(%q{<iq id="42" type="get"><query xmlns='jabber:iq:roster'/></iq>}) }
+ let(:expected) { node(%q{<iq id="42" type="result"><query xmlns="jabber:iq:roster"/></iq>}) }
+
+ before do
+ stream.expect :write, nil, [expected]
+ stream.expect :requested_roster!, nil
+ end
+
+ it 'returns an empty stanza' do
+ subject.process
+ stream.verify
+ end
+ end
+
+ describe 'when retrieving a non-empty roster' do
+ let(:xml) { node(%q{<iq id="42" type="get"><query xmlns='jabber:iq:roster'/></iq>}) }
+ let(:expected) do
+ node(%q{
+ <iq id="42" type="result">
+ <query xmlns="jabber:iq:roster">
+ <item jid="cat at wonderland.lit" subscription="none" from_diaspora="false">
+ <group>Cats</group>
+ <group>Friends</group>
+ </item>
+ <item jid="hatter at wonderland.lit" subscription="none" from_diaspora="false"/>
+ </query>
+ </iq>})
+ end
+
+ before do
+ alice.roster << Vines::Contact.new(jid: 'hatter at wonderland.lit')
+ alice.roster << Vines::Contact.new(jid: 'cat at wonderland.lit', :groups => ['Friends', 'Cats'])
+
+ stream.expect :write, nil, [expected]
+ stream.expect :requested_roster!, nil
+ end
+
+ it 'sorts groups alphabetically' do
+ subject.process
+ stream.verify
+ end
+ end
+
+ describe 'when requesting a roster for another user' do
+ let(:xml) do
+ node(%q{
+ <iq id="42" type="get" to="romeo at verona.lit">
+ <query xmlns="jabber:iq:roster"/>
+ </iq>})
+ end
+
+ it 'raises a forbidden stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::Forbidden
+ stream.verify
+ end
+ end
+
+ describe 'when saving a roster for another user' do
+ let(:xml) do
+ node(%q{
+ <iq id="42" type="set" to="romeo at verona.lit">
+ <query xmlns="jabber:iq:roster"/>
+ </iq>})
+ end
+
+ it 'raises a forbidden stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::Forbidden
+ stream.verify
+ end
+ end
+
+ describe 'when saving a roster with no items' do
+ let(:xml) do
+ node(%q{
+ <iq id="42" type="set">
+ <query xmlns="jabber:iq:roster"/>
+ </iq>})
+ end
+
+ it 'raises a bad-request stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::BadRequest
+ stream.verify
+ end
+ end
+
+ describe 'when updating a roster with more than one item' do
+ let(:xml) do
+ node(%q{
+ <iq id="42" type="set">
+ <query xmlns="jabber:iq:roster">
+ <item jid="hatter at wonderland.lit"/>
+ <item jid="cat at wonderland.lit"/>
+ </query>
+ </iq>})
+ end
+
+ it 'raises a bad-request stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::BadRequest
+ stream.verify
+ end
+ end
+
+ describe 'when adding a roster item without a jid attribute' do
+ let(:xml) do
+ node(%q{
+ <iq id="42" type="set">
+ <query xmlns="jabber:iq:roster">
+ <item name="Mad Hatter"/>
+ </query>
+ </iq>})
+ end
+
+ it 'raises a bad-request stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::BadRequest
+ stream.verify
+ end
+ end
+
+ describe 'when adding a roster item with duplicate groups' do
+ let(:xml) do
+ node(%q{
+ <iq id="42" type="set">
+ <query xmlns="jabber:iq:roster">
+ <item jid="hatter at wonderland.lit" name="Mad Hatter">
+ <group>Friends</group>
+ <group>Friends</group>
+ </item>
+ </query>
+ </iq>})
+ end
+
+ it 'raises a bad-request stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::BadRequest
+ stream.verify
+ end
+ end
+
+ describe 'when adding a roster item with an empty group name' do
+ let(:xml) do
+ node(%q{
+ <iq id="42" type="set">
+ <query xmlns="jabber:iq:roster">
+ <item jid="hatter at wonderland.lit" name="Mad Hatter">
+ <group></group>
+ </item>
+ </query>
+ </iq>})
+ end
+
+ it 'raises a not-acceptable stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::NotAcceptable
+ stream.verify
+ end
+ end
+
+ describe 'when saving a roster successfully' do
+ let(:xml) do
+ node(%q{
+ <iq id="42" type="set">
+ <query xmlns="jabber:iq:roster">
+ <item jid="hatter at wonderland.lit" name="Mad Hatter">
+ <group>Friends</group>
+ </item>
+ </query>
+ </iq>})
+ end
+
+ let(:expected) do
+ node(%q{
+ <iq to="alice at wonderland.lit/tea" type="set">
+ <query xmlns="jabber:iq:roster">
+ <item jid="hatter at wonderland.lit" name="Mad Hatter" subscription="none" from_diaspora="false">
+ <group>Friends</group>
+ </item>
+ </query>
+ </iq>})
+ end
+
+ let(:storage) { MiniTest::Mock.new }
+ let(:recipient) { MiniTest::Mock.new }
+ let(:result) { node(%Q{<iq id="42" to="#{alice.jid}" type="result"/>}) }
+
+ before do
+ storage.expect :save_user, nil, [alice]
+
+ recipient.expect :user, alice
+ def recipient.nodes; @nodes; end
+ def recipient.write(node)
+ @nodes ||= []
+ @nodes << node
+ end
+
+ stream.expect :interested_resources, [recipient], [alice.jid]
+ stream.expect :update_user_streams, nil, [alice]
+ stream.expect :storage, storage, ['wonderland.lit']
+ stream.expect :write, nil, [result]
+ end
+
+ it 'sends a result to the sender' do
+ subject.process
+ stream.verify
+ storage.verify
+ end
+
+ it 'sends the new roster item to the interested streams' do
+ subject.process
+ recipient.nodes.first.remove_attribute('id') # id is random
+ recipient.nodes.first.must_equal expected
+ end
+ end
+end
diff --git a/test/stanza/iq/session_test.rb b/test/stanza/iq/session_test.rb
new file mode 100644
index 0000000..785f22a
--- /dev/null
+++ b/test/stanza/iq/session_test.rb
@@ -0,0 +1,25 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stanza::Iq::Session do
+ subject { Vines::Stanza::Iq::Session.new(xml, stream) }
+ let(:stream) { MiniTest::Mock.new }
+ let(:alice) { Vines::User.new(jid: 'alice at wonderland.lit/tea') }
+
+ describe 'when session initiation is requested' do
+ let(:xml) { node(%q{<iq id="42" type="set"><session xmlns="urn:ietf:params:xml:ns:xmpp-session"/></iq>}) }
+ let(:result) { node(%q{<iq from="wonderland.lit" id="42" to="alice at wonderland.lit/tea" type="result"/>}) }
+
+ before do
+ stream.expect :domain, 'wonderland.lit'
+ stream.expect :user, alice
+ stream.expect :write, nil, [result]
+ end
+
+ it 'just returns a result to satisy older clients' do
+ subject.process
+ stream.verify
+ end
+ end
+end
diff --git a/test/stanza/iq/vcard_test.rb b/test/stanza/iq/vcard_test.rb
new file mode 100644
index 0000000..5681521
--- /dev/null
+++ b/test/stanza/iq/vcard_test.rb
@@ -0,0 +1,146 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stanza::Iq::Vcard do
+ subject { Vines::Stanza::Iq::Vcard.new(xml, stream) }
+ let(:alice) { Vines::User.new(jid: 'alice at wonderland.lit/tea') }
+ let(:stream) { MiniTest::Mock.new }
+ let(:storage) { MiniTest::Mock.new }
+ let(:config) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ cross_domain_messages true
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ end
+
+ before do
+ class << stream
+ attr_accessor :config, :domain, :user
+ end
+ stream.config = config
+ stream.domain = 'wonderland.lit'
+ stream.user = alice
+ end
+
+ describe 'when getting vcard' do
+ describe 'and addressed to a remote jid' do
+ let(:xml) { get('romeo at verona.lit') }
+ let(:router) { MiniTest::Mock.new }
+
+ before do
+ router.expect :route, nil, [xml]
+ stream.expect :router, router
+ end
+
+ it 'routes rather than handle locally' do
+ subject.process
+ stream.verify
+ router.verify
+ end
+ end
+
+ describe 'and missing to address' do
+ let(:xml) { get('') }
+ let(:card) { vcard('Alice') }
+ let(:expected) { result(alice.jid, '', card) }
+
+ before do
+ storage.expect :find_vcard, card, [alice.jid.bare]
+ stream.expect :storage, storage, ['wonderland.lit']
+ stream.expect :write, nil, [expected]
+ end
+
+ it 'sends vcard for authenticated jid' do
+ subject.process
+ stream.verify
+ storage.verify
+ end
+ end
+
+ describe 'for another user' do
+ let(:xml) { get(hatter) }
+ let(:card) { vcard('Hatter') }
+ let(:hatter) { Vines::JID.new('hatter at wonderland.lit') }
+ let(:expected) { result(alice.jid, hatter, card) }
+
+ before do
+ storage.expect :find_vcard, card, [hatter]
+ stream.expect :storage, storage, ['wonderland.lit']
+ stream.expect :write, nil, [expected]
+ end
+
+ it 'succeeds and returns vcard with from address' do
+ subject.process
+ stream.verify
+ storage.verify
+ end
+ end
+
+ describe 'for missing vcard' do
+ let(:xml) { get('') }
+
+ before do
+ storage.expect :find_vcard, nil, [alice.jid.bare]
+ stream.expect :storage, storage, ['wonderland.lit']
+ end
+
+ it 'returns an item-not-found stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::ItemNotFound
+ stream.verify
+ storage.verify
+ end
+ end
+ end
+
+ describe 'when setting vcard' do
+ describe 'and addressed to another user' do
+ let(:xml) { set('hatter at wonderland.lit') }
+
+ it 'raises a forbidden stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::Forbidden
+ stream.verify
+ end
+ end
+
+ describe 'and missing to address' do
+ let(:xml) { set('') }
+ let(:card) { vcard('Alice') }
+ let(:expected) { result(alice.jid) }
+
+ before do
+ storage.expect :save_vcard, nil, [alice.jid, card]
+ stream.expect :storage, storage, ['wonderland.lit']
+ stream.expect :write, nil, [expected]
+ end
+
+ it 'succeeds and returns an iq result' do
+ subject.process
+ stream.verify
+ storage.verify
+ end
+ end
+ end
+
+ private
+
+ def vcard(name)
+ node(%Q{<vCard xmlns="vcard-temp"><FN>#{name}</FN></vCard>})
+ end
+
+ def get(to)
+ card = '<vCard xmlns="vcard-temp"/>'
+ iq(id: 42, to: to, type: 'get', body: card)
+ end
+
+ def set(to)
+ card = '<vCard xmlns="vcard-temp"><FN>Alice</FN></vCard>'
+ iq(id: 42, to: to, type: 'set', body: card)
+ end
+
+ def result(to, from=nil, card=nil)
+ iq(from: from, id: 42, to: to, type: 'result', body: card)
+ end
+end
diff --git a/test/stanza/iq/version_test.rb b/test/stanza/iq/version_test.rb
new file mode 100644
index 0000000..1016e65
--- /dev/null
+++ b/test/stanza/iq/version_test.rb
@@ -0,0 +1,64 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stanza::Iq::Version do
+ subject { Vines::Stanza::Iq::Version.new(xml, stream) }
+ let(:alice) { Vines::User.new(jid: 'alice at wonderland.lit/tea') }
+ let(:stream) { MiniTest::Mock.new }
+ let(:config) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ end
+
+ before do
+ class << stream
+ attr_accessor :config, :user
+ end
+ stream.config = config
+ stream.user = alice
+ end
+
+ describe 'when not addressed to the server' do
+ let(:router) { MiniTest::Mock.new }
+ let(:xml) { node(%q{<iq id="42" to="romeo at verona.lit" type="get"><query xmlns="jabber:iq:version"/></iq>}) }
+
+ before do
+ router.expect :route, nil, [xml]
+ stream.expect :router, router
+ end
+
+ it 'routes the stanza to the recipient jid' do
+ subject.process
+ stream.verify
+ router.verify
+ end
+ end
+
+ describe 'when missing a to address' do
+ let(:xml) { node(%q{<iq id="42" type="get"><query xmlns="jabber:iq:version"/></iq>}) }
+ let(:expected) do
+ node(%Q{
+ <iq from="wonderland.lit" id="42" to="alice at wonderland.lit/tea" type="result">
+ <query xmlns="jabber:iq:version">
+ <name>Vines</name>
+ <version>#{Vines::VERSION}</version>
+ </query>
+ </iq>})
+ end
+
+ before do
+ stream.expect :domain, 'wonderland.lit'
+ stream.expect :domain, 'wonderland.lit'
+ stream.expect :write, nil, [expected]
+ end
+
+ it 'returns a version result when missing a to jid' do
+ subject.process
+ stream.verify
+ end
+ end
+end
diff --git a/test/stanza/iq_test.rb b/test/stanza/iq_test.rb
new file mode 100644
index 0000000..df43920
--- /dev/null
+++ b/test/stanza/iq_test.rb
@@ -0,0 +1,70 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stanza::Iq do
+ subject { Vines::Stanza::Iq.new(xml, stream) }
+ let(:stream) { MiniTest::Mock.new }
+ let(:alice) { Vines::User.new(jid: 'alice at wonderland.lit/tea') }
+ let(:hatter) { Vines::User.new(jid: 'hatter at wonderland.lit/crumpets') }
+ let(:config) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ end
+
+ before do
+ class << stream
+ attr_accessor :config, :user
+ end
+ stream.user = hatter
+ stream.config = config
+ end
+
+ describe 'when addressed to a user rather than the server itself' do
+ let(:recipient) { MiniTest::Mock.new }
+ let(:xml) do
+ node(%q{
+ <iq id="42" type="set" to="alice at wonderland.lit/tea" from="hatter at wonderland.lit/crumpets">
+ <si xmlns="http://jabber.org/protocol/si" id="42_si" profile="http://jabber.org/protocol/si/profile/file-transfer">
+ <file xmlns="http://jabber.org/protocol/si/profile/file-transfer" name="file" size="1"/>
+ <feature xmlns="http://jabber.org/protocol/feature-neg">
+ <x xmlns="jabber:x:data" type="form">
+ <field var="stream-method" type="list-single">
+ <option>
+ <value>http://jabber.org/protocol/bytestreams</value>
+ </option>
+ <option>
+ <value>http://jabber.org/protocol/ibb</value>
+ </option>
+ </field>
+ </x>
+ </feature>
+ </si>
+ </iq>
+ })
+ end
+
+ before do
+ recipient.expect :user, alice, []
+ recipient.expect :write, nil, [xml]
+ stream.expect :connected_resources, [recipient], [alice.jid]
+ end
+
+ it 'routes the stanza to the users connected resources' do
+ subject.process
+ stream.verify
+ recipient.verify
+ end
+ end
+
+ describe 'when given no type or body elements' do
+ let(:xml) { node('<iq type="set" id="42"/>') }
+
+ it 'raises a feature-not-implemented stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::FeatureNotImplemented
+ end
+ end
+end
diff --git a/test/stanza/message_test.rb b/test/stanza/message_test.rb
new file mode 100644
index 0000000..c44a410
--- /dev/null
+++ b/test/stanza/message_test.rb
@@ -0,0 +1,127 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stanza::Message do
+ subject { Vines::Stanza::Message.new(xml, stream) }
+ let(:stream) { MiniTest::Mock.new }
+ let(:alice) { Vines::User.new(jid: 'alice at wonderland.lit/tea') }
+ let(:romeo) { Vines::User.new(jid: 'romeo at verona.lit/balcony') }
+ let(:config) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ end
+
+ before do
+ class << stream
+ attr_accessor :config, :user
+ end
+ stream.user = alice
+ stream.config = config
+ end
+
+ describe 'when message type attribute is invalid' do
+ let(:xml) { node('<message type="bogus">hello!</message>') }
+
+ it 'raises a bad-request stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::BadRequest
+ end
+ end
+
+ describe 'when the to address is missing' do
+ let(:xml) { node('<message>hello!</message>') }
+ let(:recipient) { MiniTest::Mock.new }
+
+ before do
+ recipient.expect :user, alice
+ recipient.expect :write, nil, [xml]
+ stream.expect :connected_resources, [recipient], [alice.jid.bare]
+ end
+
+ it 'sends the message to the senders connected streams' do
+ subject.process
+ stream.verify
+ recipient.verify
+ end
+ end
+
+ describe 'when addressed to a non-user' do
+ let(:bogus) { Vines::JID.new('bogus at wonderland.lit/cake') }
+ let(:xml) { node(%Q{<message to="#{bogus}">hello!</message>}) }
+ let(:storage) { MiniTest::Mock.new }
+
+ before do
+ storage.expect :find_user, nil, [bogus]
+ stream.expect :storage, storage, [bogus.domain]
+ stream.expect :connected_resources, [], [bogus]
+ end
+
+ it 'ignores the stanza' do
+ subject.process
+ stream.verify
+ storage.verify
+ end
+ end
+
+ describe 'when addressed to an offline user' do
+ let(:hatter) { Vines::User.new(jid: 'hatter at wonderland.lit/cake') }
+ let(:xml) { node(%Q{<message to="#{hatter.jid}">hello!</message>}) }
+ let(:storage) { MiniTest::Mock.new }
+
+ before do
+ skip # due offline message implementation
+ storage.expect :find_user, hatter, [hatter.jid]
+ stream.expect :storage, storage, [hatter.jid.domain]
+ stream.expect :connected_resources, [], [hatter.jid]
+ end
+
+ it 'raises a service-unavailable stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::ServiceUnavailable
+ stream.verify
+ storage.verify
+ end
+ end
+
+ describe 'when address to a local user in a different domain' do
+ let(:xml) { node(%Q{<message to="#{romeo.jid}">hello!</message>}) }
+ let(:expected) { node(%Q{<message to="#{romeo.jid}" from="#{alice.jid}">hello!</message>}) }
+ let(:recipient) { MiniTest::Mock.new }
+
+ before do
+ recipient.expect :user, romeo
+ recipient.expect :write, nil, [expected]
+
+ config.host 'verona.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+
+ stream.expect :connected_resources, [recipient], [romeo.jid]
+ end
+
+ it 'delivers the stanza to the user' do
+ subject.process
+ stream.verify
+ recipient.verify
+ end
+ end
+
+ describe 'when addressed to a remote user' do
+ let(:xml) { node(%Q{<message to="#{romeo.jid}">hello!</message>}) }
+ let(:expected) { node(%Q{<message to="#{romeo.jid}" from="#{alice.jid}">hello!</message>}) }
+ let(:router) { MiniTest::Mock.new }
+
+ before do
+ router.expect :route, nil, [expected]
+ stream.expect :router, router
+ end
+
+ it 'routes rather than handle locally' do
+ subject.process
+ stream.verify
+ router.verify
+ end
+ end
+end
diff --git a/test/stanza/presence/probe_test.rb b/test/stanza/presence/probe_test.rb
new file mode 100644
index 0000000..fcaff07
--- /dev/null
+++ b/test/stanza/presence/probe_test.rb
@@ -0,0 +1,50 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stanza::Presence::Probe do
+ def setup
+ @alice = Vines::JID.new('alice at wonderland.lit/tea')
+ @stream = MiniTest::Mock.new
+ @config = Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ end
+
+ def test_missing_to_address_raises
+ node = node(%q{<presence id="42" type="probe"/>})
+ stanza = Vines::Stanza::Presence::Probe.new(node, @stream)
+ def stanza.inbound?; false; end
+
+ @stream.expect(:user, Vines::User.new(jid: @alice))
+
+ assert_raises(Vines::StanzaErrors::BadRequest) { stanza.process }
+ assert @stream.verify
+ end
+
+ def test_to_remote_address_routes
+ node = node(%q{<presence id="42" to="romeo at verona.lit" type="probe"/>})
+ stanza = Vines::Stanza::Presence::Probe.new(node, @stream)
+ def stanza.inbound?; false; end
+
+ expected = node(%Q{<presence id="42" to="romeo at verona.lit" type="probe" from="#{@alice}"/>})
+ router = MiniTest::Mock.new
+ router.expect(:route, nil, [expected])
+
+ @stream.expect(:router, router)
+ @stream.expect(:user, Vines::User.new(jid: @alice))
+ @stream.expect(:config, @config)
+
+ stanza.process
+ assert @stream.verify
+ assert router.verify
+ end
+
+ private
+
+ def node(xml)
+ Nokogiri::XML(xml).root
+ end
+end
diff --git a/test/stanza/presence/subscribe_test.rb b/test/stanza/presence/subscribe_test.rb
new file mode 100644
index 0000000..000e3da
--- /dev/null
+++ b/test/stanza/presence/subscribe_test.rb
@@ -0,0 +1,83 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stanza::Presence::Subscribe do
+ subject { Vines::Stanza::Presence::Subscribe.new(xml, stream) }
+ let(:stream) { MiniTest::Mock.new }
+ let(:alice) { Vines::JID.new('alice at wonderland.lit/tea') }
+ let(:hatter) { Vines::JID.new('hatter at wonderland.lit') }
+ let(:contact) { Vines::Contact.new(jid: hatter) }
+
+ before do
+ class << stream
+ attr_accessor :user, :nodes
+ def write(node)
+ @nodes ||= []
+ @nodes << node
+ end
+ end
+ end
+
+ describe 'outbound subscription to a local jid, but missing contact' do
+ let(:xml) { node(%q{<presence id="42" to="hatter at wonderland.lit" type="subscribe"/>}) }
+ let(:user) { MiniTest::Mock.new }
+ let(:storage) { MiniTest::Mock.new }
+ let(:recipient) { MiniTest::Mock.new }
+
+ before do
+ class << user
+ attr_accessor :jid
+ end
+ user.jid = alice
+ user.expect :request_subscription, nil, [hatter]
+ user.expect :contact, contact, [hatter]
+
+ storage.expect :save_user, nil, [user]
+ storage.expect :find_user, nil, [hatter]
+
+ recipient.expect :user, user
+ class << recipient
+ attr_accessor :nodes
+ def write(node)
+ @nodes ||= []
+ @nodes << node
+ end
+ end
+
+ stream.user = user
+ stream.expect :domain, 'wonderland.lit'
+ stream.expect :storage, storage, ['wonderland.lit']
+ stream.expect :storage, storage, ['wonderland.lit']
+ stream.expect :interested_resources, [recipient], [alice]
+ stream.expect :update_user_streams, nil, [user]
+
+ class << subject
+ def route_iq; false; end
+ def inbound?; false; end
+ def local?; true; end
+ end
+ end
+
+ it 'rejects the subscription with an unsubscribed response' do
+ subject.process
+ stream.verify
+ user.verify
+ storage.verify
+ stream.nodes.size.must_equal 1
+
+ expected = node(%q{<presence from="hatter at wonderland.lit" id="42" to="alice at wonderland.lit" type="unsubscribed"/>})
+ stream.nodes.first.must_equal expected
+ end
+
+ it 'sends a roster set to the interested resources with subscription none' do
+ subject.process
+ recipient.nodes.size.must_equal 1
+
+ query = %q{<query xmlns="jabber:iq:roster"><item jid="hatter at wonderland.lit" subscription="none" from_diaspora="false"/></query>}
+ expected = node(%Q{<iq to="alice at wonderland.lit/tea" type="set">#{query}</iq>})
+ recipient.nodes.first.remove_attribute('id') # id is random
+ recipient.nodes.first.must_equal expected
+ end
+ end
+end
diff --git a/test/stanza/pubsub/create_test.rb b/test/stanza/pubsub/create_test.rb
new file mode 100644
index 0000000..02010c6
--- /dev/null
+++ b/test/stanza/pubsub/create_test.rb
@@ -0,0 +1,116 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stanza::PubSub::Create do
+ subject { Vines::Stanza::PubSub::Create.new(xml, stream) }
+ let(:user) { Vines::User.new(jid: 'alice at wonderland.lit/tea') }
+ let(:stream) { MiniTest::Mock.new }
+ let(:config) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ pubsub 'games'
+ end
+ end
+ end
+
+ before do
+ class << stream
+ attr_accessor :config, :nodes, :user
+ def write(node)
+ @nodes ||= []
+ @nodes << node
+ end
+ end
+ stream.config = config
+ stream.user = user
+ end
+
+ describe 'when missing a to address' do
+ let(:xml) { create('') }
+
+ it 'raises a feature-not-implemented stanza error' do
+ stream.expect :domain, 'wonderland.lit'
+ -> { subject.process }.must_raise Vines::StanzaErrors::FeatureNotImplemented
+ stream.verify
+ end
+ end
+
+ describe 'when addressed to bare server domain' do
+ let(:xml) { create('wonderland.lit') }
+
+ it 'raises a feature-not-implemented stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::FeatureNotImplemented
+ stream.verify
+ end
+ end
+
+ describe 'when addressed to a non-pubsub component' do
+ let(:router) { MiniTest::Mock.new }
+ let(:xml) { create('bogus.wonderland.lit') }
+
+ before do
+ router.expect :route, nil, [xml]
+ stream.expect :router, router
+ end
+
+ it 'routes rather than handle locally' do
+ subject.process
+ stream.verify
+ router.verify
+ end
+ end
+
+ describe 'when attempting to create multiple nodes' do
+ let(:xml) { create('games.wonderland.lit', true) }
+
+ it 'raises a bad-request stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::BadRequest
+ stream.verify
+ end
+ end
+
+ describe 'when attempting to create duplicate nodes' do
+ let(:pubsub) { MiniTest::Mock.new }
+ let(:xml) { create('games.wonderland.lit') }
+
+ it 'raises a conflict stanza error' do
+ pubsub.expect :node?, true, ['game_13']
+ subject.stub :pubsub, pubsub do
+ -> { subject.process }.must_raise Vines::StanzaErrors::Conflict
+ end
+ stream.verify
+ pubsub.verify
+ end
+ end
+
+ describe 'when given a valid stanza' do
+ let(:xml) { create('games.wonderland.lit') }
+ let(:expected) { result(user.jid, 'games.wonderland.lit') }
+
+ it 'sends an iq result stanza to sender' do
+ subject.process
+ stream.nodes.size.must_equal 1
+ stream.nodes.first.must_equal expected
+ stream.verify
+ end
+ end
+
+ private
+
+ def create(to, multiple=false)
+ extra_create = "<create node='game_14'/>" if multiple
+ body = %Q{
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <create node='game_13'/>
+ #{extra_create}
+ </pubsub>}
+ iq(type: 'set', to: to, id: 42, body: body)
+ end
+
+ def result(to, from)
+ body = '<pubsub xmlns="http://jabber.org/protocol/pubsub"><create node="game_13"/></pubsub>'
+ iq(from: from, id: 42, to: to, type: 'result', body: body)
+ end
+end
diff --git a/test/stanza/pubsub/delete_test.rb b/test/stanza/pubsub/delete_test.rb
new file mode 100644
index 0000000..2ec2894
--- /dev/null
+++ b/test/stanza/pubsub/delete_test.rb
@@ -0,0 +1,169 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stanza::PubSub::Delete do
+ subject { Vines::Stanza::PubSub::Delete.new(xml, stream) }
+ let(:alice) { Vines::User.new(jid: 'alice at wonderland.lit/tea') }
+ let(:stream) { MiniTest::Mock.new }
+ let(:config) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ pubsub 'games'
+ end
+ end
+ end
+
+ before do
+ class << stream
+ attr_accessor :config, :domain, :nodes, :user
+ def write(node)
+ @nodes ||= []
+ @nodes << node
+ end
+ end
+ stream.config = config
+ stream.domain = 'wonderland.lit'
+ stream.user = alice
+ end
+
+ describe 'when missing a to address' do
+ let(:xml) do
+ node(%q{
+ <iq type='set' id='42'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <delete node='game_13'/>
+ </pubsub>
+ </iq>
+ })
+ end
+
+ it 'raises a feature-not-implemented stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::FeatureNotImplemented
+ stream.verify
+ end
+ end
+
+ describe 'when addressed to a bare server domain jid' do
+ let(:xml) do
+ node(%q{
+ <iq type='set' to='wonderland.lit' id='42'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <delete node='game_13'/>
+ </pubsub>
+ </iq>
+ })
+ end
+
+ it 'raises a feature-not-implemented stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::FeatureNotImplemented
+ stream.verify
+ end
+ end
+
+ describe 'when addressed to a non-pubsub address' do
+ let(:router) { MiniTest::Mock.new }
+ let(:xml) do
+ node(%q{
+ <iq type='set' to='bogus.wonderland.lit' id='42'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <delete node='game_13'/>
+ </pubsub>
+ </iq>
+ })
+ end
+
+ before do
+ router.expect :route, nil, [xml]
+ stream.expect :router, router
+ end
+
+ it 'routes rather than handle locally' do
+ subject.process
+ stream.verify
+ router.verify
+ end
+ end
+
+ describe 'when stanza contains multiple delete elements' do
+ let(:xml) do
+ node(%q{
+ <iq type='set' to='games.wonderland.lit' id='42'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <delete node='game_13'/>
+ <delete node='game_14'/>
+ </pubsub>
+ </iq>
+ })
+ end
+
+ it 'raises a bad-request stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::BadRequest
+ stream.verify
+ end
+ end
+
+ describe 'when deleting a missing node' do
+ let(:xml) do
+ node(%q{
+ <iq type='set' to='games.wonderland.lit' id='42'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <delete node='game_13'/>
+ </pubsub>
+ </iq>
+ })
+ end
+
+ it 'raises an item-not-found stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::ItemNotFound
+ stream.verify
+ end
+ end
+
+ describe 'when valid stanza is received' do
+ let(:pubsub) { MiniTest::Mock.new }
+ let(:xml) do
+ node(%q{
+ <iq type='set' to='games.wonderland.lit' id='42'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <delete node='game_13'/>
+ </pubsub>
+ </iq>
+ })
+ end
+
+ let(:result) { node(%Q{<iq from="games.wonderland.lit" id="42" to="#{alice.jid}" type="result"/>}) }
+
+ let(:broadcast) do
+ node(%q{
+ <message>
+ <event xmlns="http://jabber.org/protocol/pubsub#event">
+ <delete node="game_13"/>
+ </event>
+ </message>})
+ end
+
+ before do
+ pubsub.expect :node?, true, ['game_13']
+ pubsub.expect :publish, nil, ['game_13', broadcast]
+ pubsub.expect :delete_node, nil, ['game_13']
+ end
+
+ it 'broadcasts the delete to subscribers' do
+ subject.stub :pubsub, pubsub do
+ subject.process
+ end
+ stream.verify
+ pubsub.verify
+ end
+
+ it 'sends a result stanza to sender' do
+ subject.stub :pubsub, pubsub do
+ subject.process
+ end
+ stream.nodes.size.must_equal 1
+ stream.nodes.first.must_equal result
+ end
+ end
+end
diff --git a/test/stanza/pubsub/publish_test.rb b/test/stanza/pubsub/publish_test.rb
new file mode 100644
index 0000000..08b190c
--- /dev/null
+++ b/test/stanza/pubsub/publish_test.rb
@@ -0,0 +1,309 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stanza::PubSub::Publish do
+ subject { Vines::Stanza::PubSub::Publish.new(xml, stream) }
+ let(:user) { Vines::User.new(jid: 'alice at wonderland.lit/tea') }
+ let(:stream) { MiniTest::Mock.new }
+ let(:config) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ pubsub 'games'
+ end
+ end
+ end
+
+ before do
+ class << stream
+ attr_accessor :config, :nodes, :user
+ def write(node)
+ @nodes ||= []
+ @nodes << node
+ end
+ end
+ stream.config = config
+ stream.user = user
+ end
+
+ describe 'when missing a to address' do
+ let(:xml) { publish('') }
+
+ it 'raises a feature-not-implemented stanza error' do
+ stream.expect(:domain, 'wonderland.lit')
+ -> { subject.process }.must_raise Vines::StanzaErrors::FeatureNotImplemented
+ stream.verify
+ end
+ end
+
+ describe 'when addressed to bare server domain' do
+ let(:xml) { publish('wonderland.lit') }
+
+ it 'raises a feature-not-implemented stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::FeatureNotImplemented
+ stream.verify
+ end
+ end
+
+ describe 'when addressed to a non-pubsub component' do
+ let(:router) { MiniTest::Mock.new }
+ let(:xml) { publish('bogus.wonderland.lit') }
+
+ before do
+ router.expect :route, nil, [xml]
+ stream.expect :router, router
+ end
+
+ it 'routes rather than handle locally' do
+ subject.process
+ stream.verify
+ router.verify
+ end
+ end
+
+ describe 'when publishing to multiple nodes' do
+ let(:xml) do
+ node(%q{
+ <iq type='set' to='games.wonderland.lit' id='42'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <publish node='game_13'>
+ <item id='item_42'>
+ <entry xmlns='http://www.w3.org/2005/Atom'>
+ <title>Test</title>
+ <summary>This is a summary.</summary>
+ </entry>
+ </item>
+ </publish>
+ <publish node='game_13'>
+ <item id='item_42'>
+ <entry xmlns='http://www.w3.org/2005/Atom'>
+ <title>Test</title>
+ <summary>This is a summary.</summary>
+ </entry>
+ </item>
+ </publish>
+ </pubsub>
+ </iq>
+ })
+ end
+
+ it 'raises a bad-request stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::BadRequest
+ stream.verify
+ end
+ end
+
+ describe 'when publishing multiple items' do
+ let(:pubsub) { MiniTest::Mock.new }
+ let(:xml) do
+ node(%q{
+ <iq type='set' to='games.wonderland.lit' id='42'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <publish node='game_13'>
+ <item id='item_42'>
+ <entry xmlns='http://www.w3.org/2005/Atom'>
+ <title>Test</title>
+ <summary>This is a summary.</summary>
+ </entry>
+ </item>
+ <item id="item_43">bad</item>
+ </publish>
+ </pubsub>
+ </iq>
+ })
+ end
+
+ it 'raises a bad-request stanza error' do
+ pubsub.expect :node?, true, ['game_13']
+ subject.stub :pubsub, pubsub do
+ -> { subject.process }.must_raise Vines::StanzaErrors::BadRequest
+ end
+ stream.verify
+ pubsub.verify
+ end
+ end
+
+ describe 'when publishing one item with multiple payloads' do
+ let(:pubsub) { MiniTest::Mock.new }
+ let(:xml) do
+ node(%q{
+ <iq type='set' to='games.wonderland.lit' id='42'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <publish node='game_13'>
+ <item id='item_42'>
+ <entry xmlns='http://www.w3.org/2005/Atom'>
+ <title>Test</title>
+ <summary>This is a summary.</summary>
+ </entry>
+ <entry>bad</entry>
+ </item>
+ </publish>
+ </pubsub>
+ </iq>
+ })
+ end
+
+ it 'raises a bad-request stanza error' do
+ pubsub.expect :node?, true, ['game_13']
+ subject.stub :pubsub, pubsub do
+ -> { subject.process }.must_raise Vines::StanzaErrors::BadRequest
+ end
+ stream.verify
+ pubsub.verify
+ end
+ end
+
+ describe 'when publishing with no payload' do
+ let(:pubsub) { MiniTest::Mock.new }
+ let(:xml) do
+ node(%q{
+ <iq type='set' to='games.wonderland.lit' id='42'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <publish node='game_13'>
+ <item id='item_42'>
+ </item>
+ </publish>
+ </pubsub>
+ </iq>
+ })
+ end
+
+ it 'raises a bad-request stanza error' do
+ pubsub.expect :node?, true, ['game_13']
+ subject.stub :pubsub, pubsub do
+ -> { subject.process }.must_raise Vines::StanzaErrors::BadRequest
+ end
+ stream.verify
+ pubsub.verify
+ end
+ end
+
+ describe 'when publishing to a missing node' do
+ let(:xml) { publish('games.wonderland.lit') }
+
+ it 'raises an item-not-found stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::ItemNotFound
+ stream.verify
+ end
+ end
+
+ describe 'when publishing an item without an id' do
+ let(:pubsub) { MiniTest::Mock.new }
+ let(:xml) { publish('games.wonderland.lit', '') }
+ let(:broadcast) { message_broadcast('') }
+ let(:response) do
+ node(%q{
+ <iq from="games.wonderland.lit" id="42" to="alice at wonderland.lit/tea" type="result">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <publish node="game_13">
+ <item/>
+ </publish>
+ </pubsub>
+ </iq>})
+ end
+
+ before do
+ pubsub.expect :node?, true, ['game_13']
+ def pubsub.published; @published; end
+ def pubsub.publish(node, message)
+ @published ||= []
+ @published << [node, message]
+ end
+ end
+
+ it 'generates an item id in the response' do
+ subject.stub :pubsub, pubsub do
+ subject.process
+ end
+ stream.verify
+ pubsub.verify
+ stream.nodes.size.must_equal 1
+
+ # id is random
+ item = stream.nodes.first.xpath('ns:pubsub/ns:publish/ns:item',
+ 'ns' => 'http://jabber.org/protocol/pubsub').first
+ item['id'].wont_be_nil
+ item.remove_attribute('id')
+ stream.nodes.first.must_equal response
+ end
+
+ it 'broadcasts the message with the generated item id' do
+ subject.stub :pubsub, pubsub do
+ subject.process
+ end
+ stream.verify
+ pubsub.verify
+ stream.nodes.size.must_equal 1
+
+ published_node, published_message = *pubsub.published[0]
+ published_node.must_equal 'game_13'
+ # id is random
+ item = published_message.xpath('ns:event/ns:items/ns:item',
+ 'ns' => 'http://jabber.org/protocol/pubsub#event').first
+ item['id'].wont_be_nil
+ item.remove_attribute('id')
+ published_message.must_equal broadcast
+ end
+ end
+
+ describe 'when publishing a valid stanza' do
+ let(:pubsub) { MiniTest::Mock.new }
+ let(:xml) { publish('games.wonderland.lit') }
+ let(:response) { result(user.jid, 'games.wonderland.lit') }
+ let(:broadcast) { message_broadcast('item_42') }
+
+ it 'broadcasts and returns result to sender' do
+ pubsub.expect :node?, true, ['game_13']
+ pubsub.expect :publish, nil, ['game_13', broadcast]
+
+ subject.stub :pubsub, pubsub do
+ subject.process
+ end
+
+ stream.nodes.size.must_equal 1
+ stream.nodes.first.must_equal response
+ stream.verify
+ pubsub.verify
+ end
+ end
+
+ private
+
+ def message_broadcast(item_id)
+ item_id = (item_id.nil? || item_id.empty?) ? ' ' : " id='#{item_id}' "
+ node(%Q{
+ <message>
+ <event xmlns="http://jabber.org/protocol/pubsub#event">
+ <items node="game_13">
+ <item#{item_id}publisher="alice at wonderland.lit/tea">
+ <entry xmlns="http://www.w3.org/2005/Atom">
+ <title>Test</title>
+ <summary>This is a summary.</summary>
+ </entry>
+ </item>
+ </items>
+ </event>
+ </message>})
+ end
+
+ def publish(to, item_id='item_42')
+ item_id = "id='#{item_id}'" unless item_id.nil? || item_id.empty?
+ body = %Q{
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <publish node='game_13'>
+ <item #{item_id}>
+ <entry xmlns='http://www.w3.org/2005/Atom'>
+ <title>Test</title>
+ <summary>This is a summary.</summary>
+ </entry>
+ </item>
+ </publish>
+ </pubsub>}
+ iq(type: 'set', to: to, id: 42, body: body)
+ end
+
+ def result(to, from)
+ iq(from: from, id: 42, to: to, type: 'result')
+ end
+end
diff --git a/test/stanza/pubsub/subscribe_test.rb b/test/stanza/pubsub/subscribe_test.rb
new file mode 100644
index 0000000..2a97238
--- /dev/null
+++ b/test/stanza/pubsub/subscribe_test.rb
@@ -0,0 +1,205 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stanza::PubSub::Subscribe do
+ subject { Vines::Stanza::PubSub::Subscribe.new(xml, stream) }
+ let(:alice) { Vines::User.new(jid: 'alice at wonderland.lit/tea') }
+ let(:stream) { MiniTest::Mock.new }
+ let(:config) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ pubsub 'games'
+ end
+ end
+ end
+
+ before do
+ class << stream
+ attr_accessor :config, :domain, :nodes, :user
+ def write(node)
+ @nodes ||= []
+ @nodes << node
+ end
+ end
+ stream.config = config
+ stream.user = alice
+ stream.domain = 'wonderland.lit'
+ end
+
+ describe 'when missing a to address' do
+ let(:xml) do
+ node(%q{
+ <iq type='set' id='42'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <subscribe node='game_13' jid="alice at wonderland.lit/tea"/>
+ </pubsub>
+ </iq>
+ })
+ end
+
+ it 'raises a feature-not-implemented stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::FeatureNotImplemented
+ stream.verify
+ end
+ end
+
+ describe 'when addressed to a bare server domain' do
+ let(:xml) do
+ node(%q{
+ <iq type='set' to='wonderland.lit' id='42'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <subscribe node='game_13' jid="alice at wonderland.lit/tea"/>
+ </pubsub>
+ </iq>
+ })
+ end
+
+ it 'raises a feature-not-implemented stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::FeatureNotImplemented
+ stream.verify
+ end
+ end
+
+ describe 'when addressed to a non-pubsub address' do
+ let(:router) { MiniTest::Mock.new }
+ let(:xml) do
+ node(%q{
+ <iq type='set' to='bogus.wonderland.lit' id='42'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <subscribe node='game_13' jid="alice at wonderland.lit/tea"/>
+ </pubsub>
+ </iq>
+ })
+ end
+
+ it 'routes rather than handle locally' do
+ router.expect :route, nil, [xml]
+ stream.expect :router, router
+
+ subject.process
+ stream.verify
+ router.verify
+ end
+ end
+
+ describe 'when stanza contains multiple subscribe elements' do
+ let(:xml) do
+ node(%q{
+ <iq type='set' to='games.wonderland.lit' id='42'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <subscribe node='game_13' jid="alice at wonderland.lit/tea"/>
+ <subscribe node='game_14' jid="alice at wonderland.lit/tea"/>
+ </pubsub>
+ </iq>
+ })
+ end
+
+ it 'raises a bad-request stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::BadRequest
+ stream.verify
+ end
+ end
+
+ describe 'when stanza is missing a subscribe element' do
+ let(:xml) do
+ node(%q{
+ <iq type='set' to='games.wonderland.lit' id='42'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <subscribe node='game_13' jid="alice at wonderland.lit/tea"/>
+ </pubsub>
+ </iq>
+ })
+ end
+
+ it 'raises an item-not-found stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::ItemNotFound
+ stream.verify
+ end
+ end
+
+ describe 'when attempting to subscribe to a node twice' do
+ let(:pubsub) { MiniTest::Mock.new }
+ let(:xml) do
+ node(%q{
+ <iq type='set' to='games.wonderland.lit' id='42'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <subscribe node='game_13' jid="alice at wonderland.lit/tea"/>
+ </pubsub>
+ </iq>
+ })
+ end
+
+ before do
+ pubsub.expect :node?, true, ['game_13']
+ pubsub.expect :subscribed?, true, ['game_13', alice.jid]
+ end
+
+ it 'raises a policy-violation stanza error' do
+ subject.stub :pubsub, pubsub do
+ -> { subject.process }.must_raise Vines::StanzaErrors::PolicyViolation
+ end
+ stream.verify
+ pubsub.verify
+ end
+ end
+
+ describe 'when subscribing with an illegal jid' do
+ let(:xml) do
+ node(%q{
+ <iq type='set' to='games.wonderland.lit' id='42'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <subscribe node='game_13' jid="not_alice at wonderland.lit/tea"/>
+ </pubsub>
+ </iq>
+ })
+ end
+
+ it 'raises a bad-request stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::BadRequest
+ stream.verify
+ end
+ end
+
+ describe 'when subscribing with a valid stanza' do
+ let(:xml) do
+ node(%q{
+ <iq type='set' to='games.wonderland.lit' id='42'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <subscribe node='game_13' jid="alice at wonderland.lit/tea"/>
+ </pubsub>
+ </iq>
+ })
+ end
+
+ let(:expected) do
+ node(%q{
+ <iq from="games.wonderland.lit" id="42" to="alice at wonderland.lit/tea" type="result">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <subscription node="game_13" jid="alice at wonderland.lit/tea" subscription="subscribed"/>
+ </pubsub>
+ </iq>
+ })
+ end
+
+ let(:pubsub) { MiniTest::Mock.new }
+
+ before do
+ pubsub.expect :node?, true, ['game_13']
+ pubsub.expect :subscribed?, false, ['game_13', alice.jid]
+ pubsub.expect :subscribe, nil, ['game_13', alice.jid]
+ end
+
+ it 'writes a result stanza to the stream' do
+ subject.stub :pubsub, pubsub do
+ subject.process
+ end
+
+ stream.verify
+ pubsub.verify
+ stream.nodes.size.must_equal 1
+ stream.nodes.first.must_equal expected
+ end
+ end
+end
diff --git a/test/stanza/pubsub/unsubscribe_test.rb b/test/stanza/pubsub/unsubscribe_test.rb
new file mode 100644
index 0000000..35ecd4d
--- /dev/null
+++ b/test/stanza/pubsub/unsubscribe_test.rb
@@ -0,0 +1,148 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stanza::PubSub::Unsubscribe do
+ subject { Vines::Stanza::PubSub::Unsubscribe.new(xml, stream) }
+ let(:user) { Vines::User.new(jid: 'alice at wonderland.lit/tea') }
+ let(:stream) { MiniTest::Mock.new }
+ let(:config) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ pubsub 'games'
+ end
+ end
+ end
+
+ before do
+ class << stream
+ attr_accessor :config, :nodes, :user
+ def write(node)
+ @nodes ||= []
+ @nodes << node
+ end
+ end
+ stream.config = config
+ stream.user = user
+ end
+
+ describe 'when missing a to address' do
+ let(:xml) { unsubscribe('') }
+
+ it 'raises a feature-not-implemented stanza error' do
+ stream.expect :domain, 'wonderland.lit'
+ -> { subject.process }.must_raise Vines::StanzaErrors::FeatureNotImplemented
+ stream.verify
+ end
+ end
+
+ describe 'when addressed to bare server domain' do
+ let(:xml) { unsubscribe('wonderland.lit') }
+
+ it 'raises a feature-not-implemented stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::FeatureNotImplemented
+ stream.verify
+ end
+ end
+
+ describe 'when addressed to a non-pubsub component' do
+ let(:router) { MiniTest::Mock.new }
+ let(:xml) { unsubscribe('bogus.wonderland.lit') }
+
+ before do
+ router.expect :route, nil, [xml]
+ stream.expect :router, router
+ end
+
+ it 'routes rather than handle locally' do
+ subject.process
+ stream.verify
+ router.verify
+ end
+ end
+
+ describe 'when attempting to unsubscribe from multiple nodes' do
+ let(:xml) { unsubscribe('games.wonderland.lit', true) }
+
+ it 'raises a bad-request stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::BadRequest
+ stream.verify
+ end
+ end
+
+ describe 'when unsubscribing from a missing node' do
+ let(:xml) { unsubscribe('games.wonderland.lit') }
+
+ it 'raises an item-not-found stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::ItemNotFound
+ stream.verify
+ end
+ end
+
+ describe 'when unsubscribing without a subscription' do
+ let(:pubsub) { MiniTest::Mock.new }
+ let(:xml) { unsubscribe('games.wonderland.lit') }
+
+ before do
+ pubsub.expect :node?, true, ['game_13']
+ pubsub.expect :subscribed?, false, ['game_13', user.jid]
+ end
+
+ it 'raises an unexpected-request stanza error' do
+ subject.stub :pubsub, pubsub do
+ -> { subject.process }.must_raise Vines::StanzaErrors::UnexpectedRequest
+ end
+ stream.verify
+ pubsub.verify
+ end
+ end
+
+ describe 'when unsubscribing an illegal jid' do
+ let(:xml) { unsubscribe('games.wonderland.lit', false, 'not_alice at wonderland.lit/tea') }
+
+ it 'raises a forbidden stanza error' do
+ -> { subject.process }.must_raise Vines::StanzaErrors::Forbidden
+ stream.verify
+ end
+ end
+
+ describe 'when given a valid stanza' do
+ let(:pubsub) { MiniTest::Mock.new }
+ let(:xml) { unsubscribe('games.wonderland.lit') }
+ let(:expected) { result(user.jid, 'games.wonderland.lit') }
+
+ before do
+ pubsub.expect :node?, true, ['game_13']
+ pubsub.expect :subscribed?, true, ['game_13', user.jid]
+ pubsub.expect :unsubscribe, nil, ['game_13', user.jid]
+ end
+
+ it 'sends an iq result stanza to sender' do
+ subject.stub :pubsub, pubsub do
+ subject.process
+ end
+
+ stream.nodes.size.must_equal 1
+ stream.nodes.first.must_equal expected
+ stream.verify
+ pubsub.verify
+ end
+ end
+
+ private
+
+ def unsubscribe(to, multiple=false, jid=user.jid)
+ extra = "<unsubscribe node='game_14' jid='#{jid}'/>" if multiple
+ body = %Q{
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <unsubscribe node='game_13' jid="#{jid}"/>
+ #{extra}
+ </pubsub>}
+ iq(type: 'set', to: to, id: 42, body: body)
+ end
+
+ def result(to, from)
+ iq(from: from, id: 42, to: to, type: 'result')
+ end
+end
diff --git a/test/stanza_test.rb b/test/stanza_test.rb
new file mode 100644
index 0000000..b004735
--- /dev/null
+++ b/test/stanza_test.rb
@@ -0,0 +1,85 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stanza do
+ subject { Vines::Stanza::Message.new(xml, stream) }
+ let(:alice) { Vines::JID.new('alice at wonderland.lit/tea') }
+ let(:romeo) { Vines::JID.new('romeo at verona.lit/balcony') }
+ let(:stream) { MiniTest::Mock.new }
+ let(:config) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ end
+
+ describe 'when stanza contains no addresses' do
+ let(:xml) { node(%Q{<message>hello!</message>}) }
+
+ it 'validates them as nil' do
+ subject.validate_to.must_be_nil
+ subject.validate_from.must_be_nil
+ stream.verify
+ end
+ end
+
+ describe 'when stanza contains valid addresses' do
+ let(:xml) { node(%Q{<message from="#{alice}" to="#{romeo}">hello!</message>}) }
+
+ it 'validates and returns JID objects' do
+ subject.validate_to.must_equal romeo
+ subject.validate_from.must_equal alice
+ stream.verify
+ end
+ end
+
+ describe 'when stanza contains invalid addresses' do
+ let(:xml) { node(%Q{<message from="a lice at wonderland.lit" to="romeo at v erona.lit">hello!</message>}) }
+
+ it 'raises a jid-malformed stanza error' do
+ -> { subject.validate_to }.must_raise Vines::StanzaErrors::JidMalformed
+ -> { subject.validate_from }.must_raise Vines::StanzaErrors::JidMalformed
+ stream.verify
+ end
+ end
+
+ describe 'when receiving a non-routable stanza type' do
+ let(:xml) { node('<auth/>') }
+
+ it 'handles locally rather than routing' do
+ subject.local?.must_equal true
+ stream.verify
+ end
+ end
+
+ describe 'when stanza is missing a to address' do
+ let(:xml) { node(%Q{<message>hello!</message>}) }
+
+ it 'handles locally rather than routing' do
+ subject.local?.must_equal true
+ stream.verify
+ end
+ end
+
+ describe 'when stanza is addressed to a local jid' do
+ let(:xml) { node(%Q{<message to="#{alice}">hello!</message>}) }
+
+ it 'handles locally rather than routing' do
+ stream.expect :config, config
+ subject.local?.must_equal true
+ stream.verify
+ end
+ end
+
+ describe 'when stanza is addressed to a remote jid' do
+ let(:xml) { node(%Q{<message to="#{romeo}">hello!</message>}) }
+
+ it 'is not considered a local stanza' do
+ stream.expect :config, config
+ subject.local?.must_equal false
+ stream.verify
+ end
+ end
+end
diff --git a/test/storage/local_test.rb b/test/storage/local_test.rb
new file mode 100644
index 0000000..b848e92
--- /dev/null
+++ b/test/storage/local_test.rb
@@ -0,0 +1,59 @@
+# encoding: UTF-8
+
+require 'storage_tests'
+require 'test_helper'
+
+describe Vines::Storage::Local do
+ include StorageTests
+
+ DIR = Dir.mktmpdir
+
+ def setup
+ Dir.mkdir(DIR) unless File.exists?(DIR)
+ %w[user vcard fragment].each do |d|
+ Dir.mkdir(File.join(DIR, d))
+ end
+
+ files = {
+ :empty => "#{DIR}/user/empty at wonderland.lit",
+ :no_pass => "#{DIR}/user/no_password at wonderland.lit",
+ :clear_pass => "#{DIR}/user/clear_password at wonderland.lit",
+ :bcrypt => "#{DIR}/user/bcrypt_password at wonderland.lit",
+ :full => "#{DIR}/user/full at wonderland.lit",
+ :vcard => "#{DIR}/vcard/full at wonderland.lit",
+ :fragment => "#{DIR}/fragment/full at wonderland.lit-#{StorageTests::FRAGMENT_ID}"
+ }
+ File.open(files[:empty], 'w') {|f| f.write('') }
+ File.open(files[:no_pass], 'w') {|f| f.write('foo: bar') }
+ File.open(files[:clear_pass], 'w') {|f| f.write('password: secret') }
+ File.open(files[:bcrypt], 'w') {|f| f.write("password: #{BCrypt::Password.create('secret')}") }
+ File.open(files[:full], 'w') do |f|
+ f.puts("password: #{BCrypt::Password.create('secret')}")
+ f.puts("name: Tester")
+ f.puts("roster:")
+ f.puts(" contact1 at wonderland.lit:")
+ f.puts(" name: Contact1")
+ f.puts(" groups: [Group1, Group2]")
+ f.puts(" contact2 at wonderland.lit:")
+ f.puts(" name: Contact2")
+ f.puts(" groups: [Group3, Group4]")
+ end
+ File.open(files[:vcard], 'w') {|f| f.write(StorageTests::VCARD.to_xml) }
+ File.open(files[:fragment], 'w') {|f| f.write(StorageTests::FRAGMENT.to_xml) }
+ end
+
+ def teardown
+ FileUtils.remove_entry_secure(DIR)
+ end
+
+ def storage
+ Vines::Storage::Local.new { dir DIR }
+ end
+
+ def test_init
+ assert_raises(RuntimeError) { Vines::Storage::Local.new {} }
+ assert_raises(RuntimeError) { Vines::Storage::Local.new { dir 'bogus' } }
+ assert_raises(RuntimeError) { Vines::Storage::Local.new { dir '/sbin' } }
+ Vines::Storage::Local.new { dir DIR } # shouldn't raise an error
+ end
+end
diff --git a/test/storage/mock_redis.rb b/test/storage/mock_redis.rb
new file mode 100644
index 0000000..6b4f4aa
--- /dev/null
+++ b/test/storage/mock_redis.rb
@@ -0,0 +1,97 @@
+# encoding: UTF-8
+
+# A mock redis storage implementation that saves data to an in-memory Hash.
+class MockRedis
+ attr_reader :db
+
+ # Mimic em-hiredis behavior.
+ def self.defer(method)
+ old = instance_method(method)
+ define_method method do |*args, &block|
+ result = old.bind(self).call(*args)
+ deferred = EM::DefaultDeferrable.new
+ deferred.callback(&block) if block
+ EM.next_tick { deferred.succeed(result) }
+ deferred
+ end
+ end
+
+ def initialize
+ @db = {}
+ end
+
+ def del(key)
+ @db.delete(key)
+ end
+ defer :del
+
+ def get(key)
+ @db[key]
+ end
+ defer :get
+
+ def set(key, value)
+ @db[key] = value
+ end
+ defer :set
+
+ def hget(key, field)
+ @db[key][field] rescue nil
+ end
+ defer :hget
+
+ def hdel(key, field)
+ @db[key].delete(field) rescue nil
+ end
+ defer :hdel
+
+ def hgetall(key)
+ (@db[key] || {}).map do |k, v|
+ [k, v]
+ end.flatten
+ end
+ defer :hgetall
+
+ def hset(key, field, value)
+ @db[key] ||= {}
+ @db[key][field] = value
+ end
+ defer :hset
+
+ def hmset(key, *args)
+ @db[key] = Hash[*args]
+ end
+ defer :hmset
+
+ def sadd(key, obj)
+ @db[key] ||= Set.new
+ @db[key] << obj
+ end
+ defer :sadd
+
+ def srem(key, obj)
+ @db[key].delete(obj) rescue nil
+ end
+ defer :srem
+
+ def smembers
+ @db[key].to_a rescue []
+ end
+ defer :smembers
+
+ def flushdb
+ @db.clear
+ end
+ defer :flushdb
+
+ def multi
+ @transaction = true
+ end
+ defer :multi
+
+ def exec
+ raise 'transaction must start with multi' unless @transaction
+ @transaction = false
+ end
+ defer :exec
+end
\ No newline at end of file
diff --git a/test/storage/null_test.rb b/test/storage/null_test.rb
new file mode 100644
index 0000000..a0cf662
--- /dev/null
+++ b/test/storage/null_test.rb
@@ -0,0 +1,29 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Storage::Null do
+ before do
+ @storage = Vines::Storage::Null.new
+ @user = Vines::User.new(jid: 'alice at wonderland.lit')
+ end
+
+ def test_find_user_returns_nil
+ assert_nil @storage.find_user(@user.jid)
+ @storage.save_user(@user)
+ assert_nil @storage.find_user(@user.jid)
+ end
+
+ def test_find_vcard_returns_nil
+ assert_nil @storage.find_vcard(@user.jid)
+ @storage.save_vcard(@user.jid, 'card')
+ assert_nil @storage.find_vcard(@user.jid)
+ end
+
+ def test_find_fragment_returns_nil
+ assert_nil @storage.find_fragment(@user.jid, 'node')
+ @storage.save_fragment(@user.jid, 'node')
+ assert_nil @storage.find_fragment(@user.jid, 'node')
+ nil
+ end
+end
diff --git a/test/storage/sql_schema.rb b/test/storage/sql_schema.rb
new file mode 100644
index 0000000..1279c7b
--- /dev/null
+++ b/test/storage/sql_schema.rb
@@ -0,0 +1,186 @@
+module SqlSchema
+ def fibered
+ EM.run do
+ Fiber.new do
+ yield
+ EM.stop
+ end.resume
+ end
+ end
+
+ def fragment_id
+ Digest::SHA1.hexdigest("characters:urn:wonderland")
+ end
+
+ def fragment
+ Nokogiri::XML(%q{
+ <characters xmlns="urn:wonderland">
+ <character>Alice</character>
+ </characters>
+ }.strip).root
+ end
+
+ def db_file
+ Rails.application.config.database_configuration["development"]["database"]
+ end
+
+ def storage
+ Vines::Storage::Sql.new
+ end
+
+ def create_schema(args={})
+ args[:force] ||= false
+
+ # disable stdout logging
+ ActiveRecord::Migration.verbose = false
+ ActiveRecord::Schema.define do
+ create_table "people", :force => true do |t|
+ t.string "guid", :null => false
+ t.text "url", :null => false
+ t.string "diaspora_handle", :null => false
+ t.text "serialized_public_key", :null => false
+ t.integer "owner_id"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
+ t.boolean "closed_account", :default => false
+ t.integer "fetch_status", :default => 0
+ end
+
+ add_index "people", ["diaspora_handle"], :name => "index_people_on_diaspora_handle", :unique => true
+ add_index "people", ["guid"], :name => "index_people_on_guid", :unique => true
+ add_index "people", ["owner_id"], :name => "index_people_on_owner_id", :unique => true
+
+ create_table "profiles", force: true do |t|
+ t.string "diaspora_handle"
+ t.string "first_name", limit: 127
+ t.string "last_name", limit: 127
+ t.string "image_url"
+ t.string "image_url_small"
+ t.string "image_url_medium"
+ t.date "birthday"
+ t.string "gender"
+ t.text "bio"
+ t.boolean "searchable", default: true, null: false
+ t.integer "person_id", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.string "location"
+ t.string "full_name", limit: 70
+ t.boolean "nsfw", default: false
+ end
+
+ add_index "profiles", ["full_name", "searchable"], name: "index_profiles_on_full_name_and_searchable", using: :btree
+ add_index "profiles", ["full_name"], name: "index_profiles_on_full_name", using: :btree
+ add_index "profiles", ["person_id"], name: "index_profiles_on_person_id", using: :btree
+
+ create_table "aspects", :force => true do |t|
+ t.string "name", :null => false
+ t.integer "user_id", :null => false
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
+ t.boolean "contacts_visible", :default => true, :null => false
+ t.integer "order_id"
+ t.boolean "chat_enabled", default: false
+ end
+
+ add_index "aspects", ["user_id", "contacts_visible"], :name => "index_aspects_on_user_id_and_contacts_visible"
+ add_index "aspects", ["user_id"], :name => "index_aspects_on_user_id"
+
+ create_table "aspect_memberships", :force => true do |t|
+ t.integer "aspect_id", :null => false
+ t.integer "contact_id", :null => false
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
+ end
+
+ add_index "aspect_memberships", ["aspect_id", "contact_id"], :name => "index_aspect_memberships_on_aspect_id_and_contact_id", :unique => true
+ add_index "aspect_memberships", ["aspect_id"], :name => "index_aspect_memberships_on_aspect_id"
+ add_index "aspect_memberships", ["contact_id"], :name => "index_aspect_memberships_on_contact_id"
+
+ create_table "contacts", :force => true do |t|
+ t.integer "user_id", :null => false
+ t.integer "person_id", :null => false
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
+ t.boolean "sharing", :default => false, :null => false
+ t.boolean "receiving", :default => false, :null => false
+ end
+
+ add_index "contacts", ["person_id"], :name => "index_contacts_on_person_id"
+ add_index "contacts", ["user_id", "person_id"], :name => "index_contacts_on_user_id_and_person_id", :unique => true
+
+ create_table "chat_contacts", :force => true do |t|
+ t.integer "user_id", :null => false
+ t.string "jid", :null => false
+ t.string "name"
+ t.string "ask", :limit => 128
+ t.string "subscription", :limit => 128, :null => false
+ t.text "groups"
+ end
+
+ add_index "chat_contacts", ["user_id", "jid"], :name => "index_chat_contacts_on_user_id_and_jid", :unique => true
+
+ create_table "chat_fragments", :force => true do |t|
+ t.integer "user_id", :null => false
+ t.string "root", :limit => 256, :null => false
+ t.string "namespace", :limit => 256, :null => false
+ t.text "xml", :null => false
+ end
+
+ add_index "chat_fragments", ["user_id"], :name => "index_chat_fragments_on_user_id", :unique => true
+
+ create_table "chat_offline_messages", force: true do |t|
+ t.string "from", null: false
+ t.string "to", null: false
+ t.text "message", null: false
+ t.datetime "created_at", null: false
+ end
+
+ create_table "users", :force => true do |t|
+ t.string "username"
+ t.text "serialized_private_key"
+ t.boolean "getting_started", :default => true, :null => false
+ t.boolean "disable_mail", :default => false, :null => false
+ t.string "language"
+ t.string "email", :default => "", :null => false
+ t.string "encrypted_password", :default => "", :null => false
+ t.string "invitation_token", :limit => 60
+ t.datetime "invitation_sent_at"
+ t.string "reset_password_token"
+ t.datetime "remember_created_at"
+ t.integer "sign_in_count", :default => 0
+ t.datetime "current_sign_in_at"
+ t.datetime "last_sign_in_at"
+ t.string "current_sign_in_ip"
+ t.string "last_sign_in_ip"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
+ t.string "invitation_service", :limit => 127
+ t.string "invitation_identifier", :limit => 127
+ t.integer "invitation_limit"
+ t.integer "invited_by_id"
+ t.string "invited_by_type"
+ t.string "authentication_token", :limit => 30
+ t.string "unconfirmed_email"
+ t.string "confirm_email_token", :limit => 30
+ t.datetime "locked_at"
+ t.boolean "show_community_spotlight_in_stream", :default => true, :null => false
+ t.boolean "auto_follow_back", :default => false
+ t.integer "auto_follow_back_aspect_id"
+ t.text "hidden_shareables"
+ t.datetime "reset_password_sent_at"
+ t.datetime "last_seen"
+ end
+
+ add_index "users", ["authentication_token"], :name => "index_users_on_authentication_token", :unique => true
+ add_index "users", ["email"], :name => "index_users_on_email"
+ add_index "users", ["invitation_service", "invitation_identifier"], :name => "index_users_on_invitation_service_and_invitation_identifier", :unique => true
+ add_index "users", ["invitation_token"], :name => "index_users_on_invitation_token"
+ add_index "users", ["username"], :name => "index_users_on_username", :unique => true
+
+ #add_foreign_key "aspect_memberships", "aspects", name: "aspect_memberships_aspect_id_fk", dependent: :delete
+ #add_foreign_key "aspect_memberships", "contacts", name: "aspect_memberships_contact_id_fk", dependent: :delete
+ #add_foreign_key "contacts", "people", name: "contacts_person_id_fk", dependent: :delete
+ end
+ end
+end
diff --git a/test/storage/sql_test.rb b/test/storage/sql_test.rb
new file mode 100644
index 0000000..c2c969d
--- /dev/null
+++ b/test/storage/sql_test.rb
@@ -0,0 +1,290 @@
+# encoding: UTF-8
+
+require "test_helper"
+require "storage/sql_schema"
+
+module Diaspora
+ class Application < Rails::Application
+ def config.database_configuration
+ {
+ "development" => {
+ "adapter" => "sqlite3",
+ "database" => "test.db"
+ }
+ }
+ end
+ end
+end
+
+describe Vines::Storage::Sql do
+ include SqlSchema
+
+ def setup
+ _config = Vines::Config.configure do
+ max_offline_msgs 1
+
+ host "wonderland.lit" do
+ storage :fs do
+ dir Dir.tmpdir
+ end
+ end
+ end
+
+ @test_user = {
+ name: "test", url: "http://remote.host/",
+ image_url: "http://path.to/image.png", jid: "test at local.host", email: "test at test.de",
+ password: "$2a$10$c2G6rHjGeamQIOFI0c1/b.4mvFBw4AfOtgVrAkO1QPMuAyporj5e6", # pppppp
+ token: "1234"
+ }
+
+ return if File.exist?(db_file)
+ # create sql schema
+ storage && create_schema(force: true)
+
+ Vines::Storage::Sql::User.new(
+ username: @test_user[:name], email: @test_user[:email],
+ encrypted_password: @test_user[:password],
+ authentication_token: @test_user[:token]
+ ).save
+ Vines::Storage::Sql::Person.new(
+ owner_id: 1,
+ guid: "1697a4b0198901321e9b10e6ba921ce9",
+ url: @test_user[:url],
+ serialized_public_key: "some pub key",
+ diaspora_handle: @test_user[:jid]
+ ).save
+ Vines::Storage::Sql::Profile.new(
+ person_id: 1,
+ last_name: "Hirsch",
+ first_name: "Harry",
+ diaspora_handle: @test_user[:jid],
+ image_url: @test_user[:image_url]
+ ).save
+ Vines::Storage::Sql::Contact.new(
+ user_id: 1, person_id: 1,
+ sharing: true, receiving: true
+ ).save
+ Vines::Storage::Sql::Aspect.new(
+ user_id: 1, name: "without_chat",
+ contacts_visible: true, order_id: nil
+ ).save
+ Vines::Storage::Sql::AspectMembership.new(
+ # without_chat
+ aspect_id: 1, contact_id: 1
+ ).save
+ end
+
+ after do
+ # since we create the database once we
+ # have to reset it after every test run
+ Vines::Storage::Sql::ChatOfflineMessage.all.each(&:destroy)
+ end
+
+ def test_save_message
+ fibered do
+ db = storage
+
+ assert_nil db.save_message("", "", "")
+ assert_nil db.save_message("dude at valid@jid", "dude2 at valid.jid", "")
+ assert_nil db.save_message("dude at valid@jid", "", "test")
+ assert_nil db.save_message("", "dude2 at valid.jid", "test")
+
+ db.save_message(@test_user[:jid], "someone at inthe.void", "test")
+
+ msgs = Vines::Storage::Sql::ChatOfflineMessage.where(:to => "someone at inthe.void")
+ assert_equal 1, msgs.count
+ assert_equal "someone at inthe.void", msgs.first.to
+ assert_equal @test_user[:jid], msgs.first.from
+ assert_equal "test", msgs.first.message
+
+ db.save_message("someone at else.void", "someone at inthe.void", "test2")
+
+ msgs = Vines::Storage::Sql::ChatOfflineMessage.where(:to => "someone at inthe.void")
+ assert_equal 1, msgs.count # due max limit equals one (see max_offline_msgs)
+ assert_equal "someone at inthe.void", msgs.first.to
+ assert_equal "someone at else.void", msgs.first.from
+ assert_equal "test2", msgs.first.message # should be latest message
+ end
+ end
+
+ def test_find_avatar_by_jid
+ fibered do
+ db = storage
+
+ assert_nil db.find_avatar_by_jid("")
+ assert_nil db.find_avatar_by_jid("someone at inthe.void")
+
+ image_path = db.find_avatar_by_jid(@test_user[:jid])
+ assert_equal @test_user[:image_url], image_path
+ end
+ end
+
+ def test_find_messages
+ fibered do
+ db = storage
+
+ assert_nil db.find_messages("")
+ assert_equal 0, db.find_messages("someone at inthe.void").keys.count
+
+ Vines::Storage::Sql::ChatOfflineMessage.new(
+ from: @test_user[:jid], to: "someone at inthe.void", message: "test"
+ ).save
+
+ msgs = db.find_messages("someone at inthe.void")
+ assert_equal 1, msgs.keys.count
+ msgs.each {|_, msg|
+ assert_equal "someone at inthe.void", msg[:to]
+ assert_equal @test_user[:jid], msg[:from]
+ assert_equal "test", msg[:message]
+ }
+ end
+ end
+
+ def test_destroy_message
+ fibered do
+ db = storage
+ Vines::Storage::Sql::ChatOfflineMessage.new(
+ from: @test_user[:jid], to: "someone at inthe.void", message: "test"
+ ).save
+ Vines::Storage::Sql::ChatOfflineMessage.all.each do |com|
+ db.destroy_message(com.id)
+ end
+ count = Vines::Storage::Sql::ChatOfflineMessage.count(id: 1)
+ assert_equal 0, count
+ end
+ end
+
+ def test_aspect_chat_enabled
+ fibered do
+ db = storage
+ user = db.find_user(@test_user[:jid])
+ assert_equal 0, user.roster.length
+
+ aspect = Vines::Storage::Sql::Aspect.where(:id => 1)
+ aspect.update_all(name: "with_chat", chat_enabled: true)
+ user = db.find_user(@test_user[:jid])
+ assert_equal 1, user.roster.length
+ end
+ end
+
+ def test_save_user
+ fibered do
+ db = storage
+ user = Vines::User.new(jid: "test2 at test.de",
+ name: "test2 at test.de", password: "secret")
+ db.save_user(user)
+ assert_nil db.find_user("test2 at test.de")
+ end
+ end
+
+ def test_find_user
+ fibered do
+ db = storage
+ user = db.find_user(nil)
+ assert_nil user
+
+ user = db.find_user(@test_user[:jid])
+ assert (user != nil), "no user found"
+ assert_equal @test_user[:name], user.name
+
+ user.roster do |contact|
+ assert_equal "Harry Hirsch", contact.name
+ end
+
+ user = db.find_user(Vines::JID.new(@test_user[:jid]))
+ assert (user != nil), "no user found"
+ assert_equal @test_user[:name], user.name
+
+ user = db.find_user(Vines::JID.new("#{@test_user[:jid]}/resource"))
+ assert (user != nil), "no user found"
+ assert_equal @test_user[:name], user.name
+ end
+ end
+
+ def test_authenticate
+ fibered do
+ db = storage
+
+ assert_nil db.authenticate(nil, nil)
+ assert_nil db.authenticate(nil, "secret")
+ assert_nil db.authenticate("bogus", nil)
+
+ # user credential auth
+ pepper = "065eb8798b181ff0ea2c5c16aee0ff8b70e04e2ee6bd6e08b49da46924223e39127d5335e466207d42bf2a045c12be5f90e92012a4f05f7fc6d9f3c875f4c95b"
+ user = db.authenticate(@test_user[:jid], "pppppp#{pepper}")
+ assert (user != nil), "no user found"
+ assert_equal @test_user[:name], user.name
+
+ # user token auth
+ user = db.authenticate(@test_user[:jid], @test_user[:token])
+ assert (user != nil), "no user found"
+ assert_equal @test_user[:name], user.name
+ end
+ end
+
+ def test_find_vcard
+ fibered do
+ db = storage
+ xml = db.find_vcard(@test_user[:jid])
+ assert (xml != nil), "no vcard found"
+
+ doc = node(xml)
+ assert_equal "Harry Hirsch", doc.search("FN").text
+ assert_equal "Harry", doc.search("GIVEN").text
+ assert_equal "Hirsch", doc.search("FAMILY").text
+ assert_equal @test_user[:url], doc.search("URL").text
+ assert_equal @test_user[:image_url], doc.search("EXTVAL").text
+ end
+ end
+
+ def test_save_vcard
+ fibered do
+ assert_nil storage.save_vcard(@test_user[:jid], "<vCard></vCard>")
+ end
+ end
+
+ def test_find_fragment
+ skip("not working probably")
+
+ fibered do
+ db = storage
+ root = Nokogiri::XML(%(<characters xmlns="urn:wonderland"/>)).root
+ bad_name = Nokogiri::XML(%(<not_characters xmlns="urn:wonderland"/>)).root
+ bad_ns = Nokogiri::XML(%(<characters xmlns="not:wonderland"/>)).root
+
+ node = db.find_fragment(nil, nil)
+ assert_nil node
+
+ node = db.find_fragment("full at wonderland.lit", bad_name)
+ assert_nil node
+
+ node = db.find_fragment("full at wonderland.lit", bad_ns)
+ assert_nil node
+
+ node = db.find_fragment("full at wonderland.lit", root)
+ assert (node != nil), "node should include fragment"
+ assert_equal fragment.to_s, node.to_s
+
+ node = db.find_fragment(Vines::JID.new("full at wonderland.lit"), root)
+ assert (node != nil), "node should include fragment"
+ assert_equal fragment.to_s, node.to_s
+
+ node = db.find_fragment(Vines::JID.new("full at wonderland.lit/resource"), root)
+ assert (node != nil), "node should include fragment"
+ assert_equal fragment.to_s, node.to_s
+ end
+ end
+
+ def test_save_fragment
+ skip("not working probably")
+
+ fibered do
+ db = storage
+ root = Nokogiri::XML(%(<characters xmlns="urn:wonderland"/>)).root
+ db.save_fragment("test at test.de/resource1", fragment)
+ node = db.find_fragment("test at test.de", root)
+ assert (node != nil), "node should include fragment"
+ assert_equal fragment.to_s, node.to_s
+ end
+ end
+end
diff --git a/test/storage/storage_tests.rb b/test/storage/storage_tests.rb
new file mode 100644
index 0000000..7403a7b
--- /dev/null
+++ b/test/storage/storage_tests.rb
@@ -0,0 +1,182 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+# Mixin methods for storage implementation test classes. The behavioral
+# tests are the same regardless of implementation so share those methods
+# here.
+module StorageTests
+ FRAGMENT_ID = Digest::SHA1.hexdigest("characters:urn:wonderland")
+
+ FRAGMENT = Nokogiri::XML(%q{
+ <characters xmlns="urn:wonderland">
+ <character>Alice</character>
+ </characters>
+ }.strip).root
+
+ VCARD = Nokogiri::XML(%q{
+ <vCard xmlns="vcard-temp">
+ <FN>Alice in Wonderland</FN>
+ </vCard>
+ }.strip).root
+
+ class EMLoop
+ def initialize
+ EM.run do
+ Fiber.new do
+ yield
+ EM.stop
+ end.resume
+ end
+ end
+ end
+
+ def test_authenticate
+ EMLoop.new do
+ db = storage
+ assert_nil db.authenticate(nil, nil)
+ assert_nil db.authenticate(nil, 'secret')
+ assert_nil db.authenticate('bogus', nil)
+ assert_nil db.authenticate('bogus', 'secret')
+ assert_nil db.authenticate('empty at wonderland.lit', 'secret')
+ assert_nil db.authenticate('no_password at wonderland.lit', 'secret')
+ assert_nil db.authenticate('clear_password at wonderland.lit', 'secret')
+
+ user = db.authenticate('bcrypt_password at wonderland.lit', 'secret')
+ refute_nil user
+ assert_equal('bcrypt_password at wonderland.lit', user.jid.to_s)
+
+ user = db.authenticate('full at wonderland.lit', 'secret')
+ refute_nil user
+ assert_equal 'Tester', user.name
+ assert_equal 'full at wonderland.lit', user.jid.to_s
+
+ assert_equal 2, user.roster.length
+ assert_equal 'contact1 at wonderland.lit', user.roster[0].jid.to_s
+ assert_equal 'Contact1', user.roster[0].name
+ assert_equal 2, user.roster[0].groups.length
+ assert_equal 'Group1', user.roster[0].groups[0]
+ assert_equal 'Group2', user.roster[0].groups[1]
+
+ assert_equal 'contact2 at wonderland.lit', user.roster[1].jid.to_s
+ assert_equal 'Contact2', user.roster[1].name
+ assert_equal 2, user.roster[1].groups.length
+ assert_equal 'Group3', user.roster[1].groups[0]
+ assert_equal 'Group4', user.roster[1].groups[1]
+ end
+ end
+
+ def test_find_user
+ EMLoop.new do
+ db = storage
+ user = db.find_user(nil)
+ assert_nil user
+
+ user = db.find_user('full at wonderland.lit')
+ refute_nil user
+ assert_equal 'full at wonderland.lit', user.jid.to_s
+
+ user = db.find_user(Vines::JID.new('full at wonderland.lit'))
+ refute_nil user
+ assert_equal 'full at wonderland.lit', user.jid.to_s
+
+ user = db.find_user(Vines::JID.new('full at wonderland.lit/resource'))
+ refute_nil user
+ assert_equal 'full at wonderland.lit', user.jid.to_s
+ end
+ end
+
+ def test_save_user
+ EMLoop.new do
+ db = storage
+ user = Vines::User.new(
+ :jid => 'save_user at domain.tld/resource1',
+ :name => 'Save User',
+ :password => 'secret')
+ user.roster << Vines::Contact.new(
+ :jid => 'contact1 at domain.tld/resource2',
+ :name => 'Contact 1')
+ db.save_user(user)
+ user = db.find_user('save_user at domain.tld')
+ refute_nil user
+ assert_equal 'save_user at domain.tld', user.jid.to_s
+ assert_equal 'Save User', user.name
+ assert_equal 1, user.roster.length
+ assert_equal 'contact1 at domain.tld', user.roster[0].jid.to_s
+ assert_equal 'Contact 1', user.roster[0].name
+ end
+ end
+
+ def test_find_vcard
+ EMLoop.new do
+ db = storage
+ card = db.find_vcard(nil)
+ assert_nil card
+
+ card = db.find_vcard('full at wonderland.lit')
+ refute_nil card
+ assert_equal VCARD, card
+
+ card = db.find_vcard(Vines::JID.new('full at wonderland.lit'))
+ refute_nil card
+ assert_equal VCARD, card
+
+ card = db.find_vcard(Vines::JID.new('full at wonderland.lit/resource'))
+ refute_nil card
+ assert_equal VCARD, card
+ end
+ end
+
+ def test_save_vcard
+ EMLoop.new do
+ db = storage
+ db.save_user(Vines::User.new(:jid => 'save_user at domain.tld'))
+ db.save_vcard('save_user at domain.tld/resource1', VCARD)
+ card = db.find_vcard('save_user at domain.tld')
+ refute_nil card
+ assert_equal VCARD, card
+ end
+ end
+
+ def test_find_fragment
+ EMLoop.new do
+ db = storage
+ root = Nokogiri::XML(%q{<characters xmlns="urn:wonderland"/>}).root
+ bad_name = Nokogiri::XML(%q{<not_characters xmlns="urn:wonderland"/>}).root
+ bad_ns = Nokogiri::XML(%q{<characters xmlns="not:wonderland"/>}).root
+
+ node = db.find_fragment(nil, nil)
+ assert_nil node
+
+ node = db.find_fragment('full at wonderland.lit', bad_name)
+ assert_nil node
+
+ node = db.find_fragment('full at wonderland.lit', bad_ns)
+ assert_nil node
+
+ node = db.find_fragment('full at wonderland.lit', root)
+ refute_nil node
+ assert_equal FRAGMENT, node
+
+ node = db.find_fragment(Vines::JID.new('full at wonderland.lit'), root)
+ refute_nil node
+ assert_equal FRAGMENT, node
+
+ node = db.find_fragment(Vines::JID.new('full at wonderland.lit/resource'), root)
+ refute_nil node
+ assert_equal FRAGMENT, node
+ end
+ end
+
+ def test_save_fragment
+ EMLoop.new do
+ db = storage
+ root = Nokogiri::XML(%q{<characters xmlns="urn:wonderland"/>}).root
+ db.save_user(Vines::User.new(:jid => 'save_user at domain.tld'))
+ db.save_fragment('save_user at domain.tld/resource1', FRAGMENT)
+ node = db.find_fragment('save_user at domain.tld', root)
+ refute_nil node
+ assert_equal FRAGMENT, node
+ end
+ end
+end
diff --git a/test/store_test.rb b/test/store_test.rb
new file mode 100644
index 0000000..4be98e8
--- /dev/null
+++ b/test/store_test.rb
@@ -0,0 +1,164 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Store do
+ let(:dir) { 'conf/certs' }
+ let(:domain_pair) { certificate('wonderland.lit') }
+ let(:wildcard_pair) { certificate('*.wonderland.lit') }
+ subject { Vines::Store.new(dir) }
+
+ before do
+ @files =
+ save('wonderland.lit', domain_pair) +
+ save('wildcard.lit', wildcard_pair) +
+ save('duplicate.lit', domain_pair)
+ end
+
+ after do
+ @files.each do |name|
+ File.delete(name) if File.exists?(name)
+ end
+ end
+
+ describe 'creating a store' do
+ it 'parses certificate files' do
+ refute subject.certs.empty?
+ assert_equal OpenSSL::X509::Certificate, subject.certs.first.class
+ end
+
+ it 'ignores expired certificates' do
+ assert subject.certs.all? {|c| c.not_after > Time.new }
+ end
+
+ it 'does not raise an error for duplicate certificates' do
+ assert Vines::Store.new(dir)
+ end
+ end
+
+ describe 'files_for_domain' do
+ it 'handles invalid input' do
+ assert_nil subject.files_for_domain(nil)
+ assert_nil subject.files_for_domain('')
+ end
+
+ it 'finds files by name' do
+ refute_nil subject.files_for_domain('wonderland.lit')
+ cert, key = subject.files_for_domain('wonderland.lit')
+ assert_certificate_matches_key cert, key
+ assert_equal 'wonderland.lit.crt', File.basename(cert)
+ assert_equal 'wonderland.lit.key', File.basename(key)
+ end
+
+ it 'finds files for wildcard' do
+ refute_nil subject.files_for_domain('foo.wonderland.lit')
+ cert, key = subject.files_for_domain('foo.wonderland.lit')
+ assert_certificate_matches_key cert, key
+ assert_equal 'wildcard.lit.crt', File.basename(cert)
+ assert_equal 'wildcard.lit.key', File.basename(key)
+ end
+ end
+
+ describe 'trusted?' do
+ it 'does not trust malformed certificates' do
+ refute subject.trusted?('bogus')
+ end
+
+ it 'does not trust unsigned certificates' do
+ pair = certificate('something.lit')
+ refute subject.trusted?(pair.cert)
+ end
+ end
+
+ describe 'domain?' do
+ it 'handles invalid input' do
+ pair = certificate('wonderland.lit')
+ refute subject.domain?(nil, nil)
+ refute subject.domain?(pair.cert, nil)
+ refute subject.domain?(pair.cert, '')
+ refute subject.domain?(nil, '')
+ assert subject.domain?(pair.cert, 'wonderland.lit')
+ end
+
+ it 'verifies certificate subject domains' do
+ pair = certificate('wonderland.lit')
+ refute subject.domain?(pair.cert, 'bogus')
+ refute subject.domain?(pair.cert, 'www.wonderland.lit')
+ assert subject.domain?(pair.cert, 'wonderland.lit')
+ end
+
+ it 'verifies certificate subject alt domains' do
+ pair = certificate('wonderland.lit', 'www.wonderland.lit')
+ refute subject.domain?(pair.cert, 'bogus')
+ refute subject.domain?(pair.cert, 'tea.wonderland.lit')
+ assert subject.domain?(pair.cert, 'www.wonderland.lit')
+ assert subject.domain?(pair.cert, 'wonderland.lit')
+ end
+
+ it 'verifies certificate wildcard domains' do
+ pair = certificate('wonderland.lit', '*.wonderland.lit')
+ refute subject.domain?(pair.cert, 'bogus')
+ refute subject.domain?(pair.cert, 'one.two.wonderland.lit')
+ assert subject.domain?(pair.cert, 'tea.wonderland.lit')
+ assert subject.domain?(pair.cert, 'www.wonderland.lit')
+ assert subject.domain?(pair.cert, 'wonderland.lit')
+ end
+ end
+
+ private
+
+ # A public certificate + private key pair.
+ Pair = Struct.new(:cert, :key)
+
+ def assert_certificate_matches_key(cert, key)
+ refute_nil cert
+ refute_nil key
+ cert = OpenSSL::X509::Certificate.new(File.read(cert))
+ key = OpenSSL::PKey::RSA.new(File.read(key))
+ assert_equal cert.public_key.to_s, key.public_key.to_s
+ end
+
+ def certificate(domain, altname=nil)
+ # Use small key so tests are fast.
+ key = OpenSSL::PKey::RSA.generate(512)
+
+ name = OpenSSL::X509::Name.parse("/C=US/ST=Colorado/L=Denver/O=Test/CN=#{domain}")
+ cert = OpenSSL::X509::Certificate.new
+ cert.version = 2
+ cert.subject = name
+ cert.issuer = name
+ cert.serial = Time.now.to_i
+ cert.public_key = key.public_key
+ cert.not_before = Time.now
+ cert.not_after = Time.now + 3600
+
+ if altname
+ factory = OpenSSL::X509::ExtensionFactory.new
+ factory.subject_certificate = cert
+ factory.issuer_certificate = cert
+ cert.extensions = [
+ %w[subjectKeyIdentifier hash],
+ %w[subjectAltName] << [domain, altname].map {|n| "DNS:#{n}" }.join(',')
+ ].map {|k, v| factory.create_ext(k, v) }
+ end
+
+ cert.sign key, OpenSSL::Digest::SHA1.new
+
+ Pair.new(cert.to_pem, key.to_pem)
+ end
+
+ # Write the domain's certificate and private key files to the filesystem for
+ # the store to use.
+ #
+ # domain - The domain name String to use in the file name (e.g. wonderland.lit).
+ # pair - The Pair containing the public certificate and private key data.
+ #
+ # Returns a String Array of file names that were written.
+ def save(domain, pair)
+ crt = File.expand_path("#{domain}.crt", dir)
+ key = File.expand_path("#{domain}.key", dir)
+ File.open(crt, 'w') {|f| f.write(pair.cert) }
+ File.open(key, 'w') {|f| f.write(pair.key) }
+ [crt, key]
+ end
+end
diff --git a/test/stream/client/auth_test.rb b/test/stream/client/auth_test.rb
new file mode 100644
index 0000000..1cb0f41
--- /dev/null
+++ b/test/stream/client/auth_test.rb
@@ -0,0 +1,137 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stream::Client::Auth do
+ # disable logging for tests
+ Class.new.extend(Vines::Log).log.level = Logger::FATAL
+
+ class MockStorage < Vines::Storage
+ def initialize(raise_error=false)
+ @raise_error = raise_error
+ end
+
+ def authenticate(username, password)
+ username = username.to_s
+ raise 'temp auth fail' if @raise_error
+ user = Vines::User.new(jid: 'alice at wonderland.lit')
+ users = {'alice at wonderland.lit' => 'secr3t'}
+ (users.key?(username) && (users[username] == password)) ? user : nil
+ end
+
+ def find_user(jid)
+ end
+
+ def save_user(user)
+ end
+ end
+
+ subject { Vines::Stream::Client::Auth.new(stream) }
+ let(:stream) { MiniTest::Mock.new }
+
+ describe 'error handling' do
+ it 'rejects invalid element' do
+ node = node('<bogus/>')
+ -> { subject.node(node) }.must_raise Vines::StreamErrors::NotAuthorized
+ end
+
+ it 'rejects invalid element in sasl namespace' do
+ node = node(%Q{<bogus xmlns="#{Vines::NAMESPACES[:sasl]}"/>})
+ -> { subject.node(node) }.must_raise Vines::StreamErrors::NotAuthorized
+ end
+
+ it 'rejects auth elements missing sasl namespace' do
+ node = node('<auth/>')
+ -> { subject.node(node) }.must_raise Vines::StreamErrors::NotAuthorized
+ end
+
+ it 'rejects auth element with invalid namespace' do
+ node = node('<auth xmlns="bogus"/>')
+ -> { subject.node(node) }.must_raise Vines::StreamErrors::NotAuthorized
+ end
+
+ it 'rejects valid auth element missing mechanism' do
+ stream.expect :error, nil, [Vines::SaslErrors::InvalidMechanism]
+ stream.expect :authentication_mechanisms, ['PLAIN']
+ node = node(%Q{<auth xmlns="#{Vines::NAMESPACES[:sasl]}">tokens</auth>})
+ subject.node(node)
+ stream.verify
+ end
+
+ it 'rejects valid auth element with invalid mechanism' do
+ stream.expect :error, nil, [Vines::SaslErrors::InvalidMechanism]
+ stream.expect :authentication_mechanisms, ['PLAIN']
+ node = node(%Q{<auth xmlns="#{Vines::NAMESPACES[:sasl]}" mechanism="bogus">tokens</auth>})
+ subject.node(node)
+ stream.verify
+ end
+ end
+
+ describe 'plain auth' do
+ it 'rejects valid mechanism missing base64 text' do
+ stream.expect :error, nil, [Vines::SaslErrors::MalformedRequest]
+ node = plain('')
+ subject.node(node)
+ stream.verify
+ end
+
+ it 'rejects invalid base64 text' do
+ stream.expect :error, nil, [Vines::SaslErrors::IncorrectEncoding]
+ stream.expect :authentication_mechanisms, ['PLAIN']
+ node = plain('tokens')
+ subject.node(node)
+ stream.verify
+ end
+
+ it 'rejects invalid password' do
+ stream.expect :storage, MockStorage.new
+ stream.expect :domain, 'wonderland.lit'
+ stream.expect :error, nil, [Vines::SaslErrors::NotAuthorized]
+ stream.expect :authentication_mechanisms, ['PLAIN']
+ node = plain(Base64.strict_encode64("\x00alice\x00bogus"))
+ subject.node(node)
+ stream.verify
+ end
+
+ it 'passes with valid password' do
+ user = Vines::User.new(jid: 'alice at wonderland.lit')
+ stream.expect :reset, nil
+ stream.expect :domain, 'wonderland.lit'
+ stream.expect :storage, MockStorage.new
+ stream.expect :user=, nil, [user]
+ stream.expect :write, nil, [%Q{<success xmlns="#{Vines::NAMESPACES[:sasl]}"/>}]
+ stream.expect :advance, nil, [Vines::Stream::Client::BindRestart]
+ stream.expect :authentication_mechanisms, ['PLAIN']
+ node = plain(Base64.strict_encode64("\x00alice\x00secr3t"))
+ subject.node(node)
+ stream.verify
+ end
+
+ it 'raises policy-violation after max auth attempts is reached' do
+ stream.expect :domain, 'wonderland.lit'
+ stream.expect :storage, MockStorage.new
+ node = -> { plain(Base64.strict_encode64("\x00alice\x00bogus")) }
+
+ stream.expect :authentication_mechanisms, ['PLAIN']
+ stream.expect :error, nil, [Vines::SaslErrors::NotAuthorized]
+ subject.node(node.call)
+ stream.verify
+
+ stream.expect :authentication_mechanisms, ['PLAIN']
+ stream.expect :error, nil, [Vines::SaslErrors::NotAuthorized]
+ subject.node(node.call)
+ stream.verify
+
+ stream.expect :authentication_mechanisms, ['PLAIN']
+ stream.expect :error, nil, [Vines::StreamErrors::PolicyViolation]
+ subject.node(node.call)
+ stream.verify
+ end
+ end
+
+ private
+
+ def plain(authzid)
+ node(%Q{<auth xmlns="#{Vines::NAMESPACES[:sasl]}" mechanism="PLAIN">#{authzid}</auth>})
+ end
+end
diff --git a/test/stream/client/ready_test.rb b/test/stream/client/ready_test.rb
new file mode 100644
index 0000000..39f84eb
--- /dev/null
+++ b/test/stream/client/ready_test.rb
@@ -0,0 +1,47 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stream::Client::Ready do
+ STANZAS = []
+
+ before do
+ @stream = MiniTest::Mock.new
+ @state = Vines::Stream::Client::Ready.new(@stream, nil)
+ def @state.to_stanza(node)
+ if node.name == 'bogus'
+ nil
+ else
+ stanza = MiniTest::Mock.new
+ stanza.expect(:process, nil)
+ stanza.expect(:validate_to, nil)
+ stanza.expect(:validate_from, nil)
+ STANZAS << stanza
+ stanza
+ end
+ end
+ end
+
+ after do
+ STANZAS.clear
+ end
+
+ it 'processes a valid node' do
+ node = node('<message/>')
+ @state.node(node)
+ assert_equal 1, STANZAS.size
+ assert STANZAS.map {|s| s.verify }.all?
+ end
+
+ it 'raises an unsupported-stanza-type stream error for invalid node' do
+ node = node('<bogus/>')
+ assert_raises(Vines::StreamErrors::UnsupportedStanzaType) { @state.node(node) }
+ assert STANZAS.empty?
+ end
+
+ private
+
+ def node(xml)
+ Nokogiri::XML(xml).root
+ end
+end
diff --git a/test/stream/client/session_test.rb b/test/stream/client/session_test.rb
new file mode 100644
index 0000000..04e8bef
--- /dev/null
+++ b/test/stream/client/session_test.rb
@@ -0,0 +1,27 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stream::Client::Session do
+ subject { Vines::Stream::Client::Session.new(stream) }
+ let(:another) { Vines::Stream::Client::Session.new(stream) }
+ let(:stream) { OpenStruct.new(config: nil) }
+
+ describe 'session equality checks' do
+ it 'uses class in equality check' do
+ (subject <=> 42).must_be_nil
+ end
+
+ it 'is equal to itself' do
+ assert subject == subject
+ assert subject.eql?(subject)
+ assert subject.hash == subject.hash
+ end
+
+ it 'is not equal to another session' do
+ refute subject == another
+ refute subject.eql?(another)
+ refute subject.hash == another.hash
+ end
+ end
+end
diff --git a/test/stream/component/handshake_test.rb b/test/stream/component/handshake_test.rb
new file mode 100644
index 0000000..91dea81
--- /dev/null
+++ b/test/stream/component/handshake_test.rb
@@ -0,0 +1,52 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stream::Component::Handshake do
+ subject { Vines::Stream::Component::Handshake.new(stream) }
+ let(:stream) { MiniTest::Mock.new }
+
+ describe 'when invalid element is received' do
+ it 'raises a not-authorized stream error' do
+ node = node('<message/>')
+ -> { subject.node(node) }.must_raise Vines::StreamErrors::NotAuthorized
+ end
+ end
+
+ describe 'when handshake with no text is received' do
+ it 'raises a not-authorized stream error' do
+ stream.expect :secret, 'secr3t'
+ node = node('<handshake/>')
+ -> { subject.node(node) }.must_raise Vines::StreamErrors::NotAuthorized
+ stream.verify
+ end
+ end
+
+ describe 'when handshake with invalid secret is received' do
+ it 'raises a not-authorized stream error' do
+ stream.expect :secret, 'secr3t'
+ node = node('<handshake>bogus</handshake>')
+ -> { subject.node(node) }.must_raise Vines::StreamErrors::NotAuthorized
+ stream.verify
+ end
+ end
+
+ describe 'when good handshake is received' do
+ let(:router) { MiniTest::Mock.new }
+
+ before do
+ router.expect :<<, nil, [stream]
+ stream.expect :router, router
+ stream.expect :secret, 'secr3t'
+ stream.expect :write, nil, ['<handshake/>']
+ stream.expect :advance, nil, [Vines::Stream::Component::Ready.new(stream)]
+ end
+
+ it 'completes the handshake and advances the stream into the ready state' do
+ node = node('<handshake>secr3t</handshake>')
+ subject.node(node)
+ stream.verify
+ router.verify
+ end
+ end
+end
diff --git a/test/stream/component/ready_test.rb b/test/stream/component/ready_test.rb
new file mode 100644
index 0000000..59d0ddb
--- /dev/null
+++ b/test/stream/component/ready_test.rb
@@ -0,0 +1,103 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stream::Component::Ready do
+ subject { Vines::Stream::Component::Ready.new(stream, nil) }
+ let(:alice) { Vines::User.new(jid: 'alice at tea.wonderland.lit') }
+ let(:hatter) { Vines::User.new(jid: 'hatter at wonderland.lit') }
+ let(:stream) { MiniTest::Mock.new }
+ let(:config) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ end
+
+ before do
+ class << stream
+ attr_accessor :config
+ end
+ stream.config = config
+ end
+
+ describe 'when missing to and from addresses' do
+ it 'raises an improper-addressing stream error' do
+ node = node('<message/>')
+ -> { subject.node(node) }.must_raise Vines::StreamErrors::ImproperAddressing
+ stream.verify
+ end
+ end
+
+ describe 'when missing from address' do
+ it 'raises an improper-addressing stream error' do
+ node = node(%q{<message to="hatter at wonderland.lit"/>})
+ -> { subject.node(node) }.must_raise Vines::StreamErrors::ImproperAddressing
+ stream.verify
+ end
+ end
+
+ describe 'when missing to address' do
+ it 'raises an improper-addressing stream error' do
+ node = node(%q{<message from="alice at tea.wonderland.lit"/>})
+ -> { subject.node(node) }.must_raise Vines::StreamErrors::ImproperAddressing
+ stream.verify
+ end
+ end
+
+ describe 'when from address domain does not match component domain' do
+ it 'raises and invalid-from stream error' do
+ stream.expect :remote_domain, 'tea.wonderland.lit'
+ node = node(%q{<message from="alice at bogus.wonderland.lit" to="hatter at wonderland.lit"/>})
+ -> { subject.node(node) }.must_raise Vines::StreamErrors::InvalidFrom
+ stream.verify
+ end
+ end
+
+ describe 'when unrecognized element is received' do
+ it 'raises an unsupported-stanza-type stream error' do
+ node = node('<bogus/>')
+ -> { subject.node(node) }.must_raise Vines::StreamErrors::UnsupportedStanzaType
+ stream.verify
+ end
+ end
+
+ describe 'when addressed to a remote jid' do
+ let(:router) { MiniTest::Mock.new }
+ let(:xml) { node(%q{<message from="alice at tea.wonderland.lit" to="romeo at verona.lit"/>}) }
+
+ before do
+ router.expect :route, nil, [xml]
+ stream.expect :remote_domain, 'tea.wonderland.lit'
+ stream.expect :user=, nil, [alice]
+ stream.expect :router, router
+ end
+
+ it 'routes rather than handle locally' do
+ subject.node(xml)
+ stream.verify
+ router.verify
+ end
+ end
+
+ describe 'when addressed to a local jid' do
+ let(:recipient) { MiniTest::Mock.new }
+ let(:xml) { node(%q{<message from="alice at tea.wonderland.lit" to="hatter at wonderland.lit"/>}) }
+
+ before do
+ recipient.expect :user, hatter
+ recipient.expect :write, nil, [xml]
+ stream.expect :remote_domain, 'tea.wonderland.lit'
+ stream.expect :user=, nil, [alice]
+ stream.expect :user, alice
+ stream.expect :connected_resources, [recipient], [hatter.jid]
+ end
+
+ it 'sends the message to the connected stream' do
+ subject.node(xml)
+ stream.verify
+ recipient.verify
+ end
+ end
+end
diff --git a/test/stream/component/start_test.rb b/test/stream/component/start_test.rb
new file mode 100644
index 0000000..3257fdd
--- /dev/null
+++ b/test/stream/component/start_test.rb
@@ -0,0 +1,39 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stream::Component::Start do
+ before do
+ @stream = MiniTest::Mock.new
+ @state = Vines::Stream::Component::Start.new(@stream)
+ end
+
+ it 'raises not-authorized stream error for invalid element' do
+ node = node('<message/>')
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ end
+
+ it 'raises not-authorized stream error for missing stream namespace' do
+ node = node('<stream:stream/>')
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ end
+
+ it 'raises not-authorized stream error for invalid stream namespace' do
+ node = node('<stream:stream xmlns="bogus"/>')
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ end
+
+ it 'advances the state machine for valid stream header' do
+ node = node(%q{<stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:component:accept" to="tea.wonderland.lit"/>})
+ @stream.expect(:start, nil, [node])
+ @stream.expect(:advance, nil, [Vines::Stream::Component::Handshake.new(@stream)])
+ @state.node(node)
+ assert @stream.verify
+ end
+
+ private
+
+ def node(xml)
+ Nokogiri::XML(xml).root
+ end
+end
diff --git a/test/stream/http/auth_test.rb b/test/stream/http/auth_test.rb
new file mode 100644
index 0000000..911a3bd
--- /dev/null
+++ b/test/stream/http/auth_test.rb
@@ -0,0 +1,70 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stream::Http::Auth do
+ before do
+ @stream = MiniTest::Mock.new
+ @state = Vines::Stream::Http::Auth.new(@stream, nil)
+ end
+
+ def test_missing_body_raises_error
+ node = node('<presence type="unavailable"/>')
+ @stream.expect(:valid_session?, true, [nil])
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ end
+
+ def test_body_with_missing_namespace_raises_error
+ node = node('<body rid="42" sid="12"/>')
+ @stream.expect(:valid_session?, true, ['12'])
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ end
+
+ def test_missing_rid_raises_error
+ node = node('<body xmlns="http://jabber.org/protocol/httpbind" sid="12"/>')
+ @stream.expect(:valid_session?, true, ['12'])
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ end
+
+ def test_invalid_session_raises_error
+ @stream.expect(:valid_session?, false, ['12'])
+ node = node('<body xmlns="http://jabber.org/protocol/httpbind" rid="42" sid="12"/>')
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ assert @stream.verify
+ end
+
+ def test_empty_body_raises_error
+ node = node('<body xmlns="http://jabber.org/protocol/httpbind" rid="42" sid="12"/>')
+ @stream.expect(:valid_session?, true, ['12'])
+ @stream.expect(:parse_body, [], [node])
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ assert @stream.verify
+ end
+
+ def test_body_with_two_children_raises_error
+ node = node('<body xmlns="http://jabber.org/protocol/httpbind" rid="42" sid="12"><message/><message/></body>')
+ message = node('<message/>')
+ @stream.expect(:valid_session?, true, ['12'])
+ @stream.expect(:parse_body, [message, message], [node])
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ assert @stream.verify
+ end
+
+ def test_valid_body_processes
+ auth = node(%Q{<auth xmlns="#{Vines::NAMESPACES[:sasl]}" mechanism="PLAIN"/>})
+ node = node('<body xmlns="http://jabber.org/protocol/httpbind" rid="42" sid="12"></body>')
+ node << auth
+ @stream.expect(:valid_session?, true, ['12'])
+ @stream.expect(:parse_body, [auth], [node])
+ # this error means we correctly called the parent method Client#node
+ @stream.expect(:error, nil, [Vines::SaslErrors::MalformedRequest.new])
+ @state.node(node)
+ assert @stream.verify
+ end
+
+ private
+
+ def node(xml)
+ Nokogiri::XML(xml).root
+ end
+end
diff --git a/test/stream/http/ready_test.rb b/test/stream/http/ready_test.rb
new file mode 100644
index 0000000..50c0614
--- /dev/null
+++ b/test/stream/http/ready_test.rb
@@ -0,0 +1,86 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stream::Http::Ready do
+ subject { Vines::Stream::Http::Ready.new(stream, nil) }
+ let(:stream) { MiniTest::Mock.new }
+ let(:alice) { Vines::User.new(jid: 'alice at wonderland.lit') }
+ let(:hatter) { Vines::User.new(jid: 'hatter at wonderland.lit') }
+ let(:config) do
+ Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ end
+ end
+
+ it "raises when body element is missing" do
+ node = node('<presence type="unavailable"/>')
+ stream.expect :valid_session?, true, [nil]
+ -> { subject.node(node) }.must_raise Vines::StreamErrors::NotAuthorized
+ end
+
+ it "raises when namespace is missing" do
+ node = node('<body rid="42" sid="12"/>')
+ stream.expect :valid_session?, true, ['12']
+ -> { subject.node(node) }.must_raise Vines::StreamErrors::NotAuthorized
+ end
+
+ it "raises when rid attribute is missing" do
+ node = node('<body xmlns="http://jabber.org/protocol/httpbind" sid="12"/>')
+ stream.expect :valid_session?, true, ['12']
+ -> { subject.node(node) }.must_raise Vines::StreamErrors::NotAuthorized
+ end
+
+ it "raises when session id is invalid" do
+ stream.expect :valid_session?, false, ['12']
+ node = node('<body xmlns="http://jabber.org/protocol/httpbind" rid="42" sid="12"/>')
+ -> { subject.node(node) }.must_raise Vines::StreamErrors::NotAuthorized
+ stream.verify
+ end
+
+ it "processes when body element is empty" do
+ node = node('<body xmlns="http://jabber.org/protocol/httpbind" rid="42" sid="12"/>')
+ stream.expect :valid_session?, true, ['12']
+ stream.expect :parse_body, [], [node]
+ subject.node(node)
+ stream.verify
+ end
+
+ describe 'when receiving multiple stanzas in one body element' do
+ let(:recipient) { MiniTest::Mock.new }
+ let(:bogus) { node('<message type="bogus">raises stanza error</message>') }
+ let(:ok) { node('<message to="hatter at wonderland.lit">but processes this message</message>') }
+ let(:xml) { node(%Q{<body xmlns="http://jabber.org/protocol/httpbind" rid="42" sid="12">#{bogus}#{ok}</body>}) }
+ let(:raises) { Vines::Stanza.from_node(bogus, stream) }
+ let(:processes) { Vines::Stanza.from_node(ok, stream) }
+
+ before do
+ recipient.expect :user, hatter
+ recipient.expect :write, nil, [Vines::Stanza::Message]
+
+ stream.expect :valid_session?, true, ['12']
+ stream.expect :parse_body, [raises, processes], [xml]
+ stream.expect :error, nil, [Vines::StanzaErrors::BadRequest]
+ stream.expect :config, config
+ stream.expect :user, alice
+ stream.expect :connected_resources, [recipient], [hatter.jid]
+ end
+
+ it 'processes all stanzas' do
+ subject.node(xml)
+ stream.verify
+ recipient.verify
+ end
+ end
+
+ it "terminates the session" do
+ node = node('<body xmlns="http://jabber.org/protocol/httpbind" rid="42" sid="12" type="terminate"/>')
+ stream.expect :valid_session?, true, ['12']
+ stream.expect :parse_body, [], [node]
+ stream.expect :terminate, nil
+ subject.node(node)
+ stream.verify
+ end
+end
diff --git a/test/stream/http/request_test.rb b/test/stream/http/request_test.rb
new file mode 100644
index 0000000..830fc0f
--- /dev/null
+++ b/test/stream/http/request_test.rb
@@ -0,0 +1,194 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stream::Http::Request do
+ PASSWORD = File.expand_path('../passwords').freeze
+ INDEX = File.expand_path('index.html').freeze
+
+ before do
+ File.open(PASSWORD, 'w') {|f| f.puts '/etc/passwd contents' }
+ File.open(INDEX, 'w') {|f| f.puts 'index.html contents' }
+ @stream = MiniTest::Mock.new
+ end
+
+ after do
+ File.delete(PASSWORD)
+ File.delete(INDEX)
+ end
+
+ describe 'initialize' do
+ it 'copies request info from parser' do
+ headers = ['Host: wonderland.lit', 'Content-Type: text/html']
+ parser = parser('GET', '/blogs/12?ok=true', headers)
+ request = Vines::Stream::Http::Request.new(@stream, parser, '<html></html>')
+
+ assert_equal request.headers, {'Host' => 'wonderland.lit', 'Content-Type' => 'text/html'}
+ assert_equal request.method, 'GET'
+ assert_equal request.path, '/blogs/12'
+ assert_equal request.url, '/blogs/12?ok=true'
+ assert_equal request.query, 'ok=true'
+ assert_equal request.body, '<html></html>'
+ assert @stream.verify
+ end
+ end
+
+ describe 'reply_with_file' do
+ it 'returns 404 file not found' do
+ headers = ['Host: wonderland.lit', 'Content-Type: text/html']
+ parser = parser('GET', '/blogs/12?ok=true', headers)
+ request = Vines::Stream::Http::Request.new(@stream, parser, '<html></html>')
+
+ headers = [
+ "HTTP/1.1 404 Not Found",
+ "Content-Length: 0"
+ ].join("\r\n")
+
+ @stream.expect(:stream_write, nil, ["#{headers}\r\n\r\n"])
+
+ request.reply_with_file(Dir.pwd)
+ assert @stream.verify
+ end
+
+ it 'prevents directory traversal with 404 response' do
+ headers = ['Host: wonderland.lit', 'Content-Type: text/html']
+ parser = parser('GET', '/../passwords', headers)
+ request = Vines::Stream::Http::Request.new(@stream, parser, '<html></html>')
+
+ headers = [
+ "HTTP/1.1 404 Not Found",
+ "Content-Length: 0"
+ ].join("\r\n")
+
+ @stream.expect(:stream_write, nil, ["#{headers}\r\n\r\n"])
+
+ request.reply_with_file(Dir.pwd)
+ assert @stream.verify
+ end
+
+ it 'serves index.html for directory request' do
+ headers = ['Host: wonderland.lit', 'Content-Type: text/html']
+ parser = parser('GET', '/?ok=true', headers)
+ request = Vines::Stream::Http::Request.new(@stream, parser, '<html></html>')
+
+ mtime = File.mtime(INDEX).utc.strftime('%a, %d %b %Y %H:%M:%S GMT')
+ headers = [
+ "HTTP/1.1 200 OK",
+ 'Content-Type: text/html; charset="utf-8"',
+ "Content-Length: 20",
+ "Last-Modified: #{mtime}"
+ ].join("\r\n")
+
+ @stream.expect(:stream_write, nil, ["#{headers}\r\n\r\n"])
+ @stream.expect(:stream_write, nil, ["index.html contents\n"])
+
+ request.reply_with_file(Dir.pwd)
+ assert @stream.verify
+ end
+
+ it 'redirects for missing trailing slash' do
+ headers = ['Host: wonderland.lit', 'Content-Type: text/html']
+ parser = parser('GET', '/http?ok=true', headers)
+ request = Vines::Stream::Http::Request.new(@stream, parser, '<html></html>')
+
+ headers = [
+ "HTTP/1.1 301 Moved Permanently",
+ "Content-Length: 0",
+ "Location: http://wonderland.lit/http/?ok=true"
+ ].join("\r\n")
+
+ @stream.expect(:stream_write, nil, ["#{headers}\r\n\r\n"])
+ # so the /http url above will work
+ request.reply_with_file(File.expand_path('../../', __FILE__))
+ assert @stream.verify
+ end
+ end
+
+ describe 'reply_to_options' do
+ it 'returns cors headers' do
+ headers = [
+ 'Content-Type: text/xml',
+ 'Host: wonderland.lit',
+ 'Origin: remote.wonderland.lit',
+ 'Access-Control-Request-Headers: Content-Type, Origin'
+ ]
+ parser = parser('OPTIONS', '/xmpp', headers)
+ request = Vines::Stream::Http::Request.new(@stream, parser, '')
+
+ headers = [
+ "HTTP/1.1 200 OK",
+ "Content-Length: 0",
+ "Access-Control-Allow-Origin: *",
+ "Access-Control-Allow-Methods: POST, GET, OPTIONS",
+ "Access-Control-Allow-Headers: Content-Type, Origin",
+ "Access-Control-Max-Age: 2592000"
+ ].join("\r\n")
+
+ @stream.expect(:stream_write, nil, ["#{headers}\r\n\r\n"])
+ request.reply_to_options
+ assert @stream.verify
+ end
+ end
+
+ describe 'reply' do
+ it 'returns set-cookie header when vroute is defined' do
+ reply_with_cookie('v1')
+ end
+
+ it 'does not return set-cookie header when vroute is undefined' do
+ reply_with_cookie('')
+ end
+ end
+
+ private
+
+ # Create a parser that has completed one valid HTTP request.
+ #
+ # method - The HTTP method String (e.g. 'GET', 'POST').
+ # url - The request URL String (e.g. '/blogs/12?ok=true').
+ # headers - The optional Array of request headers.
+ #
+ # Returns an Http::Parser.
+ def parser(method, url, headers = [])
+ head = ["#{method} #{url} HTTP/1.1"].concat(headers).join("\r\n")
+ body = '<html></html>'
+ Http::Parser.new.tap do |parser|
+ parser << "%s\r\n\r\n%s" % [head, body]
+ end
+ end
+
+ def reply_with_cookie(cookie)
+ config = Vines::Config.new do
+ host 'wonderland.lit' do
+ storage(:fs) { dir Dir.tmpdir }
+ end
+ http '0.0.0.0', 5280 do
+ vroute cookie
+ end
+ end
+
+ headers = [
+ 'Content-Type: text/xml',
+ 'Host: wonderland.lit',
+ 'Origin: remote.wonderland.lit'
+ ]
+ parser = parser('POST', '/xmpp', headers)
+
+ request = Vines::Stream::Http::Request.new(@stream, parser, '')
+ message = '<message>hello</message>'
+
+ headers = [
+ "HTTP/1.1 200 OK",
+ "Access-Control-Allow-Origin: *",
+ "Content-Type: application/xml",
+ "Content-Length: 24",
+ ]
+ headers << "Set-Cookie: vroute=#{cookie}; path=/; HttpOnly" unless cookie.empty?
+ headers = headers.join("\r\n")
+
+ @stream.expect(:stream_write, nil, ["#{headers}\r\n\r\n#{message}"])
+ @stream.expect(:config, config)
+ request.reply(message, 'application/xml')
+ assert @stream.verify
+ end
+end
diff --git a/test/stream/http/sessions_test.rb b/test/stream/http/sessions_test.rb
new file mode 100644
index 0000000..ce83d98
--- /dev/null
+++ b/test/stream/http/sessions_test.rb
@@ -0,0 +1,49 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stream::Http::Sessions do
+ class MockSessions < Vines::Stream::Http::Sessions
+ def start_timer
+ # do nothing
+ end
+ end
+
+ def setup
+ @sessions = MockSessions.new
+ end
+
+ def test_session_add_and_delete
+ session = "session"
+ assert_nil @sessions['42']
+ @sessions['42'] = session
+ assert_equal session, @sessions['42']
+ @sessions.delete('42')
+ assert_nil @sessions['42']
+ end
+
+ def test_access_singleton_through_class_methods
+ session = "session"
+ assert_nil MockSessions['42']
+ MockSessions['42'] = session
+ assert_equal session, MockSessions['42']
+ MockSessions.delete('42')
+ assert_nil MockSessions['42']
+ end
+
+ def test_cleanup
+ live = MiniTest::Mock.new
+ live.expect(:expired?, false)
+
+ dead = MiniTest::Mock.new
+ dead.expect(:expired?, true)
+ dead.expect(:close, nil)
+
+ @sessions['live'] = live
+ @sessions['dead'] = dead
+
+ @sessions.send(:cleanup)
+ assert live.verify
+ assert dead.verify
+ end
+end
diff --git a/test/stream/http/start_test.rb b/test/stream/http/start_test.rb
new file mode 100644
index 0000000..23ece79
--- /dev/null
+++ b/test/stream/http/start_test.rb
@@ -0,0 +1,50 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stream::Http::Start do
+ before do
+ @stream = MiniTest::Mock.new
+ @state = Vines::Stream::Http::Start.new(@stream)
+ end
+
+ def test_missing_body_raises_error
+ node = node('<presence type="unavailable"/>')
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ end
+
+ def test_body_with_missing_namespace_raises_error
+ node = node('<body rid="42" sid="12"/>')
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ end
+
+ def test_missing_session_starts_stream
+ EM.run do
+ node = node('<body xmlns="http://jabber.org/protocol/httpbind" rid="42" sid="12"/>')
+ @stream.expect(:start, nil, [node])
+ @stream.expect(:advance, nil, [Vines::Stream::Http::Auth.new(@stream)])
+ @state.node(node)
+ assert @stream.verify
+ EM.stop
+ end
+ end
+
+ def test_valid_session_resumes_stream
+ EM.run do
+ node = node('<body xmlns="http://jabber.org/protocol/httpbind" rid="42" sid="123"/>')
+ session = MiniTest::Mock.new
+ session.expect(:resume, nil, [@stream, node])
+ Vines::Stream::Http::Sessions['123'] = session
+ @state.node(node)
+ assert @stream.verify
+ assert session.verify
+ EM.stop
+ end
+ end
+
+ private
+
+ def node(xml)
+ Nokogiri::XML(xml).root
+ end
+end
diff --git a/test/stream/parser_test.rb b/test/stream/parser_test.rb
new file mode 100644
index 0000000..fc04979
--- /dev/null
+++ b/test/stream/parser_test.rb
@@ -0,0 +1,122 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stream::Parser do
+ STREAM_START = '<stream:stream to="wonderland.lit" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams">'.freeze
+
+ before do
+ @events = []
+ @parser = Vines::Stream::Parser.new.tap do |p|
+ p.stream_open {|el| @events << el }
+ p.stream_close { @events << :close }
+ p.stanza {|el| @events << el }
+ end
+ end
+
+ def test_xpath_to_subclass
+ expected = []
+ stanzas = [
+ ['<message></message>', Vines::Stanza::Message],
+ ['<presence/>', Vines::Stanza::Presence],
+ ['<presence type="bogus"/>', Vines::Stanza::Presence],
+ ['<presence type="error"/>', Vines::Stanza::Presence::Error],
+ ['<presence type="probe"/>', Vines::Stanza::Presence::Probe],
+ ['<presence type="subscribe"/>', Vines::Stanza::Presence::Subscribe],
+ ['<presence type="subscribed"/>', Vines::Stanza::Presence::Subscribed],
+ ['<presence type="unavailable"/>', Vines::Stanza::Presence::Unavailable],
+ ['<presence type="unsubscribe"/>', Vines::Stanza::Presence::Unsubscribe],
+ ['<presence type="unsubscribed"/>', Vines::Stanza::Presence::Unsubscribed],
+ ['<iq id="42" type="get"><query xmlns="http://jabber.org/protocol/disco#info"></query></iq>', Vines::Stanza::Iq::Query::DiscoInfo],
+ ['<iq id="42" type="get"><query xmlns="http://jabber.org/protocol/disco#items"></query></iq>', Vines::Stanza::Iq::Query::DiscoItems],
+ ['<iq id="42" type="error"></iq>', Vines::Stanza::Iq::Error],
+ ['<iq id="42" type="get"><query xmlns="jabber:iq:private"/></iq>', Vines::Stanza::Iq::PrivateStorage],
+ ['<iq id="42" type="set"><query xmlns="jabber:iq:private"/></iq>', Vines::Stanza::Iq::PrivateStorage],
+ ['<iq id="42" type="get"><ping xmlns="urn:xmpp:ping"/></iq>', Vines::Stanza::Iq::Ping],
+ ['<iq id="42" type="result"></iq>', Vines::Stanza::Iq::Result],
+ ['<iq id="42" type="get"><query xmlns="jabber:iq:roster"/></iq>', Vines::Stanza::Iq::Query::Roster],
+ ['<iq id="42" type="set"><query xmlns="jabber:iq:roster"/></iq>', Vines::Stanza::Iq::Query::Roster],
+ ['<iq id="42" type="set"><session xmlns="urn:ietf:params:xml:ns:xmpp-session"/></iq>', Vines::Stanza::Iq::Session],
+ ['<iq id="42" type="get"><vCard xmlns="vcard-temp"/></iq>', Vines::Stanza::Iq::Vcard],
+ ['<iq type="get"><vCard xmlns="vcard-temp"/></iq>', Vines::Stanza::Iq],
+ ['<iq id="42"><vCard xmlns="vcard-temp"/></iq>', Vines::Stanza::Iq],
+ ['<iq><vCard xmlns="vcard-temp"/></iq>', Vines::Stanza::Iq],
+ ['<bogus/>', NilClass],
+ ]
+ @parser << STREAM_START
+ stanzas.each do |stanza, klass|
+ @parser << stanza
+ expected << klass
+ end
+ @parser << '</stream:stream>'
+ assert_equal 'stream', @events.shift.name
+ assert_equal :close, @events.pop
+ assert_equal expected.size, @events.size
+ @events.each_with_index do |ev, ix|
+ assert_equal expected[ix], Vines::Stanza.from_node(ev, nil).class
+ end
+ end
+
+ def test_stream_namespace_with_default_prefix
+ @parser << STREAM_START
+ assert_equal 1, @events.size
+ stream = @events.shift
+ assert_equal 'stream', stream.name
+ refute_nil stream.namespace
+ assert_equal 'stream', stream.namespace.prefix
+ assert_equal 'http://etherx.jabber.org/streams', stream.namespace.href
+ expected = {'xmlns' => 'jabber:client', 'xmlns:stream' => 'http://etherx.jabber.org/streams'}
+ assert_equal expected, stream.namespaces
+ end
+
+ def test_stanzas_ignore_default_namespace
+ @parser << STREAM_START
+ @parser << '<message to="alice at wonderland.lit">hello!</message>'
+ assert_equal 2, @events.size
+ @events.shift # discard stream
+ msg = @events.shift
+ assert_equal 'message', msg.name
+ assert msg.namespaces.empty?
+ assert_nil msg.namespace
+ end
+
+ def test_nested_elements_have_namespace
+ @parser << STREAM_START
+ @parser << %q{
+ <iq from='alice at wonderland.lit/tea' id='42' type='set'>
+ <query xmlns='jabber:iq:roster'>
+ <item jid='hatter at wonderland.lit' name='Mad Hatter'>
+ <group>Tea Party</group>
+ </item>
+ </query>
+ </iq>
+ }
+ assert_equal 2, @events.size
+ @events.shift # discard stream
+ iq = @events.shift
+ assert_equal 'iq', iq.name
+ assert iq.namespaces.empty?
+ assert_nil iq.namespace
+
+ query = iq.elements.first
+ refute_nil query.namespace
+ assert_nil query.namespace.prefix
+ assert_equal 'jabber:iq:roster', query.namespace.href
+ expected = {'xmlns' => 'jabber:iq:roster'}
+ assert_equal expected, query.namespaces
+ end
+
+ def test_error_stanzas_have_stream_namespace
+ @parser << STREAM_START
+ @parser << '<stream:error><not-well-formed xmlns="urn:ietf:params:xml:ns:xmpp-streams"/></stream:error>'
+ assert_equal 2, @events.size
+ @events.shift # discard stream
+ error = @events.shift
+ assert_equal 'error', error.name
+ refute_nil error.namespace
+ assert_equal 'stream', error.namespace.prefix
+ assert_equal 'http://etherx.jabber.org/streams', error.namespace.href
+ expected = {'xmlns:stream' => 'http://etherx.jabber.org/streams'}
+ assert_equal expected, error.namespaces
+ end
+end
diff --git a/test/stream/sasl_test.rb b/test/stream/sasl_test.rb
new file mode 100644
index 0000000..2c35ec3
--- /dev/null
+++ b/test/stream/sasl_test.rb
@@ -0,0 +1,195 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::Stream::SASL do
+ subject { Vines::Stream::SASL.new(stream) }
+ let(:stream) { MiniTest::Mock.new }
+ let(:storage) { MiniTest::Mock.new }
+ let(:romeo) { Vines::User.new(jid: 'romeo at verona.lit') }
+
+ before do
+ def subject.log
+ Class.new do
+ def method_missing(*args)
+ # do nothing
+ end
+ end.new
+ end
+ end
+
+ describe '#plain_auth' do
+ before do
+ stream.expect :domain, 'verona.lit'
+ end
+
+ it 'fails with empty input' do
+ -> { subject.plain_auth(nil) }.must_raise Vines::SaslErrors::IncorrectEncoding
+ -> { subject.plain_auth('') }.must_raise Vines::SaslErrors::NotAuthorized
+ end
+
+ it 'fails with plain text' do
+ -> { subject.plain_auth('bogus') }.must_raise Vines::SaslErrors::IncorrectEncoding
+ end
+
+ it 'fails with incorrectly encoded base64 text' do
+ -> { subject.plain_auth('=dmVyb25hLmxpdA==') }.must_raise Vines::SaslErrors::IncorrectEncoding
+ -> { subject.plain_auth("dmVyb25hLmxpdA==\n") }.must_raise Vines::SaslErrors::IncorrectEncoding
+ end
+
+ it 'fails when authzid does not match authcid username' do
+ encoded = Base64.strict_encode64("juliet at verona.lit\x00romeo\x00secr3t")
+ -> { subject.plain_auth(encoded) }.must_raise Vines::SaslErrors::InvalidAuthzid
+ stream.verify
+ end
+
+ it 'fails when authzid does not match authcid domain' do
+ encoded = Base64.strict_encode64("romeo at wonderland.lit\x00romeo\x00secr3t")
+ -> { subject.plain_auth(encoded) }.must_raise Vines::SaslErrors::InvalidAuthzid
+ stream.verify
+ end
+
+ it 'fails when username and password are missing' do
+ encoded = Base64.strict_encode64("\x00\x00")
+ -> { subject.plain_auth(encoded) }.must_raise Vines::SaslErrors::NotAuthorized
+ end
+
+ it 'fails when username is missing' do
+ encoded = Base64.strict_encode64("\x00\x00secr3t")
+ -> { subject.plain_auth(encoded) }.must_raise Vines::SaslErrors::NotAuthorized
+ end
+
+ it 'fails when password is missing with delimiter' do
+ encoded = Base64.strict_encode64("\x00romeo\x00")
+ -> { subject.plain_auth(encoded) }.must_raise Vines::SaslErrors::NotAuthorized
+ end
+
+ it 'fails when password is missing' do
+ encoded = Base64.strict_encode64("\x00romeo")
+ -> { subject.plain_auth(encoded) }.must_raise Vines::SaslErrors::NotAuthorized
+ end
+
+ it 'fails with invalid jid' do
+ encoded = Base64.strict_encode64("\x00#{'a' * 1024}\x00secr3t")
+ -> { subject.plain_auth(encoded) }.must_raise Vines::SaslErrors::NotAuthorized
+ stream.verify
+ end
+
+ it 'fails with invalid password' do
+ storage.expect :authenticate, nil, [romeo.jid, 'secr3t']
+ stream.expect :storage, storage
+
+ encoded = Base64.strict_encode64("\x00romeo\x00secr3t")
+ -> { subject.plain_auth(encoded) }.must_raise Vines::SaslErrors::NotAuthorized
+
+ stream.verify
+ storage.verify
+ end
+
+ it 'passes with valid password' do
+ storage.expect :authenticate, romeo, [romeo.jid, 'secr3t']
+ stream.expect :storage, storage
+
+ encoded = Base64.strict_encode64("\x00romeo\x00secr3t")
+ subject.plain_auth(encoded).must_equal romeo
+
+ stream.verify
+ storage.verify
+ end
+
+ it 'passes with valid password and unicode jid' do
+ user = Vines::User.new(jid: 'piñata at verona.lit')
+ storage.expect :authenticate, user, [user.jid, 'secr3t']
+ stream.expect :storage, storage
+
+ encoded = Base64.strict_encode64("\x00piñata\x00secr3t")
+ subject.plain_auth(encoded).must_equal user
+
+ stream.verify
+ storage.verify
+ end
+
+ it 'passes with valid password and authzid provided by strophe and blather' do
+ storage.expect :authenticate, romeo, [romeo.jid, 'secr3t']
+ stream.expect :storage, storage
+
+ encoded = Base64.strict_encode64("romeo at Verona.LIT\x00romeo\x00secr3t")
+ subject.plain_auth(encoded).must_equal romeo
+
+ stream.verify
+ storage.verify
+ end
+
+ it 'passes with valid password and authzid provided by smack' do
+ storage.expect :authenticate, romeo, [romeo.jid, 'secr3t']
+ stream.expect :storage, storage
+
+ encoded = Base64.strict_encode64("romeo\x00romeo\x00secr3t")
+ subject.plain_auth(encoded).must_equal romeo
+
+ stream.verify
+ storage.verify
+ end
+
+ it 'raises temporary-auth-failure when storage backend fails' do
+ storage = Class.new do
+ def authenticate(*args)
+ raise 'boom'
+ end
+ end.new
+
+ stream.expect :storage, storage
+ encoded = Base64.strict_encode64("\x00romeo\x00secr3t")
+ -> { subject.plain_auth(encoded) }.must_raise Vines::SaslErrors::TemporaryAuthFailure
+ stream.verify
+ end
+ end
+
+ describe '#external_auth' do
+ it 'fails with empty input' do
+ stream.expect :remote_domain, 'verona.lit'
+ -> { subject.external_auth(nil) }.must_raise Vines::SaslErrors::IncorrectEncoding
+ -> { subject.external_auth('') }.must_raise Vines::SaslErrors::InvalidAuthzid
+ stream.verify
+ end
+
+ it 'fails with plain text' do
+ -> { subject.external_auth('bogus') }.must_raise Vines::SaslErrors::IncorrectEncoding
+ stream.verify
+ end
+
+ it 'fails with incorrectly encoded base64 text' do
+ -> { subject.external_auth('=dmVyb25hLmxpdA==') }.must_raise Vines::SaslErrors::IncorrectEncoding
+ -> { subject.external_auth("dmVyb25hLmxpdA==\n") }.must_raise Vines::SaslErrors::IncorrectEncoding
+ stream.verify
+ end
+
+ it 'passes with empty authzid and matching cert' do
+ stream.expect :remote_domain, 'verona.lit'
+ stream.expect :cert_domain_matches?, true, ['verona.lit']
+ subject.external_auth('=').must_equal true
+ stream.verify
+ end
+
+ it 'fails with empty authzid and non-matching cert' do
+ stream.expect :remote_domain, 'verona.lit'
+ stream.expect :cert_domain_matches?, false, ['verona.lit']
+ -> { subject.external_auth('=') }.must_raise Vines::SaslErrors::NotAuthorized
+ stream.verify
+ end
+
+ it 'fails when authzid does not match stream from address' do
+ stream.expect :remote_domain, 'not.verona.lit'
+ -> { subject.external_auth('dmVyb25hLmxpdA==') }.must_raise Vines::SaslErrors::InvalidAuthzid
+ stream.verify
+ end
+
+ it 'passes when authzid matches stream from address' do
+ stream.expect :remote_domain, 'verona.lit'
+ stream.expect :remote_domain, 'verona.lit'
+ stream.expect :cert_domain_matches?, true, ['verona.lit']
+ subject.external_auth('dmVyb25hLmxpdA==').must_equal true
+ stream.verify
+ end
+ end
+end
diff --git a/test/stream/server/auth_method_test.rb b/test/stream/server/auth_method_test.rb
new file mode 100644
index 0000000..b0636c1
--- /dev/null
+++ b/test/stream/server/auth_method_test.rb
@@ -0,0 +1,124 @@
+# encoding: UTF-8
+
+require "test_helper"
+
+class OperatorWrapper
+ def <<(stream)
+ [stream]
+ end
+end
+
+describe Vines::Stream::Server::AuthMethod do
+ before do
+ @result = {from: "hostA.org", to: "hostB.org", token: "1234"}
+ @stream = MiniTest::Mock.new
+ @state = Vines::Stream::Server::AuthMethod.new(@stream)
+ end
+
+ def test_invalid_element
+ EM.run {
+ node = node("<message/>")
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ EM.stop
+ }
+ end
+
+ def test_invalid_tls_element
+ EM.run {
+ node = node(%(<message xmlns="#{Vines::NAMESPACES[:tls]}"/>))
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ EM.stop
+ }
+ end
+
+ def test_invalid_dialback_element
+ EM.run {
+ node = node(%(<message xmlns:db="#{Vines::NAMESPACES[:legacy_dialback]}"/>))
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ EM.stop
+ }
+ end
+
+ def test_missing_tls_namespace
+ EM.run {
+ node = node("<starttls/>")
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ EM.stop
+ }
+ end
+
+ def test_no_dialback_payload
+ EM.run {
+ node = node("<db:result/>")
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ EM.stop
+ }
+ end
+
+ def test_invalid_tls_namespace
+ EM.run {
+ node = node(%(<starttls xmlns="#{Vines::NAMESPACES[:legacy_dialback]}"/>))
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ EM.stop
+ }
+ end
+
+ def test_missing_tls_certificate
+ EM.run {
+ @stream.expect(:encrypt?, false)
+ @stream.expect(:close_connection_after_writing, nil)
+ failure = %(<failure xmlns="#{Vines::NAMESPACES[:tls]}"/>)
+ node = node(%(<starttls xmlns="#{Vines::NAMESPACES[:tls]}"/>))
+ @stream.expect(:write, nil, [failure])
+ @stream.expect(:write, nil, ["</stream:stream>"])
+ @state.node(node)
+ assert @stream.verify
+ EM.stop
+ }
+ end
+
+ def test_valid_tls
+ EM.run {
+ @stream.expect(:encrypt?, true)
+ @stream.expect(:encrypt, nil)
+ @stream.expect(:reset, nil)
+ @stream.expect(:advance, nil, [Vines::Stream::Server::AuthRestart.new(@stream)])
+ success = %(<proceed xmlns="#{Vines::NAMESPACES[:tls]}"/>)
+ node = node(%(<starttls xmlns="#{Vines::NAMESPACES[:tls]}"/>))
+ @stream.expect(:write, nil, [success])
+ @state.node(node)
+ assert @stream.verify
+ EM.stop
+ }
+ end
+
+ def test_valid_dialback
+ EM.run {
+ @stream.expect(:config, Vines::Config)
+ @stream.expect(:router, OperatorWrapper.new)
+ @stream.expect(:close_connection_after_writing, nil)
+ node = node(
+ %(<db:result xmlns:db="#{Vines::NAMESPACES[:legacy_dialback]}" ) +
+ %(from="#{@result[:from]}" to="#{@result[:to]}">#{@result[:token]}</db:result>)
+ )
+ @stream.expect(:authoritative_dialback, nil, [node])
+ assert_nothing_raised {
+ @state.node(node)
+ }.must_equal(true)
+ EM.stop
+ }
+ end
+
+ private
+
+ def assert_nothing_raised
+ yield
+ true
+ rescue
+ $!
+ end
+
+ def node(xml)
+ Nokogiri::XML(xml).root
+ end
+end
diff --git a/test/stream/server/auth_test.rb b/test/stream/server/auth_test.rb
new file mode 100644
index 0000000..bf8aca7
--- /dev/null
+++ b/test/stream/server/auth_test.rb
@@ -0,0 +1,70 @@
+# encoding: UTF-8
+
+require "test_helper"
+
+describe Vines::Stream::Server::Auth do
+ # disable logging for tests
+ Class.new.extend(Vines::Log).log.level = Logger::FATAL
+
+ subject { Vines::Stream::Server::Auth.new(stream) }
+ let(:stream) { MiniTest::Mock.new }
+
+ before do
+ class << stream
+ attr_accessor :remote_domain
+ end
+ stream.remote_domain = "wonderland.lit"
+ end
+
+ describe "when given a valid authzid" do
+ before do
+ stream.expect :cert_domain_matches?, true, ["wonderland.lit"]
+ stream.expect :write, nil, [%(<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl"/>)]
+ stream.expect :advance, nil, [Vines::Stream::Server::FinalRestart]
+ stream.expect :reset, nil
+ stream.expect :authentication_mechanisms, ["EXTERNAL"]
+ end
+
+ it "passes external auth with empty authzid" do
+ EM.run {
+ node = external("=")
+ subject.node(node)
+ stream.verify
+ EM.stop
+ }
+ end
+
+ it "passes external auth with authzid matching from domain" do
+ EM.run {
+ node = external(Base64.strict_encode64("wonderland.lit"))
+ subject.node(node)
+ stream.verify
+ EM.stop
+ }
+ end
+ end
+
+ describe "when given an invalid authzid" do
+ before do
+ stream.expect :write, nil, ["</stream:stream>"]
+ stream.expect :close_connection_after_writing, nil
+ stream.expect :error, nil, [Vines::SaslErrors::InvalidAuthzid]
+ stream.expect :authentication_mechanisms, ["EXTERNAL"]
+ end
+
+ it "fails external auth with mismatched from domain" do
+ EM.run {
+ node = external(Base64.strict_encode64("verona.lit"))
+ subject.node(node)
+ stream.verify
+ EM.stop
+ }
+ end
+ end
+
+ private
+
+ def external(authzid)
+ node(%(<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="EXTERNAL">#{authzid}</auth>))
+ end
+end
diff --git a/test/stream/server/outbound/auth_dialback_result_test.rb b/test/stream/server/outbound/auth_dialback_result_test.rb
new file mode 100644
index 0000000..14a94fa
--- /dev/null
+++ b/test/stream/server/outbound/auth_dialback_result_test.rb
@@ -0,0 +1,52 @@
+# encoding: UTF-8
+
+require "test_helper"
+
+describe Vines::Stream::Server::Outbound::AuthDialbackResult do
+ before do
+ @stream = MiniTest::Mock.new
+ @state = Vines::Stream::Server::Outbound::AuthDialbackResult.new(@stream)
+ end
+
+ def test_invalid_stanza
+ EM.run {
+ node = node("<message/>")
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ assert @stream.verify
+ EM.stop
+ }
+ end
+
+ def test_invalid_result
+ EM.run {
+ node = node(
+ %(<db:result xmlns:db="#{Vines::NAMESPACES[:legacy_dialback]}" ) +
+ %(from="remote.host" to="local.host" type="invalid"/>)
+ )
+ @stream.expect(:close_connection, nil)
+ @state.node(node)
+ assert @stream.verify
+ EM.stop
+ }
+ end
+
+ def test_valid_result
+ EM.run {
+ node = node(
+ %(<db:result xmlns:db="#{Vines::NAMESPACES[:legacy_dialback]}" ) +
+ %(from="remote.host" to="local.host" type="valid"/>)
+ )
+ @stream.expect(:advance, nil, [Vines::Stream::Server::Ready])
+ @stream.expect(:notify_connected, nil)
+ @state.node(node)
+ assert @stream.verify
+ EM.stop
+ }
+ end
+
+ private
+
+ def node(xml)
+ Nokogiri::XML(xml).root
+ end
+end
diff --git a/test/stream/server/outbound/auth_external_test.rb b/test/stream/server/outbound/auth_external_test.rb
new file mode 100644
index 0000000..483afd0
--- /dev/null
+++ b/test/stream/server/outbound/auth_external_test.rb
@@ -0,0 +1,105 @@
+# encoding: UTF-8
+
+require "test_helper"
+
+describe Vines::Stream::Server::Outbound::AuthExternal do
+ before do
+ @stream = MiniTest::Mock.new
+ @state = Vines::Stream::Server::Outbound::AuthExternal.new(@stream)
+ end
+
+ def test_invalid_element
+ EM.run {
+ node = node("<message/>")
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ EM.stop
+ }
+ end
+
+ def test_invalid_sasl_element
+ EM.run {
+ node = node(%(<message xmlns="#{Vines::NAMESPACES[:sasl]}"/>))
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ EM.stop
+ }
+ end
+
+ def test_missing_namespace
+ EM.run {
+ node = node("<stream:features/>")
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ EM.stop
+ }
+ end
+
+ def test_invalid_namespace
+ EM.run {
+ node = node(%(<stream:features xmlns="bogus"/>))
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ EM.stop
+ }
+ end
+
+ def test_missing_mechanisms
+ EM.run {
+ node = node(%(<stream:features xmlns:stream="#{Vines::NAMESPACES[:stream]}"/>))
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ EM.stop
+ }
+ end
+
+ def test_missing_mechanisms_namespace
+ EM.run {
+ node = node(%(<stream:features xmlns:stream="#{Vines::NAMESPACES[:stream]}"><mechanisms/></stream:features>))
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ EM.stop
+ }
+ end
+
+ def test_missing_mechanism
+ EM.run {
+ mechanisms = %(<mechanisms xmlns="#{Vines::NAMESPACES[:sasl]}"/>)
+ node = node(%(<stream:features xmlns:stream="#{Vines::NAMESPACES[:stream]}">#{mechanisms}</stream:features>))
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ EM.stop
+ }
+ end
+
+ def test_missing_mechanism_text
+ EM.run {
+ mechanisms = %(<mechanisms xmlns="#{Vines::NAMESPACES[:sasl]}"><mechanism></mechanism></mechanisms>)
+ node = node(%(<stream:features xmlns:stream="#{Vines::NAMESPACES[:stream]}">#{mechanisms}</stream:features>))
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ EM.stop
+ }
+ end
+
+ def test_invalid_mechanism_text
+ EM.run {
+ mechanisms = %(<mechanisms xmlns="#{Vines::NAMESPACES[:sasl]}"><mechanism>BOGUS</mechanism></mechanisms>)
+ node = node(%(<stream:features xmlns:stream="#{Vines::NAMESPACES[:stream]}">#{mechanisms}</stream:features>))
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ EM.stop
+ }
+ end
+
+ def test_valid_mechanism
+ EM.run {
+ @stream.expect(:domain, "wonderland.lit")
+ expected = %(<auth xmlns="#{Vines::NAMESPACES[:sasl]}" mechanism="EXTERNAL">d29uZGVybGFuZC5saXQ=</auth>)
+ @stream.expect(:write, nil, [expected])
+ @stream.expect(:advance, nil, [Vines::Stream::Server::Outbound::AuthExternalResult.new(@stream)])
+ mechanisms = %(<mechanisms xmlns="#{Vines::NAMESPACES[:sasl]}"><mechanism>EXTERNAL</mechanism></mechanisms>)
+ node = node(%(<stream:features xmlns:stream="#{Vines::NAMESPACES[:stream]}">#{mechanisms}</stream:features>))
+ @state.node(node)
+ assert @stream.verify
+ EM.stop
+ }
+ end
+
+ private
+
+ def node(xml)
+ Nokogiri::XML(xml).root
+ end
+end
diff --git a/test/stream/server/outbound/auth_restart_test.rb b/test/stream/server/outbound/auth_restart_test.rb
new file mode 100644
index 0000000..86e924b
--- /dev/null
+++ b/test/stream/server/outbound/auth_restart_test.rb
@@ -0,0 +1,77 @@
+# encoding: UTF-8
+
+require "test_helper"
+
+describe Vines::Stream::Server::Outbound::AuthRestart do
+ before do
+ @stream = MiniTest::Mock.new
+ @state = Vines::Stream::Server::Outbound::AuthRestart.new(@stream)
+ end
+
+ def test_missing_namespace
+ EM.run {
+ node = node("<stream:stream/>")
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ assert @stream.verify
+ EM.stop
+ }
+ end
+
+ def test_invalid_namespace
+ EM.run {
+ node = node(%(<stream:stream xmlns="#{Vines::NAMESPACES[:stream]}"/>))
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ EM.stop
+ }
+ end
+
+ def test_valid_stream
+ EM.run {
+ node = node(
+ %(<stream:stream xmlns="jabber:client" xmlns:stream="#{Vines::NAMESPACES[:stream]}" ) +
+ %(xml:lang="en" id="1234" from="host.com" version="1.0">)
+ )
+ @stream.expect(:advance, nil, [Vines::Stream::Server::Outbound::AuthExternal])
+ @stream.expect(:dialback_retry?, false)
+ @state.node(node)
+ assert @stream.verify
+ EM.stop
+ }
+ end
+
+ def test_valid_stream_restart
+ EM.run {
+ node = node(
+ %(<stream:stream xmlns="jabber:client" xmlns:stream="#{Vines::NAMESPACES[:stream]}" ) +
+ %(xml:lang="en" id="1234" from="host.com" version="1.0">)
+ )
+ @stream.expect(:advance, nil, [Vines::Stream::Server::Outbound::Auth])
+ @stream.expect(:outbound_tls_required?, false)
+ @stream.expect(:dialback_retry?, true)
+ @state.node(node)
+ assert @stream.verify
+ EM.stop
+ }
+ end
+
+ def test_valid_stream_required_tls
+ EM.run {
+ node = node(
+ %(<stream:stream xmlns="jabber:client" xmlns:stream="#{Vines::NAMESPACES[:stream]}") +
+ %( xml:lang="en" id="1234" from="host.com" version="1.0">)
+ )
+ @stream.expect(:close_connection, nil)
+ @stream.expect(:outbound_tls_required?, true)
+ @stream.expect(:dialback_retry?, true)
+ @state.node(node)
+ assert @stream.verify
+ EM.stop
+ }
+ end
+
+ private
+
+ def node(xml)
+ Nokogiri::XML(xml).root
+ end
+end
diff --git a/test/stream/server/outbound/auth_test.rb b/test/stream/server/outbound/auth_test.rb
new file mode 100644
index 0000000..9951e3a
--- /dev/null
+++ b/test/stream/server/outbound/auth_test.rb
@@ -0,0 +1,113 @@
+# encoding: UTF-8
+
+require "test_helper"
+
+class OperatorWrapper
+ def <<(stream)
+ [stream]
+ end
+end
+
+class StateWrapper
+ def dialback_secret=(secret); end
+end
+
+module Vines
+ module Kit
+ def auth_token; "1234"; end
+ end
+end
+
+module Boolean; end
+class TrueClass; include Boolean; end
+class FalseClass; include Boolean; end
+class NilClass; include Boolean; end
+
+describe Vines::Stream::Server::Outbound::Auth do
+ before do
+ @stream = MiniTest::Mock.new
+ @state = Vines::Stream::Server::Outbound::Auth.new(@stream)
+ end
+
+ def test_missing_children
+ EM.run {
+ node = node("<stream:features/>")
+ @stream.expect(:dialback_verify_key?, false)
+ @stream.expect(:outbound_tls_required, nil, [Boolean])
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ assert @stream.verify
+ EM.stop
+ }
+ end
+
+ def test_invalid_children
+ EM.run {
+ node = node(%(<stream:features><message/></stream:features>))
+ @stream.expect(:dialback_verify_key?, false)
+ @stream.expect(:outbound_tls_required, nil, [Boolean])
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ assert @stream.verify
+ EM.stop
+ }
+ end
+
+ def test_valid_stream_features
+ EM.run {
+ node = node(
+ %(<stream:features xmlns:stream="#{Vines::NAMESPACES[:stream]}">) +
+ %(<starttls xmlns="#{Vines::NAMESPACES[:tls]}"><required/></starttls>) +
+ %(<dialback xmlns="#{Vines::NAMESPACES[:dialback]}"/></stream:features>)
+ )
+ starttls = %(<starttls xmlns='#{Vines::NAMESPACES[:tls]}'/>)
+ @stream.expect(:dialback_verify_key?, false)
+ @stream.expect(:outbound_tls_required, nil, [Boolean])
+ @stream.expect(:advance, nil, [Vines::Stream::Server::Outbound::TLSResult])
+ @stream.expect(:write, nil, [starttls])
+ @state.node(node)
+ assert @stream.verify
+ EM.stop
+ }
+ end
+
+ def test_dialback_feature_only
+ EM.run {
+ node = node(
+ %(<stream:features xmlns:stream="#{Vines::NAMESPACES[:stream]}">) +
+ %(<dialback xmlns="#{Vines::NAMESPACES[:dialback]}"/></stream:features>)
+ )
+ @stream.expect(:dialback_verify_key?, false)
+ @stream.expect(:router, OperatorWrapper.new)
+ @stream.expect(:domain, "local.host")
+ @stream.expect(:remote_domain, "remote.host")
+ @stream.expect(:domain, "local.host")
+ @stream.expect(:remote_domain, "remote.host")
+ @stream.expect(:id, "1234")
+ @stream.expect(:write, nil, [String])
+ @stream.expect(:outbound_tls_required, nil, [Boolean])
+ @stream.expect(:advance, nil, [Vines::Stream::Server::Outbound::AuthDialbackResult])
+ @stream.expect(:state, StateWrapper.new)
+ @state.node(node)
+ assert @stream.verify
+ EM.stop
+ }
+ end
+
+ def test_dialback_verify_key
+ EM.run {
+ node = node("<stream:stream/>")
+ @stream.expect(:advance, nil, [Vines::Stream::Server::Outbound::Authoritative])
+ @stream.expect(:dialback_verify_key?, true)
+ @stream.expect(:callback!, nil)
+ @stream.expect(:outbound_tls_required, nil, [Boolean])
+ @state.node(node)
+ assert @stream.verify
+ EM.stop
+ }
+ end
+
+ private
+
+ def node(xml)
+ Nokogiri::XML(xml).root
+ end
+end
diff --git a/test/stream/server/outbound/authoritative_test.rb b/test/stream/server/outbound/authoritative_test.rb
new file mode 100644
index 0000000..7611bd3
--- /dev/null
+++ b/test/stream/server/outbound/authoritative_test.rb
@@ -0,0 +1,86 @@
+# encoding: UTF-8
+
+require "test_helper"
+
+class RouterWrapper
+ def initialize(stream); @stream = stream; end
+ def stream_by_id(id); @stream; end
+end
+
+describe Vines::Stream::Server::Outbound::Authoritative do
+ before do
+ @stream = MiniTest::Mock.new
+ @router = RouterWrapper.new(@stream)
+ @state = Vines::Stream::Server::Outbound::Authoritative.new(@stream)
+ end
+
+ def test_invalid_stanza
+ EM.run {
+ node = node("<message/>")
+ @stream.expect(:router, @router)
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ assert @stream.verify
+ EM.stop
+ }
+ end
+
+ def test_invalid_token
+ EM.run {
+ node = node("<db:verify/>")
+ router = RouterWrapper.new(nil)
+ @stream.expect(:router, router)
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ assert @stream.verify
+ EM.stop
+ }
+ end
+
+ def test_valid_verification
+ EM.run {
+ node = node(
+ %(<db:verify xmlns:db="#{Vines::NAMESPACES[:legacy_dialback]}" ) +
+ %(from="remote.host" to="local.host" id="1234" type="valid"/>)
+ )
+ result = %(<db:result xmlns:db='#{Vines::NAMESPACES[:legacy_dialback]}' ) +
+ %(from='#{node[:to]}' to='#{node[:from]}' type='#{node[:type]}'/>)
+ @stream.expect(:router, @router)
+ # NOTE this tests the "inbound" stream var
+ @stream.expect(:write, nil, [result])
+ @stream.expect(:advance, nil, [Vines::Stream::Server::Ready])
+ @stream.expect(:notify_connected, nil)
+ # end
+ @stream.expect(:nil?, false)
+ @stream.expect(:close_connection, nil)
+ @state.node(node)
+ assert @stream.verify
+ EM.stop
+ }
+ end
+
+ def test_invalid_verification
+ EM.run {
+ node = node(
+ %(<db:verify xmlns:db="#{Vines::NAMESPACES[:legacy_dialback]}" ) +
+ %(from="remote.host" to="local.host" id="1234" type="invalid"/>)
+ )
+ result = %(<db:result xmlns:db='#{Vines::NAMESPACES[:legacy_dialback]}' ) +
+ %(from='#{node[:to]}' to='#{node[:from]}' type='#{node[:type]}'/>)
+ @stream.expect(:router, @router)
+ # NOTE this tests the "inbound" stream var
+ @stream.expect(:close_connection_after_writing, nil)
+ @stream.expect(:write, nil, [result])
+ # end
+ @stream.expect(:nil?, false)
+ @stream.expect(:close_connection, nil)
+ @state.node(node)
+ assert @stream.verify
+ EM.stop
+ }
+ end
+
+ private
+
+ def node(xml)
+ Nokogiri::XML(xml).root
+ end
+end
diff --git a/test/stream/server/outbound/start_test.rb b/test/stream/server/outbound/start_test.rb
new file mode 100644
index 0000000..2e9fbae
--- /dev/null
+++ b/test/stream/server/outbound/start_test.rb
@@ -0,0 +1,45 @@
+# encoding: UTF-8
+
+require "test_helper"
+
+describe Vines::Stream::Server::Outbound::Start do
+ before do
+ @stream = MiniTest::Mock.new
+ @state = Vines::Stream::Server::Outbound::Start.new(@stream)
+ end
+
+ def test_missing_namespace
+ EM.run {
+ node = node("<stream:stream/>")
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ EM.stop
+ }
+ end
+
+ def test_invalid_namespace
+ EM.run {
+ node = node(%(<stream:stream xmlns="#{Vines::NAMESPACES[:stream]}"/>))
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ EM.stop
+ }
+ end
+
+ def test_valid_stream
+ EM.run {
+ node = node(
+ %(<stream:stream xmlns="jabber:client" xmlns:stream="#{Vines::NAMESPACES[:stream]}" ) +
+ %(xml:lang="en" id="1234" from="host.com" version="1.0">)
+ )
+ @stream.expect(:advance, nil, [Vines::Stream::Server::Outbound::Auth])
+ @state.node(node)
+ assert @stream.verify
+ EM.stop
+ }
+ end
+
+ private
+
+ def node(xml)
+ Nokogiri::XML(xml).root
+ end
+end
diff --git a/test/stream/server/ready_test.rb b/test/stream/server/ready_test.rb
new file mode 100644
index 0000000..3a5dd6f
--- /dev/null
+++ b/test/stream/server/ready_test.rb
@@ -0,0 +1,122 @@
+# encoding: UTF-8
+
+require "test_helper"
+
+describe Vines::Stream::Server::Ready do
+ subject { Vines::Stream::Server::Ready.new(stream, nil) }
+ let(:stream) { MiniTest::Mock.new }
+
+ SERVER_STANZAS = []
+
+ before do
+ def subject.to_stanza(node)
+ Vines::Stanza.from_node(node, stream).tap do |stanza|
+ def stanza.process
+ SERVER_STANZAS << self
+ end if stanza
+ end
+ end
+ end
+
+ after do
+ SERVER_STANZAS.clear
+ end
+
+ it "processes a valid node" do
+ EM.run {
+ config = MiniTest::Mock.new
+ config.expect(:local_jid?, true, [Vines::JID.new("romeo at verona.lit")])
+
+ stream.expect(:config, config)
+ stream.expect(:remote_domain, "wonderland.lit")
+ stream.expect(:domain, "verona.lit")
+ stream.expect(:user=, nil, [Vines::User.new(jid: "alice at wonderland.lit")])
+
+ node = node(%(<message from="alice at wonderland.lit" to="romeo at verona.lit"/>))
+ subject.node(node)
+ assert_equal 1, SERVER_STANZAS.size
+ assert stream.verify
+ assert config.verify
+ EM.stop
+ }
+ end
+
+ it "raises unsupported-stanza-type stream error" do
+ EM.run {
+ node = node("<bogus/>")
+ -> { subject.node(node) }.must_raise Vines::StreamErrors::UnsupportedStanzaType
+ assert SERVER_STANZAS.empty?
+ assert stream.verify
+ EM.stop
+ }
+ end
+
+ it "raises improper-addressing stream error when to address is missing" do
+ EM.run {
+ node = node(%(<message from="alice at wonderland.lit"/>))
+ -> { subject.node(node) }.must_raise Vines::StreamErrors::ImproperAddressing
+ assert SERVER_STANZAS.empty?
+ assert stream.verify
+ EM.stop
+ }
+ end
+
+ it "raises jid-malformed stanza error when to address is invalid" do
+ EM.run {
+ node = node(%(<message from="alice at wonderland.lit" to=" "/>))
+ -> { subject.node(node) }.must_raise Vines::StanzaErrors::JidMalformed
+ assert SERVER_STANZAS.empty?
+ assert stream.verify
+ EM.stop
+ }
+ end
+
+ it "raises improper-addressing stream error" do
+ EM.run {
+ node = node(%(<message to="romeo at verona.lit"/>))
+ -> { subject.node(node) }.must_raise Vines::StreamErrors::ImproperAddressing
+ assert SERVER_STANZAS.empty?
+ assert stream.verify
+ EM.stop
+ }
+ end
+
+ it "raises jid-malformed stanza error for invalid from address" do
+ EM.run {
+ node = node(%(<message from=" " to="romeo at verona.lit"/>))
+ -> { subject.node(node) }.must_raise Vines::StanzaErrors::JidMalformed
+ assert SERVER_STANZAS.empty?
+ assert stream.verify
+ EM.stop
+ }
+ end
+
+ it "raises invalid-from stream error" do
+ EM.run {
+ stream.expect(:remote_domain, "wonderland.lit")
+ node = node(%(<message from="alice at bogus.lit" to="romeo at verona.lit"/>))
+ -> { subject.node(node) }.must_raise Vines::StreamErrors::InvalidFrom
+ assert SERVER_STANZAS.empty?
+ assert stream.verify
+ EM.stop
+ }
+ end
+
+ it "raises host-unknown stream error" do
+ EM.run {
+ stream.expect(:remote_domain, "wonderland.lit")
+ stream.expect(:domain, "verona.lit")
+ node = node(%(<message from="alice at wonderland.lit" to="romeo at bogus.lit"/>))
+ -> { subject.node(node) }.must_raise Vines::StreamErrors::HostUnknown
+ assert SERVER_STANZAS.empty?
+ assert stream.verify
+ EM.stop
+ }
+ end
+
+ private
+
+ def node(xml)
+ Nokogiri::XML(xml).root
+ end
+end
diff --git a/test/stream/server/start_test.rb b/test/stream/server/start_test.rb
new file mode 100644
index 0000000..808ec76
--- /dev/null
+++ b/test/stream/server/start_test.rb
@@ -0,0 +1,105 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+class VhostWrapper
+ def initialize(force = false)
+ @force_s2s_encryption = force
+ end
+ def force_s2s_encryption?
+ @force_s2s_encryption
+ end
+end
+
+describe Vines::Stream::Server::AuthMethod do
+ before do
+ @stream = MiniTest::Mock.new
+ @state = Vines::Stream::Server::Start.new(@stream)
+ end
+
+ def test_missing_namespace
+ EM.run {
+ node = node("<stream:stream/>")
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ EM.stop
+ }
+ end
+
+ def test_invalid_namespace
+ EM.run {
+ node = node(%(<stream:stream xmlns="#{Vines::NAMESPACES[:stream]}"/>))
+ assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
+ EM.stop
+ }
+ end
+
+ def test_valid_stream_tls_required
+ EM.run {
+ node = node(
+ %(<stream:stream xmlns="jabber:client" ) +
+ %(xmlns:stream="#{Vines::NAMESPACES[:stream]}" to="host.com" version="1.0"/>)
+ )
+ features = node(
+ %(<stream:features xmlns:stream="#{Vines::NAMESPACES[:stream]}">) +
+ %(<starttls xmlns="#{Vines::NAMESPACES[:tls]}"/>) +
+ %(<dialback xmlns="#{Vines::NAMESPACES[:dialback]}"/></stream:features>)
+ )
+ @stream.expect(:start, nil, [node])
+ @stream.expect(:vhost, VhostWrapper.new(false))
+ @stream.expect(:advance, nil, [Vines::Stream::Server::AuthMethod])
+ @stream.expect(:dialback_retry?, false)
+ @stream.expect(:write, nil, [features])
+ @state.node(node)
+ assert @stream.verify
+ EM.stop
+ }
+ end
+
+ def test_valid_stream_with_dialback_flag
+ EM.run {
+ node = node(
+ %(<stream:stream xmlns="jabber:client" ) +
+ %(xmlns:stream="#{Vines::NAMESPACES[:stream]}" to="host.com" version="1.0"/>)
+ )
+ features = node(
+ %(<stream:features xmlns:stream="#{Vines::NAMESPACES[:stream]}">) +
+ %(<dialback xmlns="#{Vines::NAMESPACES[:dialback]}"/></stream:features>)
+ )
+ @stream.expect(:start, nil, [node])
+ @stream.expect(:advance, nil, [Vines::Stream::Server::AuthMethod])
+ @stream.expect(:dialback_retry?, true)
+ @stream.expect(:write, nil, [features])
+ @state.node(node)
+ assert @stream.verify
+ EM.stop
+ }
+ end
+
+ def test_valid_stream
+ EM.run {
+ node = node(
+ %(<stream:stream xmlns="jabber:client" ) +
+ %(xmlns:stream="#{Vines::NAMESPACES[:stream]}" to="host.com" version="1.0"/>)
+ )
+ features = node(
+ %(<stream:features xmlns:stream="#{Vines::NAMESPACES[:stream]}">) +
+ %(<starttls xmlns="#{Vines::NAMESPACES[:tls]}"><required/></starttls>) +
+ %(<dialback xmlns="#{Vines::NAMESPACES[:dialback]}"/></stream:features>)
+ )
+ @stream.expect(:start, nil, [node])
+ @stream.expect(:vhost, VhostWrapper.new(true))
+ @stream.expect(:advance, nil, [Vines::Stream::Server::AuthMethod])
+ @stream.expect(:dialback_retry?, false)
+ @stream.expect(:write, nil, [features])
+ @state.node(node)
+ assert @stream.verify
+ EM.stop
+ }
+ end
+
+ private
+
+ def node(xml)
+ Nokogiri::XML(xml).root
+ end
+end
diff --git a/test/test_helper.rb b/test/test_helper.rb
new file mode 100644
index 0000000..9f4aca3
--- /dev/null
+++ b/test/test_helper.rb
@@ -0,0 +1,51 @@
+# encoding: UTF-8
+
+require 'tmpdir'
+require 'vines'
+require 'ext/nokogiri'
+require 'minitest/autorun'
+require 'rails/all'
+
+# A simple hook allowing you to run a block of code after everything is done running
+# In this case we want to delete the old sqlite database in case we run rake-test twice
+Minitest.after_run {
+ db_file = "test.db"
+ File.delete(db_file) if File.exist?(db_file)
+ puts "After_run hook deleted #{db_file}"
+}
+
+class MiniTest::Spec
+
+ # Build an <iq> xml node with the given attributes. This is useful as a
+ # quick way to build a node to use as expected stanza output from a
+ # Stream#write call.
+ #
+ # options - The Hash of xml attributes to include on the iq element. Attribute
+ # values of nil or empty? are excluded from the generated element.
+ # :body - The String xml content to include in the iq element.
+ #
+ # Examples
+ #
+ # iq(from: from, id: 42, to: to, type: 'result', body: card)
+ #
+ # Returns a Nokogiri::XML::Node.
+ def iq(options)
+ body = options.delete(:body)
+ options.delete_if {|k, v| v.nil? || v.to_s.empty? }
+ attrs = options.map {|k, v| "#{k}=\"#{v}\"" }.join(' ')
+ node("<iq #{attrs}>#{body}</iq>")
+ end
+
+ # Parse xml into a nokogiri node. Strip excessive whitespace from the xml
+ # content before parsing because it affects comparisons in MiniTest::Mock
+ # expectations.
+ #
+ # xml - The String of xml content to parse.
+ #
+ # Returns a Nokogiri::XML::Node.
+ def node(xml)
+ xml = xml.strip.gsub(/\n|\s{2,}/, '')
+ Nokogiri::XML(xml).root
+ end
+end
+
diff --git a/test/token_bucket_test.rb b/test/token_bucket_test.rb
new file mode 100644
index 0000000..ab9d8ba
--- /dev/null
+++ b/test/token_bucket_test.rb
@@ -0,0 +1,44 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::TokenBucket do
+ subject { Vines::TokenBucket.new(10, 1) }
+
+ it 'raises with invalid capacity and rate values' do
+ -> { Vines::TokenBucket.new(0, 1) }.must_raise ArgumentError
+ -> { Vines::TokenBucket.new(1, 0) }.must_raise ArgumentError
+ -> { Vines::TokenBucket.new(-1, 1) }.must_raise ArgumentError
+ -> { Vines::TokenBucket.new(1, -1) }.must_raise ArgumentError
+ end
+
+ it 'does not allow taking a negative number of tokens' do
+ -> { subject.take(-1) }.must_raise ArgumentError
+ end
+
+ it 'does not allow taking more tokens than its capacity' do
+ refute subject.take(11)
+ end
+
+ it 'allows taking all tokens, but no more' do
+ assert subject.take(10)
+ refute subject.take(1)
+ end
+
+ it 'refills over time' do
+ assert subject.take(10)
+ refute subject.take(1)
+ Time.stub(:new, Time.now + 1) do
+ assert subject.take(1)
+ refute subject.take(1)
+ end
+ end
+
+ it 'does not refill over capacity' do
+ assert subject.take(10)
+ refute subject.take(1)
+ Time.stub(:new, Time.now + 15) do
+ refute subject.take(11)
+ end
+ end
+end
diff --git a/test/user_test.rb b/test/user_test.rb
new file mode 100644
index 0000000..b0adc36
--- /dev/null
+++ b/test/user_test.rb
@@ -0,0 +1,101 @@
+# encoding: UTF-8
+
+require 'test_helper'
+
+describe Vines::User do
+ subject { Vines::User.new(jid: 'alice at wonderland.lit', name: 'Alice', password: 'secr3t') }
+
+ describe 'user equality checks' do
+ let(:alice) { Vines::User.new(jid: 'alice at wonderland.lit') }
+ let(:hatter) { Vines::User.new(jid: 'hatter at wonderland.lit') }
+
+ it 'uses class in equality check' do
+ (subject <=> 42).must_be_nil
+ end
+
+ it 'is equal to itself' do
+ assert subject == subject
+ assert subject.eql?(subject)
+ assert subject.hash == subject.hash
+ end
+
+ it 'is equal to another user with the same jid' do
+ assert subject == alice
+ assert subject.eql?(alice)
+ assert subject.hash == alice.hash
+ end
+
+ it 'is not equal to a different jid' do
+ refute subject == hatter
+ refute subject.eql?(hatter)
+ refute subject.hash == hatter.hash
+ end
+ end
+
+ describe 'initialize' do
+ it 'raises when not given a jid' do
+ -> { Vines::User.new }.must_raise ArgumentError
+ -> { Vines::User.new(jid: '') }.must_raise ArgumentError
+ end
+
+ it 'has an empty roster' do
+ subject.roster.wont_be_nil
+ subject.roster.size.must_equal 0
+ end
+ end
+
+ describe '#update_from' do
+ let(:updated) { Vines::User.new(jid: 'alice2 at wonderland.lit', name: 'Alice 2', password: "secr3t 2") }
+
+ before do
+ subject.roster << Vines::Contact.new(jid: 'hatter at wonderland.lit', name: "Hatter")
+ updated.roster << Vines::Contact.new(jid: 'cat at wonderland.lit', name: "Cheshire")
+ end
+
+ it 'updates jid, name, and password' do
+ subject.update_from(updated)
+ subject.jid.to_s.must_equal 'alice at wonderland.lit'
+ subject.name.must_equal 'Alice 2'
+ subject.password.must_equal 'secr3t 2'
+ end
+
+ it 'overwrites the entire roster' do
+ subject.update_from(updated)
+ subject.roster.size.must_equal 1
+ subject.roster.first.must_equal updated.roster.first
+ end
+
+ it 'clones roster entries' do
+ subject.update_from(updated)
+ updated.roster.first.name = 'Updated Contact 2'
+ subject.roster.first.name.must_equal 'Cheshire'
+ end
+ end
+
+ describe '#to_roster_xml' do
+ let(:expected) do
+ node(%q{
+ <iq id="42" type="result">
+ <query xmlns="jabber:iq:roster">
+ <item jid="a at wonderland.lit" name="Contact 1" subscription="none" from_diaspora="false">
+ <group>A</group>
+ <group>B</group>
+ </item>
+ <item jid="b at wonderland.lit" name="Contact 2" subscription="none" from_diaspora="false">
+ <group>C</group>
+ </item>
+ </query>
+ </iq>
+ })
+ end
+
+ before do
+ subject.roster << Vines::Contact.new(jid: 'b at wonderland.lit', name: "Contact 2", groups: %w[C])
+ subject.roster << Vines::Contact.new(jid: 'a at wonderland.lit', name: "Contact 1", groups: %w[B A])
+ end
+
+ it 'sorts group names' do
+ subject.to_roster_xml(42).must_equal expected
+ end
+ end
+end
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-ruby-extras/ruby-diaspora-vines.git
More information about the Pkg-ruby-extras-commits
mailing list