summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorArthur Taylor <codders@octomonkey.org.uk>2016-09-05 15:43:23 +0200
committerGitHub <noreply@github.com>2016-09-05 15:43:23 +0200
commit0167dce98692f707b74395977c478c2ca44fa0c7 (patch)
tree53db4ad3d930e586be4ec946b0bbbfdda5350732
parentd37818fa5ac01e2bf05c9b6c71362b41691a01f1 (diff)
parentdb7575f02de4064a7afaa10c3ae33349fadbf605 (diff)
downloadrvi_sota_client-0167dce98692f707b74395977c478c2ca44fa0c7.tar.gz
Merge pull request #8 from advancedtelematic/stable
Merge latest advancedtelematic/stable
-rw-r--r--.gitignore6
-rw-r--r--Cargo.lock595
-rw-r--r--Cargo.toml45
-rw-r--r--Makefile107
-rw-r--r--README.md72
-rw-r--r--client.toml14
-rw-r--r--docker/Dockerfile12
-rw-r--r--docker/client.toml14
-rwxr-xr-xdocker/run.sh16
-rw-r--r--docs/client-guide.adoc208
-rw-r--r--run/Dockerfile23
-rw-r--r--run/README.md (renamed from docker/README.md)0
-rwxr-xr-xrun/pkg.sh63
-rwxr-xr-xrun/run.sh55
-rw-r--r--run/sota.toml.env34
-rw-r--r--run/sota.toml.template45
-rw-r--r--run/sota_certificates3974
-rw-r--r--run/sota_client.service15
-rwxr-xr-xrun/system_info.sh17
-rw-r--r--src/broadcast.rs60
-rw-r--r--src/configuration/client.rs177
-rw-r--r--src/configuration/common.rs128
-rw-r--r--src/configuration/configuration.rs176
-rw-r--r--src/configuration/dbus.rs155
-rw-r--r--src/configuration/mod.rs12
-rw-r--r--src/datatype/auth.rs49
-rw-r--r--src/datatype/command.rs306
-rw-r--r--src/datatype/config.rs389
-rw-r--r--src/datatype/dbus.rs81
-rw-r--r--src/datatype/error.rs119
-rw-r--r--src/datatype/event.rs56
-rw-r--r--src/datatype/json_rpc.rs107
-rw-r--r--src/datatype/mod.rs26
-rw-r--r--src/datatype/package.rs79
-rw-r--r--src/datatype/system_info.rs46
-rw-r--r--src/datatype/update_report.rs182
-rw-r--r--src/datatype/url.rs106
-rw-r--r--src/event/inbound.rs28
-rw-r--r--src/event/mod.rs9
-rw-r--r--src/event/outbound.rs68
-rw-r--r--src/gateway/console.rs46
-rw-r--r--src/gateway/dbus.rs170
-rw-r--r--src/gateway/gateway.rs32
-rw-r--r--src/gateway/http.rs135
-rw-r--r--src/gateway/mod.rs13
-rw-r--r--src/gateway/socket.rs161
-rw-r--r--src/gateway/websocket.rs169
-rw-r--r--src/genivi/dbus.rs121
-rw-r--r--src/genivi/mod.rs4
-rw-r--r--src/genivi/sc.rs126
-rw-r--r--src/genivi/start.rs72
-rw-r--r--src/genivi/swm.rs63
-rw-r--r--src/http/auth_client.rs278
-rw-r--r--src/http/http_client.rs43
-rw-r--r--src/http/http_server.rs113
-rw-r--r--src/http/mod.rs11
-rw-r--r--src/http/openssl.rs47
-rw-r--r--src/http/test_client.rs35
-rw-r--r--src/interpreter.rs364
-rw-r--r--src/lib.rs82
-rw-r--r--src/main.rs361
-rw-r--r--src/oauth2.rs53
-rw-r--r--src/package_manager/deb.rs48
-rw-r--r--src/package_manager/mod.rs8
-rw-r--r--src/package_manager/otb.rs53
-rw-r--r--src/package_manager/package_manager.rs135
-rw-r--r--src/package_manager/rpm.rs46
-rw-r--r--src/package_manager/tpm.rs162
-rw-r--r--src/remote/dw.rs581
-rw-r--r--src/remote/jsonrpc.rs150
-rw-r--r--src/remote/mod.rs5
-rw-r--r--src/remote/parm.rs189
-rw-r--r--src/remote/rvi/edge.rs162
-rw-r--r--src/remote/rvi/message.rs61
-rw-r--r--src/remote/rvi/mod.rs18
-rw-r--r--src/remote/rvi/send.rs61
-rw-r--r--src/remote/svc.rs272
-rw-r--r--src/rvi/edge.rs127
-rw-r--r--src/rvi/mod.rs9
-rw-r--r--src/rvi/parameters.rs136
-rw-r--r--src/rvi/services.rs185
-rw-r--r--src/rvi/transfers.rs278
-rw-r--r--src/sota.rs141
-rw-r--r--src/test_library.rs72
-rw-r--r--tests/sota.toml45
-rw-r--r--tests/sota_client_tests.rs134
86 files changed, 10286 insertions, 2965 deletions
diff --git a/.gitignore b/.gitignore
index 1a73257..41ba4b8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
target
-Cargo.lock
-docker/sota_client
-docker/sota-installer
+run/sota_client
+.cargo
+rust-openssl
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..4bcf69c
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,595 @@
+[root]
+name = "sota_client"
+version = "0.1.0"
+dependencies = [
+ "chan 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)",
+ "chan-signal 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "crossbeam 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "dbus 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
+ "hyper 0.9.4 (git+https://github.com/hyperium/hyper)",
+ "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "nom 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "openssl 0.7.13 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
+ "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
+ "toml 0.1.30 (registry+https://github.com/rust-lang/crates.io-index)",
+ "unix_socket 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "url 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "ws 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "bit-set"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "bit-vec 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "bitflags"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "bitflags"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "byteorder"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "bytes"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "cfg-if"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "chan"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "chan-signal"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "bit-set 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "chan 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "cookie"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "openssl 0.7.13 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
+ "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
+ "url 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "crossbeam"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "dbus"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)",
+ "pkg-config 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "regex 0.1.71 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "gcc"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "gdi32-sys"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "getopts"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "httparse"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "hyper"
+version = "0.9.4"
+source = "git+https://github.com/hyperium/hyper#006f66f34a9c3c2a655118aab2186198deeb143b"
+dependencies = [
+ "cookie 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "httparse 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "mime 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "openssl 0.7.13 (registry+https://github.com/rust-lang/crates.io-index)",
+ "openssl-verify 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rotor 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
+ "spmc 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
+ "traitobject 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "url 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "vecio 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "idna"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "unicode-bidi 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "unicode-normalization 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "kernel32-sys"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "language-tags"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "lazy_static"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "lazy_static"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "libc"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "libressl-pnacl-sys"
+version = "2.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "pnacl-build-helper 1.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "log"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "matches"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "memchr"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "mime"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "mio"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "miow 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "net2 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
+ "nix 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "slab 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "miow"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "net2 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "net2"
+version = "0.2.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "nix"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "bitflags 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "nom"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "openssl"
+version = "0.7.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "bitflags 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "gcc 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)",
+ "openssl-sys 0.7.13 (registry+https://github.com/rust-lang/crates.io-index)",
+ "openssl-sys-extras 0.7.13 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "openssl-sys"
+version = "0.7.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "gdi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libressl-pnacl-sys 2.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "pkg-config 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "user32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "openssl-sys-extras"
+version = "0.7.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "gcc 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)",
+ "openssl-sys 0.7.13 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "openssl-verify"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "openssl 0.7.13 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "pnacl-build-helper"
+version = "1.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "tempdir 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "quick-error"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "rand"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "regex"
+version = "0.1.71"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "aho-corasick 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "regex-syntax 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "thread_local 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "rotor"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "mio 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quick-error 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
+ "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "rust-crypto"
+version = "0.2.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "gcc 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
+ "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "rustc-serialize"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "rustc_version"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "semver 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "semver"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "sha1"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "byteorder 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "slab"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "spmc"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "tempdir"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "thread-id"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "thread_local"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "thread-id 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "time"
+version = "0.1.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "toml"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "traitobject"
+version = "0.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "typeable"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "unicase"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "unicode-bidi"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "unix_socket"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "url"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "idna 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "user32-sys"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "utf8-ranges"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "vecio"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "void"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "winapi"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "winapi-build"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "ws"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "httparse 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "mio 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
+ "sha1 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "url 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "ws2_32-sys"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
diff --git a/Cargo.toml b/Cargo.toml
index 9625ce5..cbc8b2a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,25 +1,36 @@
[package]
name = "sota_client"
-version = "0.2.0"
-authors = ["Jerry Trieu <jerry@advancedtelematic.com>",
- "Philipp Millar <philipp@advancedtelematic.com>"]
+version = "0.1.0"
+authors = ["Jerry Trieu <jerry@advancedtelematic.com",
+ "Shaun Taheri <shaun@advancedtelematic.com>",
+ "Stevan Andjelkovic <steven@advancedtelematic.com"]
+
+[lib]
+name = "sota"
+path = "src/lib.rs"
[[bin]]
-doc = false
name = "sota_client"
path = "src/main.rs"
+doc = false
[dependencies]
-hyper = "*"
-rustc-serialize = "*"
-time = "*"
-url = "*"
-log = "*"
-env_logger = "*"
-rust-crypto = "*"
-toml = "*"
-dbus = "0.1.2"
-getopts = "*"
-
-[dev-dependencies]
-rand = "*"
+chan = "0.1.18"
+chan-signal = "0.1.6"
+crossbeam = "0.2.9"
+dbus = "0.3.3"
+env_logger = "0.3.3"
+getopts = "0.2.14"
+hyper = { git = "https://github.com/hyperium/hyper" }
+lazy_static = "0.2.1"
+log = "0.3.6"
+nom = "1.2.3"
+openssl = "0.7.13"
+rand = "0.3.14"
+rust-crypto = "0.2.36"
+rustc-serialize = "0.3.19"
+time = "0.1.35"
+toml = "0.1.30"
+unix_socket = "0.5.0"
+url = "1.1.1"
+ws = "0.5.0"
diff --git a/Makefile b/Makefile
index 95be4d1..cd07a78 100644
--- a/Makefile
+++ b/Makefile
@@ -1,25 +1,96 @@
-.PHONY: release debug docker all clean
+# set client version for logs and packages
+LOG_VERSION := $(shell git rev-parse HEAD | cut -c-7)
+PACKAGE_VERSION := $(shell git describe --tags | cut -c2-)
-SRCS := $(wildcard src/*.rs)
-SRCS += Cargo.toml
+# command for running the rust docker image
+RUST_IN_DOCKER := \
+ @docker run --rm \
+ --env SOTA_VERSION=$(LOG_VERSION) \
+ --env CARGO_HOME=/cargo \
+ --volume ~/.cargo:/cargo \
+ --volume $(CURDIR):/build \
+ --workdir /build \
+ advancedtelematic/rust:latest
-target/release/sota_client: $(SRCS)
- cargo build --release
+CARGO := $(RUST_IN_DOCKER) cargo
-target/debug/sota_client: $(SRCS)
- cargo build
+# function for building new packages
+define make-pkg
+ @docker run --rm \
+ --env-file run/sota.toml.env \
+ --env AUTH_SERVER=$(AUTH_SERVER) \
+ --env CORE_SERVER=$(CORE_SERVER) \
+ --env PACKAGE_VERSION=$(PACKAGE_VERSION) \
+ --env CARGO_HOME=/cargo \
+ --volume ~/.cargo:/cargo \
+ --volume $(CURDIR):/build \
+ --workdir /build \
+ advancedtelematic/fpm:latest \
+ run/pkg.sh $@
+endef
-docker/sota_client: target/release/sota_client
- cp target/release/sota_client docker
-docker: docker/sota_client docker/client.toml
- docker build -t advancedtelematic/sota-client docker
+.PHONY: help run clean test doc client image deb rpm version for-meta-rust
+.DEFAULT_GOAL := help
-clean:
- rm -f docker/sota_client
- cargo clean
+help:
+ @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
-# aliases
-debug: target/debug/sota_client
-release: target/release/sota_client
-all: docker
+run: image ## Run the client inside a Docker container.
+ @docker run --rm -it --net=host \
+ --env-file run/sota.toml.env \
+ --env AUTH_SECTION=$(AUTH_SECTION) \
+ --env AUTH_SERVER=$(AUTH_SERVER) \
+ --env CORE_SERVER=$(CORE_SERVER) \
+ --env CONFIG_ONLY=$(CONFIG_ONLY) \
+ --env DEVICE_UUID=$(DEVICE_UUID) \
+ --env DEVICE_VIN=$(DEVICE_VIN) \
+ --env OUTPUT_PATH=$(OUTPUT_PATH) \
+ --env REGISTRY_SERVER=$(REGISTRY_SERVER) \
+ --env RUST_LOG=$(RUST_LOG) \
+ --env TEMPLATE_PATH=$(TEMPLATE_PATH) \
+ advancedtelematic/sota-client:latest
+
+clean: ## Remove all compiled libraries, builds and temporary files.
+ $(CARGO) clean
+ @rm -f *.deb *.rpm run/*.deb run/*.rpm run/sota_client
+ @rm -f /tmp/sota_credentials.toml /tmp/sota-tpm*
+ @rm -rf rust-openssl .cargo
+
+test: rust-openssl ## Run all cargo tests.
+ $(CARGO) test
+
+doc: ## Generate documentation for the sota crate.
+ $(CARGO) doc --lib --no-deps --release
+
+clippy: ## Run clippy lint checks using the nightly compiler.
+ @docker run --rm --volume $(CURDIR):/build advancedtelematic/rust \
+ rustup run nightly cargo clippy -- -Dclippy
+
+client: rust-openssl src/ ## Compile a new release build of the client.
+ $(CARGO) build --release
+ @cp target/release/sota_client run/
+
+image: client ## Build a Docker image for running the client.
+ @docker build --tag advancedtelematic/sota-client run
+
+deb: client ## Create a new DEB package of the client.
+ $(make-pkg)
+
+rpm: client ## Create a new RPM package of the client.
+ $(make-pkg)
+
+version: ## Print the version that will be used for building packages.
+ @echo $(PACKAGE_VERSION)
+
+for-meta-rust:
+ $(RUST_IN_DOCKER) /bin/bash -c "\
+ /root/.cargo/bin/rustup override set 1.7.0 && \
+ cargo clean && \
+ cargo test"
+
+rust-openssl:
+ @git clone https://github.com/sfackler/rust-openssl $@
+ @cd $@ && git checkout df30e9e700225fb981d8a3cdfaf0b359722a4c9a
+ @mkdir -p .cargo
+ @echo 'paths = ["$@/openssl", "$@/openssl-sys", "$@/openssl-sys-extras"]' > .cargo/config
diff --git a/README.md b/README.md
index ecd9092..3750675 100644
--- a/README.md
+++ b/README.md
@@ -1,48 +1,56 @@
-# sota-client
+# SOTA client
-This is the client (in-vehicle) portion of the SOTA project. It is provided as an RPM that can be installed on a target system. See the [main SOTA Server project](https://github.com/advancedtelematic/rvi_sota_server) and [associated architecture document](http://advancedtelematic.github.io/rvi_sota_server/dev/architecture.html) for more information.
+The client source repository for [Software Over The Air](http://advancedtelematic.github.io/rvi_sota_server/) updates.
-## Building and running
+## Prerequisites
-To see the SOTA client in action, you will need some supporting components running. The general steps are:
+The simplest way to get started is via [Docker](http://www.docker.com), which is used for compiling and running the client.
-1. Build and run RVI server and client nodes
-2. Build and run rvi_sota_client
-3. Build and run rvi_sota_demo
+Alternatively (and optionally), local compilation requires a stable installation of Rust and Cargo. The easiest way to install both is via [Rustup](https://www.rustup.rs).
-### Building and running RVI nodes
+## Running the client
-You can build RVI directly from [its GitHub repo](https://github.com/PDXostc/rvi_core), or simply run our docker image. These instructions assume you are running the docker image.
+With Docker installed, `make run` will start the client.
-1. Pull the image: `docker pull advancedtelematic/rvi`.
-2. In two terminal windows, run the rvi client and server nodes
- * Client: `docker run -it --name rvi-client --expose 8901 --expose 8905-8908 -p 8901:8901 -p 8905:8905 -p 8906:8906 -p 8907:8907 -p 8908:8908 advancedtelematic/rvi client`
- * Server: `docker run -it --name rvi-server --expose 8801 --expose 8805-8808 -p 8801:8801 -p 8805:8805 -p 8806:8806 -p 8807:8807 -p 8808:8808 advancedtelematic/rvi server`
+### Makefile targets
-### Building and running SOTA client
+Run `make help` to see the full list of targets, which are:
-The SOTA client builds as a docker container. As long as you have `rust` and `cargo` installed, `make docker` should build a docker image called `sota-client`.
+Target | Description
+-------------: | :----------
+run | Run the client inside a Docker container.
+clean | Remove all compiled libraries, builds and temporary files.
+test | Run all cargo tests.
+doc | Generate documentation for the sota crate.
+clippy | Run clippy lint checks using the nightly compiler.
+client | Compile a new release build of the client.
+image | Build a Docker image for running the client.
+deb | Create an installable DEB package of the client.
+rpm | Create an installable RPM package of the client.
+version | Print the version that will be used for building packages.
-You can also build the SOTA client from within a docker container; this will be necessary if your build environment is not running linux. From the project root, run `docker run -it --rm -v $PWD:/build advancedtelematic/rust:1.2.0 /bin/bash`. Once you are at a bash prompt, run the following commands:
+## Configuration
-```
-apt-get install -y libssl-dev
-cd /build
-cargo build --release
-exit
-```
-Now you can run `make docker` from your normal build environment.
+You can configure how the client starts with `make run` by setting the following environment variables:
-Once the sota-client docker image is built (by either of the two methods above), you can run it with `docker run -it --name sota-client -p 9000:9000 --link rvi-client:rvi-client -e RUST_LOG=info advancedtelematic/sota-client`.
+Variable | Default value | Description
+-------------------: | :------------------------ | :------------------
+`AUTH_SECTION` | `false` | Set to true to authenticate on startup.
+`CONFIG_ONLY` | `false` | Set to true to generate a config file then quit.
+`AUTH_SERVER` | http://127.0.0.1:9001 | The Auth server for client authentication.
+`CORE_SERVER` | http://127.0.0.1:9000 | The Core server for client communication.
+`REGISTRY_SERVER` | http://127.0.0.1:8083 | The server used for registering new devices.
+`OUTPUT_PATH` | `/etc/sota.toml` | Path to write the newly generated config.
+`TEMPLATE_PATH` | `/etc/sota.toml.template` | Path to the template for new config files.
+`DEVICE_VIN` | (generated) | Use this VIN rather than generating a new one.
+`DEVICE_UUID` | (generated) | Use this UUID rather than generating a new one.
+`AUTH_CLIENT_ID` | (from registry server) | Use this client ID for authentication.
+`AUTH_CLIENT_SECRET` | (from registry server) | Use this client secret for authentication.
-### Run the demo
+For example, running `CONFIG_ONLY=true make run` will output a newly generated `sota.toml` to stdout then quit.
-To watch the client in action, you can run a demo with a dummy server. Clone the [rvi_sota_demo](https://github.com/PDXostc/rvi_sota_demo) project, then run `python sota_server.py http://<docker_ip_address>:8801`.
+### Further customization
-### Documentation
+Every value in the generated `sota.toml` config file can be overwritten in the `run/sota.toml.env` file.
-To create a static HTML version of the module documentation run `cargo doc`.
-Unfortunately this will only create documentation for the public interface. If
-you want the full documentation you need to run `cargo doc -v` extract the
-`rustdoc` command from the output and append `--no-defaults --passes
-"collapse-docs" --passes "unindent-comments" --passes strip-hidden` to it.
+In addition, each config value is available as a command line flag when starting the client. Command line flags take precedence over the values set in the config file. Run `sota_client --help` to see a full list.
diff --git a/client.toml b/client.toml
deleted file mode 100644
index 297690b..0000000
--- a/client.toml
+++ /dev/null
@@ -1,14 +0,0 @@
-[client]
-storage_dir = "/var/sota"
-rvi_url = "http://127.0.0.1:8901"
-edge_url = "127.0.0.1:9080"
-timeout = 20
-vin_match = 2
-
-[dbus]
-name = "org.genivi.SotaClient"
-path = "/org/genivi/SotaClient"
-interface = "org.genivi.SotaClient"
-software_manager = "org.genivi.SoftwareLoadingManager"
-software_manager_path = "/org/genivi/SoftwareLoadingManager"
-timeout = 60
diff --git a/docker/Dockerfile b/docker/Dockerfile
deleted file mode 100644
index 791a01a..0000000
--- a/docker/Dockerfile
+++ /dev/null
@@ -1,12 +0,0 @@
-FROM debian:8
-
-RUN apt-get update \
- && apt-get install -y openssl dbus libdbus-1-3 dbus-x11 libdbus-glib-1-2 \
- && mkdir /var/sota
-
-COPY sota_client /bin/sota_client
-COPY run.sh /bin/run.sh
-COPY client.toml /var/sota/client.toml
-
-EXPOSE 9080
-CMD ["/bin/run.sh"]
diff --git a/docker/client.toml b/docker/client.toml
deleted file mode 100644
index 0882ce4..0000000
--- a/docker/client.toml
+++ /dev/null
@@ -1,14 +0,0 @@
-[client]
-storage_dir = "/var/sota"
-rvi_url = "http://127.0.0.1:8901"
-edge_url = "127.0.0.1:9080"
-timeout = 20
-vin_match = 2
-
-[dbus]
-name = "org.genivi.sota_client"
-path = "/org/genivi/sota_client"
-interface = "org.genivi.sota_client"
-software_manager = "org.genivi.software_loading_manager"
-software_manager_path = "/org/genivi/software_loading_manager"
-timeout = 60
diff --git a/docker/run.sh b/docker/run.sh
deleted file mode 100755
index 250ca82..0000000
--- a/docker/run.sh
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/bin/bash
-# bash "strict mode", see
-# http://redsymbol.net/articles/unofficial-bash-strict-mode/
-set -euo pipefail
-IFS=$'\n\t'
-
-eval $(dbus-launch)
-export DBUS_SESSION_BUS_ADDRESS
-export DBUS_SESSION_BUS_PID
-
-LOGLEVEL=${LOGLEVEL:-"info"}
-SOTA_CLIENT="${SOTA_CLIENT_ADDR:-0.0.0.0}:${SOTA_CLIENT_PORT:-9080}"
-RVI="${RVI:-http://rvi-client:8901}"
-
-export RUST_LOG=${RUST_LOG:-"sota_client=$LOGLEVEL"}
-/usr/bin/sota_client -c /var/sota/client.toml -r "$RVI" -e "$SOTA_CLIENT"
diff --git a/docs/client-guide.adoc b/docs/client-guide.adoc
new file mode 100644
index 0000000..5b1ea91
--- /dev/null
+++ b/docs/client-guide.adoc
@@ -0,0 +1,208 @@
+= ATS Garage SOTA Client: Manual installation and integration guide
+:icons: font
+:toc: left
+:toclevels: 3
+
+== Starting the SOTA client
+
+The SOTA client should be run at startup, as a service. We provide a systemd service file below as an example, but systemd is not a requirement. You can also, of course, run it from the command line to test. Setting the `RUST_LOG` environment variable will let you configure how much debug info you receive.
+
+===== Example systemd service file
+----
+[Unit]
+Description=SOTA Client
+Wants=network-online.target
+After=network.target network-online.target
+Requires=network-online.target
+
+[Service]
+RestartSec=5
+Restart=on-failure
+Environment="RUST_LOG=info"
+DefaultTimeoutStopSec=5
+ExecStart=/usr/bin/sota_client --config /etc/sota.toml
+
+[Install]
+WantedBy=multi-user.target
+----
+
+== SOTA Client config file guide
+
+The config file for the SOTA client is in TOML format, and can be invoked at startup with `sota_client --config /path/to/config_file.toml`. A config file unique to each device can be downloaded from its device page on ATS Garage.
+
+=== [auth]
+
+This section is required for connection to an SOTA server that implements authentication. It needs to be the first section of the config file.
+
+----
+[auth]
+server = "https://auth-plus.gw.prod01.advancedtelematic.com" <1>
+client_id = "bf66425f-d4d6-422b-b510-7c7f178af9fe" <2>
+client_secret = "hr8nEWzQc9" <2>
+credentials_file = "/opt/sota/credentials.toml" <3>
+----
+<1> The URL of the auth server. Should not change.
+<2> A unique client ID and secret for this device. With ATS Garage, this is assigned upon device creation and should not change.
+<3> If this value is defined, SOTA client will check this file for auth credentials first, and use the credentials it finds there. If the file does not exist, it will be created with the `client_id` and `client_secret` above.
+
+=== [core]
+
+This is simply the URL of the core SOTA update server. It should not be changed.
+
+----
+[core]
+server = "https://sota-core.gw.prod01.advancedtelematic.com"
+----
+
+=== [device]
+
+This section contains device-specific configuration.
+
+----
+[device]
+uuid = "123e4567-e89b-12d3-a456-426655440000" <1>
+vin = "" <2>
+system_info = "system_info.sh" <3>
+polling_interval = 10 <4>
+packages_dir = "/tmp/" <5>
+package_manager = "deb" <6>
+certificates_path = "/tmp/sota_certificates" <7>
+----
+<1> The UUID of the device. This is assigned by ATS Garage upon device creation, and should not be changed.
+<2> The device's VIN, if it has one. Deprecated feature; this value is not used by the current version of ATS Garage.
+<3> The script to use to gather system information.
+<4> The frequency, in seconds, with which the SOTA client should poll the server for updates.
+<5> The location SOTA Client should use for temporary package storage until they are processed by the software loading manager.
+<6> The software loading manager backend to use. Possible values are `deb`, `rpm`, and `off`. If an external software loading manager is in use, this should be set to `off`.
+<7> The certificate authorities SOTA Client trusts. Defaults taken from Mozilla Servo.
+
+=== [network]
+
+This section contains network configuration information for various gateways. Note that if the corresponding gateway is not enabled, these settings will have no effect.
+
+----
+[network]
+http_server = "127.0.0.1:8080" <1>
+socket_commands_path = "/tmp/sota-commands.socket" <2>
+socket_events_path = "/tmp/sota-events.socket" <3>
+websocket_server = "https://sota-core.gw.prod01.advancedtelematic.com" <4>
+rvi_edge_server = "http://127.0.0.1:9080" <5>
+----
+<1> The path to the http-only core server, if the http gateway is enabled in the [gateway] section.
+<2> The name of the unix domain socket to be used for sending commands, if the socket gateway is enabled in the [gateway] section.
+<3> The name of the unix domain socket to be used for sending events, if the socket gateway is enabled in the [gateway] section.
+<4> The location of the websocket server, for communication with the ATS Garage Core server.
+<5> The location of the RVI edge node, if the rvi gateway is enabled in the [gateway] section.
+
+=== [gateway]
+
+OTA Client communicates externally with the SOTA Core server, and internally with the device's software loading manager. This section defines which gateways/protocols it should use to do so.
+
+----
+[gateway]
+console = false <1>
+dbus = false
+http = false
+rvi = false
+socket = false <2>
+websocket = true
+----
+<1> REPL mode, for debug use only.
+<2> Unix domain sockets for local communication.
+
+==== Optional gateway: [rvi]
+
+Remote Vehicle Interaction (RVI) is an open source infrastructure developed by GENIVI and Jaguar Land Rover to power the next generation of connected vehicle services. This section contains values for configuration of RVI nodes. Note that having this section defined does not imply that RVI will be used; if the RVI gateway is turned off in the `[gateway]` section, this is ignored.
+
+----
+[rvi]
+client = "http://127.0.0.1:8901"
+storage_dir = "/var/sota"
+timeout = 20
+----
+
+==== Optional gateway: [dbus]
+
+This section contains values for dbus configuration, using the GENIVI software loading manager's names as the default. Note that having this section defined does not imply that dbus will be used; if the dbus gateway is turned off in the `[gateway]` section, this is ignored.
+
+----
+[dbus]
+name = "org.genivi.SotaClient"
+path = "/org/genivi/SotaClient"
+interface = "org.genivi.SotaClient"
+software_manager = "org.genivi.SoftwareLoadingManager"
+software_manager_path = "/org/genivi/SoftwareLoadingManager"
+timeout = 60
+----
+
+== API Documentation
+
+The SOTA client is capable of integrating with various types of software loading manager (SWLM). This enables, with a relatively small amount of integration work, the installation of unlimited varieties of software or firmware packages. A complete API is available via D-Bus, but there are other options in development, including a simple unix domain socket API.
+
+=== D-Bus API
+
+For more information on the D-Bus API, please check out the https://github.com/advancedtelematic/rvi_sota_client[rvi_sota_client] repo from GitHub, and use `make doc` to build the Rustdoc library for this or better yet link:mailto:support@atsgarage.com[contact us] and let us know what you'd like to do and how we can help.
+
+=== Unix domain socket API
+
+Currently, only the core functionality of making software updates available and accepting reports on their installation is present in the unix domain socket API. A software loading manager must support the following:
+
+- It must listen for `DownloadComplete` events on the events socket. At the moment, the only events published on the events socket are DownloadComplete events, but in future other types of events may be published. The SWLM should be capable of filtering for only the type of events it is interested in.
+- It must send a `SendUpdateReport` command on the command socket, with a status code, when the update finishes.
+
+==== Configuration
+
+To communicate with the SOTA Client over unix domain sockets, `socket = true` must be defined in the `[gateway]` section of the config file. Additionally, the names of the two sockets are configurable in the link:#__network[[network]] section.
+
+==== DownloadComplete
+
+Once the SOTA client has successfully downloaded an update ordered by an ATS Garage user, it will send a DownloadComplete event on the events socket with the following body:
+
+[source,json]
+----
+{
+ "update_id": "string", <1>
+ "update_image": "string", <2>
+ "signature": "string" <3>
+}
+----
+<1> A unique ID for the update. The SWLM will need to reference this ID when reporting on the status of the install.
+<2> The location of the delivered update file.
+<3> A cryptographic signature; may be blank if the package uploader chose not to supply one. The SWLM *may* implement signature verification, but is not required to do so.
+
+==== SendUpdateReport
+
+The Software Loading Manager should send this command on the command socket upon termination of the install, whether it was successful or failed. The form of the command over unix domain sockets is `SendUpdateReport update_id result_code`.
+
+* The update ID is the one received from the `DownloadComplete` message.
+* The result of the update attempt, either in numberical or verbal form. The possible update codes are enumerated below.
+
+TIP: Over D-Bus, it is also possible to sent a longer textual description of the install status/error. This feature is not yet available via unix domain sockets, but will be in the future. It is recommended that SWLM integration developers maintain the capability to send more verbose status messages.
+
+===== Possible update result codes
+
+[cols="1,2,5",options="header"]
+|===
+| Numerical | Verbal | Description
+|0 | OK | Operation executed successfully
+|1 | ALREADY_PROCESSED | Operation has already been processed
+|2 | DEPENDENCY_FAILURE | Dependency failure during package install, upgrade, or removal
+|3 | VALIDATION_FAILED | Update image integrity has been compromised
+|4 | INSTALL_FAILED | Package installation failed
+|5 | UPGRADE_FAILED | Package upgrade failed
+|6 | REMOVAL_FAILED | Package removal failed
+|7 | FLASH_FAILED | The module loader could not flash its managed module
+|8 | CREATE_PARTITION_FAILED | Partition creation failed
+|9 | DELETE_PARTITION_FAILED | Partition deletion failed
+|10 | RESIZE_PARTITION_FAILED | Partition resize failed
+|11 | WRITE_PARTITION_FAILED | Partition write failed
+|12 | PATCH_PARTITION_FAILED | Partition patching failed
+|13 | USER_DECLINED | User declined the update
+|14 | SOFTWARE_BLACKLISTED | Software was blacklisted
+|15 | DISK_FULL | Ran out of disk space
+|16 | NOT_FOUND | Software package not found
+|17 | OLD_VERSION | Tried to downgrade to older version
+|18 | INTERNAL_ERROR | SWM Internal integrity error
+|19 | GENERAL_ERROR | Other error
+|===
+
diff --git a/run/Dockerfile b/run/Dockerfile
new file mode 100644
index 0000000..f8b98aa
--- /dev/null
+++ b/run/Dockerfile
@@ -0,0 +1,23 @@
+FROM debian:8
+
+RUN apt-get update && apt-get install -y \
+ dbus \
+ dbus-x11 \
+ gettext \
+ httpie \
+ jq \
+ libdbus-1-3 \
+ libdbus-glib-1-2 \
+ openssl \
+ lshw \
+ && rm -rf /var/lib/apt/lists/* \
+ && mkdir /var/sota
+
+COPY sota_client /usr/bin/
+COPY sota.toml.template /etc/
+COPY sota_certificates /etc/
+COPY run.sh /usr/bin/
+COPY system_info.sh /usr/bin/
+
+EXPOSE 8888 9080
+CMD ["/usr/bin/run.sh"]
diff --git a/docker/README.md b/run/README.md
index e6ec854..e6ec854 100644
--- a/docker/README.md
+++ b/run/README.md
diff --git a/run/pkg.sh b/run/pkg.sh
new file mode 100755
index 0000000..cb944ab
--- /dev/null
+++ b/run/pkg.sh
@@ -0,0 +1,63 @@
+#!/bin/bash
+
+set -eo pipefail
+
+if [ $# -lt 1 ]; then
+ echo "Usage: $0 <package> [<destination>]"
+ echo "packages: deb rpm"
+ exit 1
+fi
+
+# check package version is set
+: "${PACKAGE_VERSION:?}"
+
+PACKAGE_DIR="$(cd "$(dirname "$0")" && pwd)"
+PREFIX=/opt/sota
+
+export AUTH_SERVER="${AUTH_SERVER-http://localhost:9001}"
+export CORE_SERVER="${CORE_SERVER-http://localhost:8080}"
+export OTA_CREDENTIALS_FILE="${OTA_CREDENTIALS_FILE-${PREFIX}/credentials.toml}"
+
+case $1 in
+ "deb" )
+ export PACKAGE_MANAGER="deb"
+ PKG_BUILD_OPTS="--deb-systemd ${PACKAGE_DIR}/sota_client.service"
+ ;;
+ "rpm" )
+ export PACKAGE_MANAGER="rpm"
+ PKG_BUILD_OPTS="--rpm-service ${PACKAGE_DIR}/sota_client.service"
+ ;;
+ *)
+ echo "unknown package format $1"
+ exit 2
+esac
+shift
+
+function make_pkg {
+ destination=$1
+ template=$(mktemp)
+
+ envsubst < "${PACKAGE_DIR}/sota.toml.template" > "${template}"
+ [[ "${AUTH_SECTION}" = false ]] && sed -i '1,/\[core\]/{/\[core\]/p;d}' "${template}"
+ chmod 600 "$template"
+
+ fpm \
+ -s dir \
+ -t "${PACKAGE_MANAGER}" \
+ --architecture native \
+ --name "${PACKAGE_NAME:-sota-client}" \
+ --version "${PACKAGE_VERSION}" \
+ --package NAME-VERSION.TYPE \
+ ${PKG_BUILD_OPTS} \
+ "${PACKAGE_DIR}/sota_client=/usr/bin/sota_client" \
+ "${PACKAGE_DIR}/system_info.sh=/usr/bin/system_info.sh" \
+ "${PACKAGE_DIR}/sota_certificates=/etc/sota_certificates" \
+ "${template}=/etc/sota.toml"
+
+ if [ -n "$destination" ]; then
+ mv -f "sota-client*.${PACKAGE_MANAGER}" "${destination}"
+ fi
+ rm -f "${template}"
+}
+
+make_pkg $*
diff --git a/run/run.sh b/run/run.sh
new file mode 100755
index 0000000..4ac4d05
--- /dev/null
+++ b/run/run.sh
@@ -0,0 +1,55 @@
+#!/bin/bash
+
+set -eo pipefail
+
+# set default environment variables
+AUTH_SECTION="${AUTH_SECTION:-false}"
+AUTH_SERVER="${AUTH_SERVER:-http://localhost:9001}"
+CORE_SERVER="${CORE_SERVER:-http://localhost:8080}"
+OUTPUT_PATH="${OUTPUT_PATH:-/etc/sota.toml}"
+REGISTRY_SERVER="${REGISTRY_SERVER:-http://localhost:8083}"
+TEMPLATE_PATH="${TEMPLATE_PATH:-/etc/sota.toml.template}"
+
+# generate or use existing device vin
+RAND=$(< /dev/urandom tr -dc A-HJ-NPR-Z0-9 | head -c 13 || [[ $? -eq 141 ]])
+export DEVICE_VIN=${DEVICE_VIN:-"TEST${RAND}"}
+
+# create or use existing device uuid
+if [[ -z "${DEVICE_UUID}" ]]; then
+ DEVICE_UUID=$(http post "${REGISTRY_SERVER}/api/v1/devices" \
+ deviceName="${DEVICE_VIN}" \
+ deviceId="${DEVICE_VIN}" \
+ deviceType=Vehicle \
+ --check-status --print=b \
+ | tr -d '"')
+fi
+export DEVICE_UUID
+
+# create or use existing device credentials
+if [[ -z "${AUTH_CLIENT_ID}" ]]; then
+ CREDENTIALS=$(http post "${AUTH_SERVER}/clients" \
+ client_name="${DEVICE_VIN}" \
+ grant_types:='["client_credentials"]' \
+ --check-status --print=b)
+ AUTH_CLIENT_ID=$(echo "${CREDENTIALS}" | jq -r .client_id)
+ AUTH_CLIENT_SECRET=$(echo "${CREDENTIALS}" | jq -r .client_secret)
+fi
+export AUTH_CLIENT_ID
+export AUTH_CLIENT_SECRET
+
+# generate sota.toml config
+echo "---START CONFIG---"
+envsubst < "${TEMPLATE_PATH}" | tee "${OUTPUT_PATH}"
+echo "---END CONFIG---"
+
+# optionally remove auth section and/or quit
+[[ "${AUTH_SECTION}" = false ]] && sed -i '/\[core\]/,$!d' "${OUTPUT_PATH}"
+[[ "${CONFIG_ONLY}" = true ]] && exit 0
+
+# set up dbus
+eval "$(dbus-launch)"
+export DBUS_SESSION_BUS_ADDRESS
+export DBUS_SESSION_BUS_PID
+
+# start client
+RUST_LOG="${RUST_LOG:-debug}" sota_client --config="${OUTPUT_PATH}"
diff --git a/run/sota.toml.env b/run/sota.toml.env
new file mode 100644
index 0000000..595f905
--- /dev/null
+++ b/run/sota.toml.env
@@ -0,0 +1,34 @@
+AUTH_SERVER=http://127.0.0.1:9001
+AUTH_CREDENTIALS_FILE=/opt/sota/credentials.toml
+
+CORE_SERVER=http://127.0.0.1:8080
+
+DBUS_NAME=org.genivi.SotaClient
+DBUS_PATH=/org/genivi/SotaClient
+DBUS_INTERFACE=org.genivi.SotaClient
+DBUS_SOFTWARE_MANAGER=org.genivi.SoftwareLoadingManager
+DBUS_SOFTWARE_MANAGER_PATH=/org/genivi/SoftwareLoadingManager
+DBUS_TIMEOUT=60
+
+DEVICE_PACKAGES_DIR=/tmp/
+DEVICE_PACKAGE_MANAGER=deb
+DEVICE_POLLING_INTERVAL=10
+DEVICE_CERTIFICATES_PATH=/etc/sota_certificates
+DEVICE_SYSTEM_INFO=system_info.sh
+
+GATEWAY_CONSOLE=false
+GATEWAY_DBUS=false
+GATEWAY_HTTP=false
+GATEWAY_RVI=false
+GATEWAY_SOCKET=false
+GATEWAY_WEBSOCKET=true
+
+NETWORK_HTTP_SERVER=http://127.0.0.1:8888
+NETWORK_RVI_EDGE_SERVER=http://127.0.0.1:9080
+NETWORK_SOCKET_COMMANDS_PATH=/tmp/sota-commands.socket
+NETWORK_SOCKET_EVENTS_PATH=/tmp/sota-events.socket
+NETWORK_WEBSOCKET_SERVER=ws://127.0.0.1:3012
+
+RVI_CLIENT=http://127.0.0.1:8901
+RVI_STORAGE_DIR=/var/sota
+RVI_TIMEOUT=20
diff --git a/run/sota.toml.template b/run/sota.toml.template
new file mode 100644
index 0000000..1a68ac6
--- /dev/null
+++ b/run/sota.toml.template
@@ -0,0 +1,45 @@
+[auth]
+server = "${AUTH_SERVER}"
+client_id = "${AUTH_CLIENT_ID}"
+client_secret = "${AUTH_CLIENT_SECRET}"
+credentials_file = "${AUTH_CREDENTIALS_FILE}"
+
+[core]
+server = "${CORE_SERVER}"
+
+[dbus]
+name = "${DBUS_NAME}"
+path = "${DBUS_PATH}"
+interface = "${DBUS_INTERFACE}"
+software_manager = "${DBUS_SOFTWARE_MANAGER}"
+software_manager_path = "${DBUS_SOFTWARE_MANAGER_PATH}"
+timeout = ${DBUS_TIMEOUT}
+
+[device]
+uuid = "${DEVICE_UUID}"
+vin = "${DEVICE_VIN}"
+packages_dir = "${DEVICE_PACKAGES_DIR}"
+package_manager = "${DEVICE_PACKAGE_MANAGER}"
+system_info = "${DEVICE_SYSTEM_INFO}"
+polling_interval = ${DEVICE_POLLING_INTERVAL}
+certificates_path = "${DEVICE_CERTIFICATES_PATH}"
+
+[gateway]
+console = ${GATEWAY_CONSOLE}
+dbus = ${GATEWAY_DBUS}
+http = ${GATEWAY_HTTP}
+rvi = ${GATEWAY_RVI}
+socket = ${GATEWAY_SOCKET}
+websocket = ${GATEWAY_WEBSOCKET}
+
+[network]
+http_server = "${NETWORK_HTTP_SERVER}"
+rvi_edge_server = "${NETWORK_RVI_EDGE_SERVER}"
+socket_commands_path = "${NETWORK_SOCKET_COMMANDS_PATH}"
+socket_events_path = "${NETWORK_SOCKET_EVENTS_PATH}"
+websocket_server = "${NETWORK_WEBSOCKET_SERVER}"
+
+[rvi]
+client = "${RVI_CLIENT}"
+storage_dir = "${RVI_STORAGE_DIR}"
+timeout = ${RVI_TIMEOUT}
diff --git a/run/sota_certificates b/run/sota_certificates
new file mode 100644
index 0000000..cabaa67
--- /dev/null
+++ b/run/sota_certificates
@@ -0,0 +1,3974 @@
+-----BEGIN CERTIFICATE-----
+MIIDhDCCAwqgAwIBAgIQL4D+I4wOIg9IZxIokYesszAKBggqhkjOPQQDAzCByjEL
+MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW
+ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2ln
+biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp
+U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y
+aXR5IC0gRzQwHhcNMDcxMTA1MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCByjELMAkG
+A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJp
+U2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwg
+SW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2ln
+biBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5
+IC0gRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASnVnp8Utpkmw4tXNherJI9/gHm
+GUo9FANL+mAnINmDiWn6VMaaGF5VKmTeBvaNSjutEDxlPZCIBIngMGGzrl0Bp3ve
+fLK+ymVhAIau2o970ImtTR1ZmkGxvEeA3J5iw/mjgbIwga8wDwYDVR0TAQH/BAUw
+AwEB/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJ
+aW1hZ2UvZ2lmMCEwHzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYj
+aHR0cDovL2xvZ28udmVyaXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFLMW
+kf3upm7ktS5Jj4d4gYDs5bG1MAoGCCqGSM49BAMDA2gAMGUCMGYhDBgmYFo4e1ZC
+4Kf8NoRRkSAsdk1DPcQdhCPQrNZ8NQbOzWm9kA3bbEhCHQ6qQgIxAJw9SDkjOVga
+FRJZap7v1VmyHVIsmXHNxynfGyphe3HR3vPA5Q06Sqotp9iGKt0uEA==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE
+BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz
+dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL
+MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp
+cm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP
+Hx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr
+ba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL
+MeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1
+yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr
+VwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/
+nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ
+KoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG
+XUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj
+vbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt
+Z8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g
+N53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC
+nlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDnzCCAoegAwIBAgIBJjANBgkqhkiG9w0BAQUFADBxMQswCQYDVQQGEwJERTEc
+MBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxlU2Vj
+IFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290IENB
+IDIwHhcNOTkwNzA5MTIxMTAwWhcNMTkwNzA5MjM1OTAwWjBxMQswCQYDVQQGEwJE
+RTEcMBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxl
+U2VjIFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290
+IENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrC6M14IspFLEU
+ha88EOQ5bzVdSq7d6mGNlUn0b2SjGmBmpKlAIoTZ1KXleJMOaAGtuU1cOs7TuKhC
+QN/Po7qCWWqSG6wcmtoIKyUn+WkjR/Hg6yx6m/UTAtB+NHzCnjwAWav12gz1Mjwr
+rFDa1sPeg5TKqAyZMg4ISFZbavva4VhYAUlfckE8FQYBjl2tqriTtM2e66foai1S
+NNs671x1Udrb8zH57nGYMsRUFUQM+ZtV7a3fGAigo4aKSe5TBY8ZTNXeWHmb0moc
+QqvF1afPaA+W5OFhmHZhyJF81j4A4pFQh+GdCuatl9Idxjp9y7zaAzTVjlsB9WoH
+txa2bkp/AgMBAAGjQjBAMB0GA1UdDgQWBBQxw3kbuvVT1xfgiXotF2wKsyudMzAP
+BgNVHRMECDAGAQH/AgEFMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOC
+AQEAlGRZrTlk5ynrE/5aw4sTV8gEJPB0d8Bg42f76Ymmg7+Wgnxu1MM9756Abrsp
+tJh6sTtU6zkXR34ajgv8HzFZMQSyzhfzLMdiNlXiItiJVbSYSKpk+tYcNthEeFpa
+IzpXl/V6ME+un2pMSyuOoAPjPuCp1NJ70rOo4nI8rZ7/gFnkm0W09juwzTkZmDLl
+6iFhkOQxIY40sfcvNUqFENrnijchvllj4PKFiDFT1FQUhXB59C4Gdyd1Lx+4ivn+
+xbrYNuSD7Odlt79jWvNGr4GUN9RBjNYj1h7P9WgbRGOiWrqnNVmh5XAFmw4jV5mU
+Cm26OWMohpLzGITY+9HPBVZkVw==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIF5zCCA8+gAwIBAgIITK9zQhyOdAIwDQYJKoZIhvcNAQEFBQAwgYAxODA2BgNV
+BAMML0VCRyBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx
+c8SxMTcwNQYDVQQKDC5FQkcgQmlsacWfaW0gVGVrbm9sb2ppbGVyaSB2ZSBIaXpt
+ZXRsZXJpIEEuxZ4uMQswCQYDVQQGEwJUUjAeFw0wNjA4MTcwMDIxMDlaFw0xNjA4
+MTQwMDMxMDlaMIGAMTgwNgYDVQQDDC9FQkcgRWxla3Ryb25payBTZXJ0aWZpa2Eg
+SGl6bWV0IFNhxJ9sYXnEsWPEsXPEsTE3MDUGA1UECgwuRUJHIEJpbGnFn2ltIFRl
+a25vbG9qaWxlcmkgdmUgSGl6bWV0bGVyaSBBLsWeLjELMAkGA1UEBhMCVFIwggIi
+MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDuoIRh0DpqZhAy2DE4f6en5f2h
+4fuXd7hxlugTlkaDT7byX3JWbhNgpQGR4lvFzVcfd2NR/y8927k/qqk153nQ9dAk
+tiHq6yOU/im/+4mRDGSaBUorzAzu8T2bgmmkTPiab+ci2hC6X5L8GCcKqKpE+i4s
+tPtGmggDg3KriORqcsnlZR9uKg+ds+g75AxuetpX/dfreYteIAbTdgtsApWjluTL
+dlHRKJ2hGvxEok3MenaoDT2/F08iiFD9rrbskFBKW5+VQarKD7JK/oCZTqNGFav4
+c0JqwmZ2sQomFd2TkuzbqV9UIlKRcF0T6kjsbgNs2d1s/OsNA/+mgxKb8amTD8Um
+TDGyY5lhcucqZJnSuOl14nypqZoaqsNW2xCaPINStnuWt6yHd6i58mcLlEOzrz5z
++kI2sSXFCjEmN1ZnuqMLfdb3ic1nobc6HmZP9qBVFCVMLDMNpkGMvQQxahByCp0O
+Lna9XvNRiYuoP1Vzv9s6xiQFlpJIqkuNKgPlV5EQ9GooFW5Hd4RcUXSfGenmHmMW
+OeMRFeNYGkS9y8RsZteEBt8w9DeiQyJ50hBs37vmExH8nYQKE3vwO9D8owrXieqW
+fo1IhR5kX9tUoqzVegJ5a9KK8GfaZXINFHDk6Y54jzJ0fFfy1tb0Nokb+Clsi7n2
+l9GkLqq+CxnCRelwXQIDAJ3Zo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB
+/wQEAwIBBjAdBgNVHQ4EFgQU587GT/wWZ5b6SqMHwQSny2re2kcwHwYDVR0jBBgw
+FoAU587GT/wWZ5b6SqMHwQSny2re2kcwDQYJKoZIhvcNAQEFBQADggIBAJuYml2+
+8ygjdsZs93/mQJ7ANtyVDR2tFcU22NU57/IeIl6zgrRdu0waypIN30ckHrMk2pGI
+6YNw3ZPX6bqz3xZaPt7gyPvT/Wwp+BVGoGgmzJNSroIBk5DKd8pNSe/iWtkqvTDO
+TLKBtjDOWU/aWR1qeqRFsIImgYZ29fUQALjuswnoT4cCB64kXPBfrAowzIpAoHME
+wfuJJPaaHFy3PApnNgUIMbOv2AFoKuB4j3TeuFGkjGwgPaL7s9QJ/XvCgKqTbCmY
+Iai7FvOpEl90tYeY8pUm3zTvilORiF0alKM/fCL414i6poyWqD1SNGKfAB5UVUJn
+xk1Gj7sURT0KlhaOEKGXmdXTMIXM3rRyt7yKPBgpaP3ccQfuJDlq+u2lrDgv+R4Q
+DgZxGhBM/nV+/x5XOULK1+EVoVZVWRvRo68R2E7DpSvvkL/A7IITW43WciyTTo9q
+Kd+FPNMN4KIYEsxVL0e3p5sC/kH2iExt2qkBR4NkJ2IQgtYSe14DHzSpyZH+r11t
+hie3I6p1GMog57AP14kOpmciY/SDQSsGS7tY1dHXt7kQY9iJSrSq3RZj9W6+YKH4
+7ejWkE8axsWgKdOnIaj1Wjz3x0miIZpKlVIglnKaZsv30oZDfCK+lvm9AahH3eU7
+QPl1K5srRmSGjR70j/sHd9DqSaIcjVIUpgqT
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIE5jCCA86gAwIBAgIEO45L/DANBgkqhkiG9w0BAQUFADBdMRgwFgYJKoZIhvcN
+AQkBFglwa2lAc2suZWUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKExlBUyBTZXJ0aWZp
+dHNlZXJpbWlza2Vza3VzMRAwDgYDVQQDEwdKdXVyLVNLMB4XDTAxMDgzMDE0MjMw
+MVoXDTE2MDgyNjE0MjMwMVowXTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVlMQsw
+CQYDVQQGEwJFRTEiMCAGA1UEChMZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEQ
+MA4GA1UEAxMHSnV1ci1TSzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AIFxNj4zB9bjMI0TfncyRsvPGbJgMUaXhvSYRqTCZUXP00B841oiqBB4M8yIsdOB
+SvZiF3tfTQou0M+LI+5PAk676w7KvRhj6IAcjeEcjT3g/1tf6mTll+g/mX8MCgkz
+ABpTpyHhOEvWgxutr2TC+Rx6jGZITWYfGAriPrsfB2WThbkasLnE+w0R9vXW+RvH
+LCu3GFH+4Hv2qEivbDtPL+/40UceJlfwUR0zlv/vWT3aTdEVNMfqPxZIe5EcgEMP
+PbgFPtGzlc3Yyg/CQ2fbt5PgIoIuvvVoKIO5wTtpeyDaTpxt4brNj3pssAki14sL
+2xzVWiZbDcDq5WDQn/413z8CAwEAAaOCAawwggGoMA8GA1UdEwEB/wQFMAMBAf8w
+ggEWBgNVHSAEggENMIIBCTCCAQUGCisGAQQBzh8BAQEwgfYwgdAGCCsGAQUFBwIC
+MIHDHoHAAFMAZQBlACAAcwBlAHIAdABpAGYAaQBrAGEAYQB0ACAAbwBuACAAdgDk
+AGwAagBhAHMAdABhAHQAdQBkACAAQQBTAC0AaQBzACAAUwBlAHIAdABpAGYAaQB0
+AHMAZQBlAHIAaQBtAGkAcwBrAGUAcwBrAHUAcwAgAGEAbABhAG0ALQBTAEsAIABz
+AGUAcgB0AGkAZgBpAGsAYQBhAHQAaQBkAGUAIABrAGkAbgBuAGkAdABhAG0AaQBz
+AGUAawBzMCEGCCsGAQUFBwIBFhVodHRwOi8vd3d3LnNrLmVlL2Nwcy8wKwYDVR0f
+BCQwIjAgoB6gHIYaaHR0cDovL3d3dy5zay5lZS9qdXVyL2NybC8wHQYDVR0OBBYE
+FASqekej5ImvGs8KQKcYP2/v6X2+MB8GA1UdIwQYMBaAFASqekej5ImvGs8KQKcY
+P2/v6X2+MA4GA1UdDwEB/wQEAwIB5jANBgkqhkiG9w0BAQUFAAOCAQEAe8EYlFOi
+CfP+JmeaUOTDBS8rNXiRTHyoERF5TElZrMj3hWVcRrs7EKACr81Ptcw2Kuxd/u+g
+kcm2k298gFTsxwhwDY77guwqYHhpNjbRxZyLabVAyJRld/JXIWY7zoVAtjNjGr95
+HvxcHdMdkxuLDF2FvZkwMhgJkVLpfKG6/2SSmuz+Ne6ML678IIbsSt4beDI3poHS
+na9aEhbKmVv8b20OxaAehsmR0FyYgl9jDIpaq9iVpszLita/ZEuOyoqysOkhMp6q
+qIWYNIE5ITuoOlIyPfZrN4YGWhWY3PARZv40ILcD9EEQfTmEeZZyY7aWAuVrua0Z
+TbvGRNs2yyqcjg==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEqjCCA5KgAwIBAgIOLmoAAQACH9dSISwRXDswDQYJKoZIhvcNAQEFBQAwdjEL
+MAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNV
+BAsTGVRDIFRydXN0Q2VudGVyIENsYXNzIDIgQ0ExJTAjBgNVBAMTHFRDIFRydXN0
+Q2VudGVyIENsYXNzIDIgQ0EgSUkwHhcNMDYwMTEyMTQzODQzWhcNMjUxMjMxMjI1
+OTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1c3RDZW50ZXIgR21i
+SDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMiBDQTElMCMGA1UEAxMc
+VEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMiBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBAKuAh5uO8MN8h9foJIIRszzdQ2Lu+MNF2ujhoF/RKrLqk2jf
+tMjWQ+nEdVl//OEd+DFwIxuInie5e/060smp6RQvkL4DUsFJzfb95AhmC1eKokKg
+uNV/aVyQMrKXDcpK3EY+AlWJU+MaWss2xgdW94zPEfRMuzBwBJWl9jmM/XOBCH2J
+XjIeIqkiRUuwZi4wzJ9l/fzLganx4Duvo4bRierERXlQXa7pIXSSTYtZgo+U4+lK
+8edJsBTj9WLL1XK9H7nSn6DNqPoByNkN39r8R52zyFTfSUrxIan+GE7uSNQZu+99
+5OKdy1u2bv/jzVrndIIFuoAlOMvkaZ6vQaoahPUCAwEAAaOCATQwggEwMA8GA1Ud
+EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTjq1RMgKHbVkO3
+kUrL84J6E1wIqzCB7QYDVR0fBIHlMIHiMIHfoIHcoIHZhjVodHRwOi8vd3d3LnRy
+dXN0Y2VudGVyLmRlL2NybC92Mi90Y19jbGFzc18yX2NhX0lJLmNybIaBn2xkYXA6
+Ly93d3cudHJ1c3RjZW50ZXIuZGUvQ049VEMlMjBUcnVzdENlbnRlciUyMENsYXNz
+JTIwMiUyMENBJTIwSUksTz1UQyUyMFRydXN0Q2VudGVyJTIwR21iSCxPVT1yb290
+Y2VydHMsREM9dHJ1c3RjZW50ZXIsREM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9u
+TGlzdD9iYXNlPzANBgkqhkiG9w0BAQUFAAOCAQEAjNfffu4bgBCzg/XbEeprS6iS
+GNn3Bzn1LL4GdXpoUxUc6krtXvwjshOg0wn/9vYua0Fxec3ibf2uWWuFHbhOIprt
+ZjluS5TmVfwLG4t3wVMTZonZKNaL80VKY7f9ewthXbhtvsPcW3nS7Yblok2+XnR8
+au0WOB9/WIFaGusyiC2y8zl3gK9etmF1KdsjTYjKUCjLhdLTEKJZbtOTVAB6okaV
+hgWcqRmY5TFyDADiZ9lA4CQze28suVyrZZ0srHbqNZn1l7kPJOzHdiEoZa5X6AeI
+dUpWoNIFOqTmjZKILPPy4cHGYdtBxceb9w4aUUXCYWvcZCcXjFq32nQozZfkvQ==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDVTCCAj2gAwIBAgIESTMAATANBgkqhkiG9w0BAQUFADAyMQswCQYDVQQGEwJD
+TjEOMAwGA1UEChMFQ05OSUMxEzARBgNVBAMTCkNOTklDIFJPT1QwHhcNMDcwNDE2
+MDcwOTE0WhcNMjcwNDE2MDcwOTE0WjAyMQswCQYDVQQGEwJDTjEOMAwGA1UEChMF
+Q05OSUMxEzARBgNVBAMTCkNOTklDIFJPT1QwggEiMA0GCSqGSIb3DQEBAQUAA4IB
+DwAwggEKAoIBAQDTNfc/c3et6FtzF8LRb+1VvG7q6KR5smzDo+/hn7E7SIX1mlwh
+IhAsxYLO2uOabjfhhyzcuQxauohV3/2q2x8x6gHx3zkBwRP9SFIhxFXf2tizVHa6
+dLG3fdfA6PZZxU3Iva0fFNrfWEQlMhkqx35+jq44sDB7R3IJMfAw28Mbdim7aXZO
+V/kbZKKTVrdvmW7bCgScEeOAH8tjlBAKqeFkgjH5jCftppkA9nCTGPihNIaj3XrC
+GHn2emU1z5DrvTOTn1OrczvmmzQgLx3vqR1jGqCA2wMv+SYahtKNu6m+UjqHZ0gN
+v7Sg2Ca+I19zN38m5pIEo3/PIKe38zrKy5nLAgMBAAGjczBxMBEGCWCGSAGG+EIB
+AQQEAwIABzAfBgNVHSMEGDAWgBRl8jGtKvf33VKWCscCwQ7vptU7ETAPBgNVHRMB
+Af8EBTADAQH/MAsGA1UdDwQEAwIB/jAdBgNVHQ4EFgQUZfIxrSr3991SlgrHAsEO
+76bVOxEwDQYJKoZIhvcNAQEFBQADggEBAEs17szkrr/Dbq2flTtLP1se31cpolnK
+OOK5Gv+e5m4y3R6u6jW39ZORTtpC4cMXYFDy0VwmuYK36m3knITnA3kXr5g9lNvH
+ugDnuL8BV8F3RTIMO/G0HAiw/VGgod2aHRM2mm23xzy54cXZF/qD1T0VoDy7Hgvi
+yJA/qIYM/PmLXoXLT1tLYhFHxUV8BS9BsZ4QaRuZluBVeftOhpm4lNqGOGqTo+fL
+buXf6iFViZx9fX+Y9QCJ7uOEwFyWtcVG6kbghVW2G8kS1sHNzYDzAgE8yGnLRUhj
+2JTQ7IUOO04RZfSCjKY9ri4ilAnIXOo8gV0WKgOXFlUJ24pBgp5mmxE=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEW
+MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg
+Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh
+dGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM2WhcNMzYwOTE3MTk0NjM2WjB9
+MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi
+U2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh
+cnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA
+A4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk
+pMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf
+OQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C
+Ji/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT
+Kqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi
+HzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM
+Av+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w
++2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+
+Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3
+Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B
+26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID
+AQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE
+FE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9j
+ZXJ0LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3Js
+LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFM
+BgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUHAgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0
+Y29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRwOi8vY2VydC5zdGFy
+dGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYgU3Rh
+cnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlh
+YmlsaXR5LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2Yg
+dGhlIFN0YXJ0Q29tIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFp
+bGFibGUgYXQgaHR0cDovL2NlcnQuc3RhcnRjb20ub3JnL3BvbGljeS5wZGYwEQYJ
+YIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNT
+TCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOCAgEAFmyZ
+9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8
+jhvh3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUW
+FjgKXlf2Ysd6AgXmvB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJz
+ewT4F+irsfMuXGRuczE6Eri8sxHkfY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1
+ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3fsNrarnDy0RLrHiQi+fHLB5L
+EUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZEoalHmdkrQYu
+L6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq
+yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuC
+O3NJo2pXh5Tl1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6V
+um0ABj6y6koQOdjQK/W/7HW/lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkySh
+NOsF/5oirpt9P/FlUQqmMGqz9IgcgA38corog14=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV
+BAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X
+DTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ
+BgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3
+DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4
+QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny
+gQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw
+zBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q
+130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2
+JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw
+DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw
+ZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT
+AkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj
+AQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG
+9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h
+bV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc
+fca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu
+HWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w
+t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw
+WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFVjCCBD6gAwIBAgIQ7is969Qh3hSoYqwE893EATANBgkqhkiG9w0BAQUFADCB
+8zELMAkGA1UEBhMCRVMxOzA5BgNVBAoTMkFnZW5jaWEgQ2F0YWxhbmEgZGUgQ2Vy
+dGlmaWNhY2lvIChOSUYgUS0wODAxMTc2LUkpMSgwJgYDVQQLEx9TZXJ2ZWlzIFB1
+YmxpY3MgZGUgQ2VydGlmaWNhY2lvMTUwMwYDVQQLEyxWZWdldSBodHRwczovL3d3
+dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAoYykwMzE1MDMGA1UECxMsSmVyYXJxdWlh
+IEVudGl0YXRzIGRlIENlcnRpZmljYWNpbyBDYXRhbGFuZXMxDzANBgNVBAMTBkVD
+LUFDQzAeFw0wMzAxMDcyMzAwMDBaFw0zMTAxMDcyMjU5NTlaMIHzMQswCQYDVQQG
+EwJFUzE7MDkGA1UEChMyQWdlbmNpYSBDYXRhbGFuYSBkZSBDZXJ0aWZpY2FjaW8g
+KE5JRiBRLTA4MDExNzYtSSkxKDAmBgNVBAsTH1NlcnZlaXMgUHVibGljcyBkZSBD
+ZXJ0aWZpY2FjaW8xNTAzBgNVBAsTLFZlZ2V1IGh0dHBzOi8vd3d3LmNhdGNlcnQu
+bmV0L3ZlcmFycmVsIChjKTAzMTUwMwYDVQQLEyxKZXJhcnF1aWEgRW50aXRhdHMg
+ZGUgQ2VydGlmaWNhY2lvIENhdGFsYW5lczEPMA0GA1UEAxMGRUMtQUNDMIIBIjAN
+BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyLHT+KXQpWIR4NA9h0X84NzJB5R
+85iKw5K4/0CQBXCHYMkAqbWUZRkiFRfCQ2xmRJoNBD45b6VLeqpjt4pEndljkYRm
+4CgPukLjbo73FCeTae6RDqNfDrHrZqJyTxIThmV6PttPB/SnCWDaOkKZx7J/sxaV
+HMf5NLWUhdWZXqBIoH7nF2W4onW4HvPlQn2v7fOKSGRdghST2MDk/7NQcvJ29rNd
+QlB50JQ+awwAvthrDk4q7D7SzIKiGGUzE3eeml0aE9jD2z3Il3rucO2n5nzbcc8t
+lGLfbdb1OL4/pYUKGbio2Al1QnDE6u/LDsg0qBIimAy4E5S2S+zw0JDnJwIDAQAB
+o4HjMIHgMB0GA1UdEQQWMBSBEmVjX2FjY0BjYXRjZXJ0Lm5ldDAPBgNVHRMBAf8E
+BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUoMOLRKo3pUW/l4Ba0fF4
+opvpXY0wfwYDVR0gBHgwdjB0BgsrBgEEAfV4AQMBCjBlMCwGCCsGAQUFBwIBFiBo
+dHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbDA1BggrBgEFBQcCAjApGidW
+ZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAwDQYJKoZIhvcN
+AQEFBQADggEBAKBIW4IB9k1IuDlVNZyAelOZ1Vr/sXE7zDkJlF7W2u++AVtd0x7Y
+/X1PzaBB4DSTv8vihpw3kpBWHNzrKQXlxJ7HNd+KDM3FIUPpqojlNcAZQmNaAl6k
+SBg6hW/cnbw/nZzBh7h6YQjpdwt/cKt63dmXLGQehb+8dJahw3oS7AwaboMMPOhy
+Rp/7SNVel+axofjk70YllJyJ22k4vuxcDlbHZVHlUIiIv0LVKz3l+bqeLrPK9HOS
+Agu+TGbrIP65y7WZf+a2E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xl
+nJ2lYJU6Un/10asIbvPuW/mIPX64b24D5EI=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIID9zCCAt+gAwIBAgIESJ8AATANBgkqhkiG9w0BAQUFADCBijELMAkGA1UEBhMC
+Q04xMjAwBgNVBAoMKUNoaW5hIEludGVybmV0IE5ldHdvcmsgSW5mb3JtYXRpb24g
+Q2VudGVyMUcwRQYDVQQDDD5DaGluYSBJbnRlcm5ldCBOZXR3b3JrIEluZm9ybWF0
+aW9uIENlbnRlciBFViBDZXJ0aWZpY2F0ZXMgUm9vdDAeFw0xMDA4MzEwNzExMjVa
+Fw0zMDA4MzEwNzExMjVaMIGKMQswCQYDVQQGEwJDTjEyMDAGA1UECgwpQ2hpbmEg
+SW50ZXJuZXQgTmV0d29yayBJbmZvcm1hdGlvbiBDZW50ZXIxRzBFBgNVBAMMPkNo
+aW5hIEludGVybmV0IE5ldHdvcmsgSW5mb3JtYXRpb24gQ2VudGVyIEVWIENlcnRp
+ZmljYXRlcyBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm35z
+7r07eKpkQ0H1UN+U8i6yjUqORlTSIRLIOTJCBumD1Z9S7eVnAztUwYyZmczpwA//
+DdmEEbK40ctb3B75aDFk4Zv6dOtouSCV98YPjUesWgbdYavi7NifFy2cyjw1l1Vx
+zUOFsUcW9SxTgHbP0wBkvUCZ3czY28Sf1hNfQYOL+Q2HklY0bBoQCxfVWhyXWIQ8
+hBouXJE0bhlffxdpxWXvayHG1VA6v2G5BY3vbzQ6sm8UY78WO5upKv23KzhmBsUs
+4qpnHkWnjQRmQvaPK++IIGmPMowUc9orhpFjIpryp9vOiYurXccUwVswah+xt54u
+gQEC7c+WXmPbqOY4twIDAQABo2MwYTAfBgNVHSMEGDAWgBR8cks5x8DbYqVPm6oY
+NJKiyoOCWTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4E
+FgQUfHJLOcfA22KlT5uqGDSSosqDglkwDQYJKoZIhvcNAQEFBQADggEBACrDx0M3
+j92tpLIM7twUbY8opJhJywyA6vPtI2Z1fcXTIWd50XPFtQO3WKwMVC/GVhMPMdoG
+52U7HW8228gd+f2ABsqjPWYWqJ1MFn3AlUa1UeTiH9fqBk1jjZaM7+czV0I664zB
+echNdn3e9rG3geCg+aF4RhcaVpjwTj2rHO3sOdwHSPdj/gauwqRcalsyiMXHM4Ws
+ZkJHwlgkmeHlPuV1LI5D1l08eB6olYIpUNHRFrrvwb562bTYzB5MRuF3sTGrvSrI
+zo9uoV1/A3U05K2JRVRevq4opbs/eHnrc7MKDf2+yfdWrPa37S+bISnHOLaVxATy
+wy39FCqQmbkHzJ8=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDqzCCApOgAwIBAgIRAMcoRwmzuGxFjB36JPU2TukwDQYJKoZIhvcNAQEFBQAw
+PDEbMBkGA1UEAxMSQ29tU2lnbiBTZWN1cmVkIENBMRAwDgYDVQQKEwdDb21TaWdu
+MQswCQYDVQQGEwJJTDAeFw0wNDAzMjQxMTM3MjBaFw0yOTAzMTYxNTA0NTZaMDwx
+GzAZBgNVBAMTEkNvbVNpZ24gU2VjdXJlZCBDQTEQMA4GA1UEChMHQ29tU2lnbjEL
+MAkGA1UEBhMCSUwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGtWhf
+HZQVw6QIVS3joFd67+l0Kru5fFdJGhFeTymHDEjWaueP1H5XJLkGieQcPOqs49oh
+gHMhCu95mGwfCP+hUH3ymBvJVG8+pSjsIQQPRbsHPaHA+iqYHU4Gk/v1iDurX8sW
+v+bznkqH7Rnqwp9D5PGBpX8QTz7RSmKtUxvLg/8HZaWSLWapW7ha9B20IZFKF3ue
+Mv5WJDmyVIRD9YTC2LxBkMyd1mja6YJQqTtoz7VdApRgFrFD2UNd3V2Hbuq7s8lr
+9gOUCXDeFhF6K+h2j0kQmHe5Y1yLM5d19guMsqtb3nQgJT/j8xH5h2iGNXHDHYwt
+6+UarA9z1YJZQIDTAgMBAAGjgacwgaQwDAYDVR0TBAUwAwEB/zBEBgNVHR8EPTA7
+MDmgN6A1hjNodHRwOi8vZmVkaXIuY29tc2lnbi5jby5pbC9jcmwvQ29tU2lnblNl
+Y3VyZWRDQS5jcmwwDgYDVR0PAQH/BAQDAgGGMB8GA1UdIwQYMBaAFMFL7XC29z58
+ADsAj8c+DkWfHl3sMB0GA1UdDgQWBBTBS+1wtvc+fAA7AI/HPg5Fnx5d7DANBgkq
+hkiG9w0BAQUFAAOCAQEAFs/ukhNQq3sUnjO2QiBq1BW9Cav8cujvR3qQrFHBZE7p
+iL1DRYHjZiM/EoZNGeQFsOY3wo3aBijJD4mkU6l1P7CW+6tMM1X5eCZGbxs2mPtC
+dsGCuY7e+0X5YxtiOzkGynd6qDwJz2w2PQ8KRUtpFhpFfTMDZflScZAmlaxMDPWL
+kz/MdXSFmLr/YnpNH4n+rr2UAJm/EaXc4HnFFgt9AmEd6oX5AhVP51qJThRv4zdL
+hfXBPGHg/QVBspJ/wx2g0K5SZGBrGMYmnNj1ZOQ2GmKfig8+/21OGVZOIJFsnzQz
+OjRXUDpvgV4GxvU+fE6OK85lBi5d0ipTdF7Tbieejw==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFnDCCA4SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJGUjET
+MBEGA1UEChMKQ2VydGlub21pczEXMBUGA1UECxMOMDAwMiA0MzM5OTg5MDMxJjAk
+BgNVBAMMHUNlcnRpbm9taXMgLSBBdXRvcml0w6kgUmFjaW5lMB4XDTA4MDkxNzA4
+Mjg1OVoXDTI4MDkxNzA4Mjg1OVowYzELMAkGA1UEBhMCRlIxEzARBgNVBAoTCkNl
+cnRpbm9taXMxFzAVBgNVBAsTDjAwMDIgNDMzOTk4OTAzMSYwJAYDVQQDDB1DZXJ0
+aW5vbWlzIC0gQXV0b3JpdMOpIFJhY2luZTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
+ADCCAgoCggIBAJ2Fn4bT46/HsmtuM+Cet0I0VZ35gb5j2CN2DpdUzZlMGvE5x4jY
+F1AMnmHawE5V3udauHpOd4cN5bjr+p5eex7Ezyh0x5P1FMYiKAT5kcOrJ3NqDi5N
+8y4oH3DfVS9O7cdxbwlyLu3VMpfQ8Vh30WC8Tl7bmoT2R2FFK/ZQpn9qcSdIhDWe
+rP5pqZ56XjUl+rSnSTV3lqc2W+HN3yNw2F1MpQiD8aYkOBOo7C+ooWfHpi2GR+6K
+/OybDnT0K0kCe5B1jPyZOQE51kqJ5Z52qz6WKDgmi92NjMD2AR5vpTESOH2VwnHu
+7XSu5DaiQ3XV8QCb4uTXzEIDS3h65X27uK4uIJPT5GHfceF2Z5c/tt9qc1pkIuVC
+28+BA5PY9OMQ4HL2AHCs8MF6DwV/zzRpRbWT5BnbUhYjBYkOjUjkJW+zeL9i9Qf6
+lSTClrLooyPCXQP8w9PlfMl1I9f09bze5N/NgL+RiH2nE7Q5uiy6vdFrzPOlKO1E
+nn1So2+WLhl+HPNbxxaOu2B9d2ZHVIIAEWBsMsGoOBvrbpgT1u449fCfDu/+MYHB
+0iSVL1N6aaLwD4ZFjliCK0wi1F6g530mJ0jfJUaNSih8hp75mxpZuWW/Bd22Ql09
+5gBIgl4g9xGC3srYn+Y3RyYe63j3YcNBZFgCQfna4NH4+ej9Uji29YnfAgMBAAGj
+WzBZMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBQN
+jLZh2kS40RR9w759XkjwzspqsDAXBgNVHSAEEDAOMAwGCiqBegFWAgIAAQEwDQYJ
+KoZIhvcNAQEFBQADggIBACQ+YAZ+He86PtvqrxyaLAEL9MW12Ukx9F1BjYkMTv9s
+ov3/4gbIOZ/xWqndIlgVqIrTseYyCYIDbNc/CMf4uboAbbnW/FIyXaR/pDGUu7ZM
+OH8oMDX/nyNTt7buFHAAQCvaR6s0fl6nVjBhK4tDrP22iCj1a7Y+YEq6QpA0Z43q
+619FVDsXrIvkxmUP7tCMXWY5zjKn2BCXwH40nJ+U8/aGH88bc62UeYdocMMzpXDn
+2NU4lG9jeeu/Cg4I58UvD0KgKxRA/yHgBcUn4YQRE7rWhh1BCxMjidPJC+iKunqj
+o3M3NYB9Ergzd0A4wPpeMNLytqOx1qKVl4GbUu1pTP+A5FPbVFsDbVRfsbjvJL1v
+nxHDx2TCDyhihWZeGnuyt++uNckZM6i4J9szVb9o4XVIRFb7zdNIu0eJOqxp9YDG
+5ERQL1TEqkPFMTFYvZbF6nVsmnWxTfj3l/+WFvKXTej28xH5On2KOG4Ey+HTRRWq
+pdEdnV1j6CTmNhTih60bWfVEm/vXd3wfAXBioSAaosUaKPQhA+4u2cGA6rnZgtZb
+dsLLO7XSAPCjDuGtbkD326C00EauFddEwk01+dIL8hf2rGbVJLJP0RyZwG71fet0
+BLj5TXcJ17TPBzAJ8bgAVtkXFhYKK4bfjwEZGuW7gmP/vgt2Fl43N+bYdJeimUV5
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDUzCCAjugAwIBAgIBAjANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJOTzEd
+MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxHTAbBgNVBAMMFEJ1eXBhc3Mg
+Q2xhc3MgMyBDQSAxMB4XDTA1MDUwOTE0MTMwM1oXDTE1MDUwOTE0MTMwM1owSzEL
+MAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MR0wGwYD
+VQQDDBRCdXlwYXNzIENsYXNzIDMgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEP
+ADCCAQoCggEBAKSO13TZKWTeXx+HgJHqTjnmGcZEC4DVC69TB4sSveZn8AKxifZg
+isRbsELRwCGoy+Gb72RRtqfPFfV0gGgEkKBYouZ0plNTVUhjP5JW3SROjvi6K//z
+NIqeKNc0n6wv1g/xpC+9UrJJhW05NfBEMJNGJPO251P7vGGvqaMU+8IXF4Rs4HyI
++MkcVyzwPX6UvCWThOiaAJpFBUJXgPROztmuOfbIUxAMZTpHe2DC1vqRycZxbL2R
+hzyRhkmr8w+gbCZ2Xhysm3HljbybIR6c1jh+JIAVMYKWsUnTYjdbiAwKYjT+p0h+
+mbEwi5A3lRyoH6UsjfRVyNvdWQrCrXig9IsCAwEAAaNCMEAwDwYDVR0TAQH/BAUw
+AwEB/zAdBgNVHQ4EFgQUOBTmyPCppAP0Tj4io1vy1uCtQHQwDgYDVR0PAQH/BAQD
+AgEGMA0GCSqGSIb3DQEBBQUAA4IBAQABZ6OMySU9E2NdFm/soT4JXJEVKirZgCFP
+Bdy7pYmrEzMqnji3jG8CcmPHc3ceCQa6Oyh7pEfJYWsICCD8igWKH7y6xsL+z27s
+EzNxZy5p+qksP2bAEllNC1QCkoS72xLvg3BweMhT+t/Gxv/ciC8HwEmdMldg0/L2
+mSlf56oBzKwzqBwKu5HEA6BvtjT5htOzdlSY9EqBs1OdTUDs5XcTRa9bqh/YL0yC
+e/4qxFi7T/ye/QNlGioOw6UgFpRreaaiErS7GqQjel/wroQk5PMr+4okoyeYZdow
+dXb8GZHo2+ubPzK/QJcHJrrM85SFSnonk8+QQtS4Wxam58tAA915
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDZjCCAk6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBEMQswCQYDVQQGEwJVUzEW
+MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3QgR2xvYmFs
+IENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMTkwMzA0MDUwMDAwWjBEMQswCQYDVQQG
+EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3Qg
+R2xvYmFsIENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDvPE1A
+PRDfO1MA4Wf+lGAVPoWI8YkNkMgoI5kF6CsgncbzYEbYwbLVjDHZ3CB5JIG/NTL8
+Y2nbsSpr7iFY8gjpeMtvy/wWUsiRxP89c96xPqfCfWbB9X5SJBri1WeR0IIQ13hL
+TytCOb1kLUCgsBDTOEhGiKEMuzozKmKY+wCdE1l/bztyqu6mD4b5BWHqZ38MN5aL
+5mkWRxHCJ1kDs6ZgwiFAVvqgx306E+PsV8ez1q6diYD3Aecs9pYrEw15LNnA5IZ7
+S4wMcoKK+xfNAGw6EzywhIdLFnopsk/bHdQL82Y3vdj2V7teJHq4PIu5+pIaGoSe
+2HSPqht/XvT+RSIhAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE
+FHE4NvICMVNHK266ZUapEBVYIAUJMB8GA1UdIwQYMBaAFHE4NvICMVNHK266ZUap
+EBVYIAUJMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQUFAAOCAQEAA/e1K6td
+EPx7srJerJsOflN4WT5CBP51o62sgU7XAotexC3IUnbHLB/8gTKY0UvGkpMzNTEv
+/NgdRN3ggX+d6YvhZJFiCzkIjKx0nVnZellSlxG5FntvRdOW2TF9AjYPnDtuzywN
+A0ZF66D0f0hExghAzN4bcLUprbqLOzRldRtxIR0sFAqwlpW41uryZfspuk/qkZN0
+abby/+Ea0AzRdoXLiiW9l14sbxWZJue2Kf8i7MkCx1YAzUm5s2x7UwQa4qjJqhIF
+I8LO57sEAszAR6LkxCkvW0VXiVHuPOtSCP8HNR6fNWpHSlaY0VqFH4z1Ir+rzoPz
+4iIprn2DQKi6bA==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd
+MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg
+Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow
+TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw
+HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB
+BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr
+6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV
+L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91
+1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx
+MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ
+QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB
+arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr
+Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi
+FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS
+P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN
+9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP
+AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz
+uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h
+9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s
+A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t
+OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo
++fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7
+KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2
+DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us
+H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ
+I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7
+5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h
+3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz
+Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDDDCCAfSgAwIBAgIDAQAgMA0GCSqGSIb3DQEBBQUAMD4xCzAJBgNVBAYTAlBM
+MRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD
+QTAeFw0wMjA2MTExMDQ2MzlaFw0yNzA2MTExMDQ2MzlaMD4xCzAJBgNVBAYTAlBM
+MRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD
+QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM6xwS7TT3zNJc4YPk/E
+jG+AanPIW1H4m9LcuwBcsaD8dQPugfCI7iNS6eYVM42sLQnFdvkrOYCJ5JdLkKWo
+ePhzQ3ukYbDYWMzhbGZ+nPMJXlVjhNWo7/OxLjBos8Q82KxujZlakE403Daaj4GI
+ULdtlkIJ89eVgw1BS7Bqa/j8D35in2fE7SZfECYPCE/wpFcozo+47UX2bu4lXapu
+Ob7kky/ZR6By6/qmW6/KUz/iDsaWVhFu9+lmqSbYf5VT7QqFiLpPKaVCjF62/IUg
+AKpoC6EahQGcxEZjgoi2IrHu/qpGWX7PNSzVttpd90gzFFS269lvzs2I1qsb2pY7
+HVkCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEA
+uI3O7+cUus/usESSbLQ5PqKEbq24IXfS1HeCh+YgQYHu4vgRt2PRFze+GXYkHAQa
+TOs9qmdvLdTN/mUxcMUbpgIKumB7bVjCmkn+YzILa+M6wKyrO7Do0wlRjBCDxjTg
+xSvgGrZgFCdsMneMvLJymM/NzD+5yCRCFNZX/OYmQ6kd5YCQzgNUKD73P9P4Te1q
+CjqTE5s7FCMTY5w/0YcneeVMUeMBrYVdGjux1XMQpNPyvG5k9VpWkKjHDkx0Dy5x
+O/fIR/RpbxXyEV6DHpx8Uq79AtoSqFlnGNu8cN2bsWntgM6JQEhqDjXKKWYVIZQs
+6GAqm4VKQPNriiTsBhYscw==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES
+MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU
+V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz
+WhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO
+LUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm
+aWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
+AQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE
+AcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH
+K3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX
+RfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z
+rX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx
+3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
+HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq
+hkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC
+MErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls
+XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D
+lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn
+aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ
+YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx
+KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd
+BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl
+YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1
+OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy
+aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50
+ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G
+CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN
+8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/
+RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4
+hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5
+ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM
+EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj
+QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1
+A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy
+WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ
+1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30
+6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT
+91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml
+e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p
+TpPDpFQUWw==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UE
+BhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWdu
+IFNpbHZlciBDQSAtIEcyMB4XDTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0Nlow
+RzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMY
+U3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
+MIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644N0Mv
+Fz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7br
+YT7QbNHm+/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieF
+nbAVlDLaYQ1HTWBCrpJH6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH
+6ATK72oxh9TAtvmUcXtnZLi2kUpCe2UuMGoM9ZDulebyzYLs2aFK7PayS+VFheZt
+eJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5hqAaEuSh6XzjZG6k4sIN/
+c8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5FZGkECwJ
+MoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRH
+HTBsROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTf
+jNFusB3hB48IHpmccelM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb6
+5i/4z3GcRm25xBWNOHkDRUjvxF3XCO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOB
+rDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU
+F6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRBtjpbO8tFnb0c
+wpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0
+cDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIB
+AHPGgeAn0i0P4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShp
+WJHckRE1qTodvBqlYJ7YH39FkWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9
+xCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L3XWgwF15kIwb4FDm3jH+mHtwX6WQ
+2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx/uNncqCxv1yL5PqZ
+IseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFaDGi8
+aRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2X
+em1ZqSqPe97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQR
+dAtq/gsD/KNVV4n+SsuuWxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/
+OMpXEA29MC/HpeZBoNquBYeaoKRlbEwJDIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+
+hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ubDgEj8Z+7fNzcbBGXJbLy
+tGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE
+AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG
+EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM
+FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC
+REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp
+Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM
+VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+
+SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ
+4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L
+cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi
+eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV
+HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG
+A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3
+DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j
+vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP
+DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc
+maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D
+lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv
+KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIID+zCCAuOgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBtzE/MD0GA1UEAww2VMOc
+UktUUlVTVCBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx
+c8SxMQswCQYDVQQGDAJUUjEPMA0GA1UEBwwGQU5LQVJBMVYwVAYDVQQKDE0oYykg
+MjAwNSBUw5xSS1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8
+dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWeLjAeFw0wNTA1MTMxMDI3MTdaFw0xNTAz
+MjIxMDI3MTdaMIG3MT8wPQYDVQQDDDZUw5xSS1RSVVNUIEVsZWt0cm9uaWsgU2Vy
+dGlmaWthIEhpem1ldCBTYcSfbGF5xLFjxLFzxLExCzAJBgNVBAYMAlRSMQ8wDQYD
+VQQHDAZBTktBUkExVjBUBgNVBAoMTShjKSAyMDA1IFTDnFJLVFJVU1QgQmlsZ2kg
+xLBsZXRpxZ9pbSB2ZSBCaWxpxZ9pbSBHw7x2ZW5sacSfaSBIaXptZXRsZXJpIEEu
+xZ4uMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAylIF1mMD2Bxf3dJ7
+XfIMYGFbazt0K3gNfUW9InTojAPBxhEqPZW8qZSwu5GXyGl8hMW0kWxsE2qkVa2k
+heiVfrMArwDCBRj1cJ02i67L5BuBf5OI+2pVu32Fks66WJ/bMsW9Xe8iSi9BB35J
+YbOG7E6mQW6EvAPs9TscyB/C7qju6hJKjRTP8wrgUDn5CDX4EVmt5yLqS8oUBt5C
+urKZ8y1UiBAG6uEaPj1nH/vO+3yC6BFdSsG5FOpU2WabfIl9BJpiyelSPJ6c79L1
+JuTm5Rh8i27fbMx4W09ysstcP4wFjdFMjK2Sx+F4f2VsSQZQLJ4ywtdKxnWKWU51
+b0dewQIDAQABoxAwDjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAV
+9VX/N5aAWSGk/KEVTCD21F/aAyT8z5Aa9CEKmu46sWrv7/hg0Uw2ZkUd82YCdAR7
+kjCo3gp2D++Vbr3JN+YaDayJSFvMgzbC9UZcWYJWtNX+I7TYVBxEq8Sn5RTOPEFh
+fEPmzcSBCYsk+1Ql1haolgxnB2+zUEfjHCQo3SqYpGH+2+oSN7wBGjSFvW5P55Fy
+B0SFHljKVETd96y5y4khctuPwGkplyqjrhgjlxxBKot8KsF8kOipKMDTkcatKIdA
+aLX/7KfS0zgYnNN9aV3wxqUeJBujR/xpB2jn5Jq07Q+hh4cCzofSSE7hvP/L8XKS
+RGQDJereW26fyfJOrN3H
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEGjCCAwICEQDsoKeLbnVqAc/EfMwvlF7XMA0GCSqGSIb3DQEBBQUAMIHKMQsw
+CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl
+cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu
+LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT
+aWduIENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp
+dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD
+VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT
+aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ
+bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu
+IENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg
+LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK3LpRFpxlmr8Y+1
+GQ9Wzsy1HyDkniYlS+BzZYlZ3tCD5PUPtbut8XzoIfzk6AzufEUiGXaStBO3IFsJ
++mGuqPKljYXCKtbeZjbSmwL0qJJgfJxptI8kHtCGUvYynEFYHiK9zUVilQhu0Gbd
+U6LM8BDcVHOLBKFGMzNcF0C5nk3T875Vg+ixiY5afJqWIpA7iCXy0lOIAgwLePLm
+NxdLMEYH5IBtptiWLugs+BGzOA1mppvqySNb247i8xOOGlktqgLw7KSHZtzBP/XY
+ufTsgsbSPZUd5cBPhMnZo0QoBmrXRazwa2rvTl/4EYIeOGM0ZlDUPpNz+jDDZq3/
+ky2X7wMCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAj/ola09b5KROJ1WrIhVZPMq1
+CtRK26vdoV9TxaBXOcLORyu+OshWv8LZJxA6sQU8wHcxuzrTBXttmhwwjIDLk5Mq
+g6sFUYICABFna/OIYUdfA5PVWw3g8dShMjWFsjrbsIKr0csKvE+MW8VLADsfKoKm
+fjaF3H48ZwC15DtS4KjrXRX5xm3wrR0OhbepmnMUWluPQSjA1egtTaRezarZ7c7c
+2NU8Qh0XwRJdRTjDOPP8hS6DRkiy1yBfkjaP53kPmF6Z6PDQpLv1U70qzlmwr25/
+bLvSHgCwIe34QWKCudiyxLtGUPMxxY8BqHTr9Xgn2uf3ZkPznoM+IKrDNWCRzg==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEdDCCA1ygAwIBAgIQRL4Mi1AAJLQR0zYq/mUK/TANBgkqhkiG9w0BAQUFADCB
+lzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug
+Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho
+dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3Qt
+SGFyZHdhcmUwHhcNOTkwNzA5MTgxMDQyWhcNMTkwNzA5MTgxOTIyWjCBlzELMAkG
+A1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEe
+MBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8v
+d3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3QtSGFyZHdh
+cmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCx98M4P7Sof885glFn
+0G2f0v9Y8+efK+wNiVSZuTiZFvfgIXlIwrthdBKWHTxqctU8EGc6Oe0rE81m65UJ
+M6Rsl7HoxuzBdXmcRl6Nq9Bq/bkqVRcQVLMZ8Jr28bFdtqdt++BxF2uiiPsA3/4a
+MXcMmgF6sTLjKwEHOG7DpV4jvEWbe1DByTCP2+UretNb+zNAHqDVmBe8i4fDidNd
+oI6yqqr2jmmIBsX6iSHzCJ1pLgkzmykNRg+MzEk0sGlRvfkGzWitZky8PqxhvQqI
+DsjfPe58BEydCl5rkdbux+0ojatNh4lz0G6k0B4WixThdkQDf2Os5M1JnMWS9Ksy
+oUhbAgMBAAGjgbkwgbYwCwYDVR0PBAQDAgHGMA8GA1UdEwEB/wQFMAMBAf8wHQYD
+VR0OBBYEFKFyXyYbKJhDlV0HN9WFlp1L0sNFMEQGA1UdHwQ9MDswOaA3oDWGM2h0
+dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9VVE4tVVNFUkZpcnN0LUhhcmR3YXJlLmNy
+bDAxBgNVHSUEKjAoBggrBgEFBQcDAQYIKwYBBQUHAwUGCCsGAQUFBwMGBggrBgEF
+BQcDBzANBgkqhkiG9w0BAQUFAAOCAQEARxkP3nTGmZev/K0oXnWO6y1n7k57K9cM
+//bey1WiCuFMVGWTYGufEpytXoMs61quwOQt9ABjHbjAbPLPSbtNk28Gpgoiskli
+CE7/yMgUsogWXecB5BKV5UU0s4tpvc+0hY91UZ59Ojg6FEgSxvunOxqNDYJAB+gE
+CJChicsZUN/KHAG8HQQZexB2lzvukJDKxA4fFm517zP4029bHpbj4HR3dHuKom4t
+3XbWOTCC8KucUvIqx69JXn7HaOWCgchqJ/kniCrVWFCVH/A7HFe7fRQ5YiuayZSS
+KqMiDP+JJn1fIytH1xUdqWqeUQ0qUZ6B+dQ7XnASfxAynB67nfhmqA==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB
+gjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk
+MCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY
+UmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQxMTAxMTcx
+NDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3
+dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2Vy
+dmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB
+dXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS6
+38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP
+KZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q
+DxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4
+qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqsAxcZZPRa
+JSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi
+PvryxS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0P
+BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASs
+jVy16bYbMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0
+eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQEwDQYJKoZIhvcNAQEFBQAD
+ggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR
+vbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt
+qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa
+IR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy
+i6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQ
+O+7ETPTsJ3xCwnR8gooJybQDJbw=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL
+BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc
+BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00
+MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM
+aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG
+SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR
+/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu
+FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR
+U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c
+ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR
+FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k
+A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw
+eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl
+sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp
+VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q
+A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+
+ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
+BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD
+ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px
+KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI
+FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv
+oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg
+u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP
+0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf
+3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl
+8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+
+DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN
+PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/
+ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIF0DCCBLigAwIBAgIEOrZQizANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJC
+TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDElMCMGA1UECxMcUm9vdCBDZXJ0
+aWZpY2F0aW9uIEF1dGhvcml0eTEuMCwGA1UEAxMlUXVvVmFkaXMgUm9vdCBDZXJ0
+aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMTAzMTkxODMzMzNaFw0yMTAzMTcxODMz
+MzNaMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMSUw
+IwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYDVQQDEyVR
+dW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv2G1lVO6V/z68mcLOhrfEYBklbTRvM16z/Yp
+li4kVEAkOPcahdxYTMukJ0KX0J+DisPkBgNbAKVRHnAEdOLB1Dqr1607BxgFjv2D
+rOpm2RgbaIr1VxqYuvXtdj182d6UajtLF8HVj71lODqV0D1VNk7feVcxKh7YWWVJ
+WCCYfqtffp/p1k3sg3Spx2zY7ilKhSoGFPlU5tPaZQeLYzcS19Dsw3sgQUSj7cug
+F+FxZc4dZjH3dgEZyH0DWLaVSR2mEiboxgx24ONmy+pdpibu5cxfvWenAScOospU
+xbF6lR1xHkopigPcakXBpBlebzbNw6Kwt/5cOOJSvPhEQ+aQuwIDAQABo4ICUjCC
+Ak4wPQYIKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwczovL29jc3AucXVv
+dmFkaXNvZmZzaG9yZS5jb20wDwYDVR0TAQH/BAUwAwEB/zCCARoGA1UdIASCAREw
+ggENMIIBCQYJKwYBBAG+WAABMIH7MIHUBggrBgEFBQcCAjCBxxqBxFJlbGlhbmNl
+IG9uIHRoZSBRdW9WYWRpcyBSb290IENlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBh
+c3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFy
+ZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRpb24gcHJh
+Y3RpY2VzLCBhbmQgdGhlIFF1b1ZhZGlzIENlcnRpZmljYXRlIFBvbGljeS4wIgYI
+KwYBBQUHAgEWFmh0dHA6Ly93d3cucXVvdmFkaXMuYm0wHQYDVR0OBBYEFItLbe3T
+KbkGGew5Oanwl4Rqy+/fMIGuBgNVHSMEgaYwgaOAFItLbe3TKbkGGew5Oanwl4Rq
+y+/foYGEpIGBMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1p
+dGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYD
+VQQDEyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggQ6tlCL
+MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAitQUtf70mpKnGdSk
+fnIYj9lofFIk3WdvOXrEql494liwTXCYhGHoG+NpGA7O+0dQoE7/8CQfvbLO9Sf8
+7C9TqnN7Az10buYWnuulLsS/VidQK2K6vkscPFVcQR0kvoIgR13VRH56FmjffU1R
+cHhXHTMe/QKZnAzNCgVPx7uOpHX6Sm2xgI4JVrmcGmD+XcHXetwReNDWXcG31a0y
+mQM6isxUJTkxgXsTIlG6Rmyhu576BGxJJnSP0nPrzDCi5upZIof4l/UO/erMkqQW
+xFIY6iHOsfHmhIHluqmGKPJDWl0Snawe2ajlCmqnf6CHKc/yiU3U7MXi5nrQNiOK
+SnQ2+Q==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEAzCCAuugAwIBAgIQVID5oHPtPwBMyonY43HmSjANBgkqhkiG9w0BAQUFADB1
+MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1
+czEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG
+CSqGSIb3DQEJARYJcGtpQHNrLmVlMCIYDzIwMTAxMDMwMTAxMDMwWhgPMjAzMDEy
+MTcyMzU5NTlaMHUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0aWZpdHNl
+ZXJpbWlza2Vza3VzMSgwJgYDVQQDDB9FRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBS
+b290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwggEiMA0GCSqGSIb3DQEB
+AQUAA4IBDwAwggEKAoIBAQDIIMDs4MVLqwd4lfNE7vsLDP90jmG7sWLqI9iroWUy
+euuOF0+W2Ap7kaJjbMeMTC55v6kF/GlclY1i+blw7cNRfdCT5mzrMEvhvH2/UpvO
+bntl8jixwKIy72KyaOBhU8E2lf/slLo2rpwcpzIP5Xy0xm90/XsY6KxX7QYgSzIw
+WFv9zajmofxwvI6Sc9uXp3whrj3B9UiHbCe9nyV0gVWw93X2PaRka9ZP585ArQ/d
+MtO8ihJTmMmJ+xAdTX7Nfh9WDSFwhfYggx/2uh8Ej+p3iDXE/+pOoYtNP2MbRMNE
+1CV2yreN1x5KZmTNXMWcg+HCCIia7E6j8T4cLNlsHaFLAgMBAAGjgYowgYcwDwYD
+VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBLyWj7qVhy/
+zQas8fElyalL1BSZMEUGA1UdJQQ+MDwGCCsGAQUFBwMCBggrBgEFBQcDAQYIKwYB
+BQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCAYIKwYBBQUHAwkwDQYJKoZIhvcNAQEF
+BQADggEBAHv25MANqhlHt01Xo/6tu7Fq1Q+e2+RjxY6hUFaTlrg4wCQiZrxTFGGV
+v9DHKpY5P30osxBAIWrEr7BSdxjhlthWXePdNl4dp1BUoMUq5KqMlIpPnTX/dqQG
+E5Gion0ARD9V04I8GtVbvFZMIi5GQ4okQC3zErg7cBqklrkar4dBGmoYDQZPxz5u
+uSlNDUmJEYcyW+ZLBMjkXOZ0c5RdFpgTlf7727FE5TpwrDdr5rMzcijJs1eg9gIW
+iAYLtqZLICjU3j2LrTcFU3T+bsy8QxdxXvnFzBqpYe73dgzzcvRyrc9yAjYHR8/v
+GVCJYMzpJJUPwssd8m92kMfMdcGWxZ0=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFYzCCA0ugAwIBAgIBOzANBgkqhkiG9w0BAQsFADBTMQswCQYDVQQGEwJJTDEW
+MBQGA1UEChMNU3RhcnRDb20gTHRkLjEsMCoGA1UEAxMjU3RhcnRDb20gQ2VydGlm
+aWNhdGlvbiBBdXRob3JpdHkgRzIwHhcNMTAwMTAxMDEwMDAxWhcNMzkxMjMxMjM1
+OTAxWjBTMQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjEsMCoG
+A1UEAxMjU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgRzIwggIiMA0G
+CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2iTZbB7cgNr2Cu+EWIAOVeq8Oo1XJ
+JZlKxdBWQYeQTSFgpBSHO839sj60ZwNq7eEPS8CRhXBF4EKe3ikj1AENoBB5uNsD
+vfOpL9HG4A/LnooUCri99lZi8cVytjIl2bLzvWXFDSxu1ZJvGIsAQRSCb0AgJnoo
+D/Uefyf3lLE3PbfHkffiAez9lInhzG7TNtYKGXmu1zSCZf98Qru23QumNK9LYP5/
+Q0kGi4xDuFby2X8hQxfqp0iVAXV16iulQ5XqFYSdCI0mblWbq9zSOdIxHWDirMxW
+RST1HFSr7obdljKF+ExP6JV2tgXdNiNnvP8V4so75qbsO+wmETRIjfaAKxojAuuK
+HDp2KntWFhxyKrOq42ClAJ8Em+JvHhRYW6Vsi1g8w7pOOlz34ZYrPu8HvKTlXcxN
+nw3h3Kq74W4a7I/htkxNeXJdFzULHdfBR9qWJODQcqhaX2YtENwvKhOuJv4KHBnM
+0D4LnMgJLvlblnpHnOl68wVQdJVznjAJ85eCXuaPOQgeWeU1FEIT/wCc976qUM/i
+UUjXuG+v+E5+M5iSFGI6dWPPe/regjupuznixL0sAA7IF6wT700ljtizkC+p2il9
+Ha90OrInwMEePnWjFqmveiJdnxMaz6eg6+OGCtP95paV1yPIN93EfKo2rJgaErHg
+TuixO/XWb/Ew1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE
+AwIBBjAdBgNVHQ4EFgQUS8W0QGutHLOlHGVuRjaJhwUMDrYwDQYJKoZIhvcNAQEL
+BQADggIBAHNXPyzVlTJ+N9uWkusZXn5T50HsEbZH77Xe7XRcxfGOSeD8bpkTzZ+K
+2s06Ctg6Wgk/XzTQLwPSZh0avZyQN8gMjgdalEVGKua+etqhqaRpEpKwfTbURIfX
+UfEpY9Z1zRbkJ4kd+MIySP3bmdCPX1R0zKxnNBFi2QwKN4fRoxdIjtIXHfbX/dtl
+6/2o1PXWT6RbdejF0mCy2wl+JYt7ulKSnj7oxXehPOBKc2thz4bcQ///If4jXSRK
+9dNtD2IEBVeC2m6kMyV5Sy5UGYvMLD0w6dEG/+gyRr61M3Z3qAFdlsHB1b6uJcDJ
+HgoJIIihDsnzb02CVAAgp9KP5DlUFy6NHrgbuxu9mk47EDTcnIhT76IxW1hPkWLI
+wpqazRVdOKnWvvgTtZ8SafJQYqz7Fzf07rh1Z2AQ+4NQ+US1dZxAF7L+/XldblhY
+XzD8AK6vM8EOTmy6p6ahfzLbOOCxchcKK5HsamMm7YnUeMx0HgX4a/6ManY5Ka5l
+IxKVCCIcl85bBu4M4ru8H0ST9tg4RQUh7eStqxK2A6RCLi3ECToDZ2mEmuFZkIoo
+hdVddLHRDiBYmxOlsGOm7XtH/UVVMKTumtTm4ofvmMkyghEpIrwACjFeLQ/Ajulr
+so8uBtjRkcfGEvRM/TAXw8HaOFvjqermobp573PYtlNXLfbQ4ddI
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEPTCCAyWgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBvzE/MD0GA1UEAww2VMOc
+UktUUlVTVCBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx
+c8SxMQswCQYDVQQGEwJUUjEPMA0GA1UEBwwGQW5rYXJhMV4wXAYDVQQKDFVUw5xS
+S1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kg
+SGl6bWV0bGVyaSBBLsWeLiAoYykgQXJhbMSxayAyMDA3MB4XDTA3MTIyNTE4Mzcx
+OVoXDTE3MTIyMjE4MzcxOVowgb8xPzA9BgNVBAMMNlTDnFJLVFJVU1QgRWxla3Ry
+b25payBTZXJ0aWZpa2EgSGl6bWV0IFNhxJ9sYXnEsWPEsXPEsTELMAkGA1UEBhMC
+VFIxDzANBgNVBAcMBkFua2FyYTFeMFwGA1UECgxVVMOcUktUUlVTVCBCaWxnaSDE
+sGxldGnFn2ltIHZlIEJpbGnFn2ltIEfDvHZlbmxpxJ9pIEhpem1ldGxlcmkgQS7F
+ni4gKGMpIEFyYWzEsWsgMjAwNzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
+ggEBAKu3PgqMyKVYFeaK7yc9SrToJdPNM8Ig3BnuiD9NYvDdE3ePYakqtdTyuTFY
+KTsvP2qcb3N2Je40IIDu6rfwxArNK4aUyeNgsURSsloptJGXg9i3phQvKUmi8wUG
++7RP2qFsmmaf8EMJyupyj+sA1zU511YXRxcw9L6/P8JorzZAwan0qafoEGsIiveG
+HtyaKhUG9qPw9ODHFNRRf8+0222vR5YXm3dx2KdxnSQM9pQ/hTEST7ruToK4uT6P
+IzdezKKqdfcYbwnTrqdUKDT74eA7YH2gvnmJhsifLfkKS8RQouf9eRbHegsYz85M
+733WB2+Y8a+xwXrXgTW4qhe04MsCAwEAAaNCMEAwHQYDVR0OBBYEFCnFkKslrxHk
+Yb+j/4hhkeYO/pyBMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0G
+CSqGSIb3DQEBBQUAA4IBAQAQDdr4Ouwo0RSVgrESLFF6QSU2TJ/sPx+EnWVUXKgW
+AkD6bho3hO9ynYYKVZ1WKKxmLNA6VpM0ByWtCLCPyA8JWcqdmBzlVPi5RX9ql2+I
+aE1KBiY3iAIOtsbWcpnOa3faYjGkVh+uX4132l32iPwa2Z61gfAyuOOI0JzzaqC5
+mxRZNTZPz/OOXl0XrRWV2N2y1RVuAE6zS89mlOTgzbUF2mNXi+WzqtvALhyQRNsa
+XRik7r4EW5nVcV9VZWRi1aKbBFmGyGJ353yCRWo9F7/snXUMrqNvWtMvmDb08PUZ
+qxFdyKbjKlhqQgnDvZImZjINXQhVdP+MmNAKpoRq0Tl9
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEFTCCAv2gAwIBAgIBATANBgkqhkiG9w0BAQUFADBkMQswCQYDVQQGEwJTRTEU
+MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3
+b3JrMSAwHgYDVQQDExdBZGRUcnVzdCBQdWJsaWMgQ0EgUm9vdDAeFw0wMDA1MzAx
+MDQxNTBaFw0yMDA1MzAxMDQxNTBaMGQxCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtB
+ZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5ldHdvcmsxIDAeBgNV
+BAMTF0FkZFRydXN0IFB1YmxpYyBDQSBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEA6Rowj4OIFMEg2Dybjxt+A3S72mnTRqX4jsIMEZBRpS9mVEBV
+6tsfSlbunyNu9DnLoblv8n75XYcmYZ4c+OLspoH4IcUkzBEMP9smcnrHAZcHF/nX
+GCwwfQ56HmIexkvA/X1id9NEHif2P0tEs7c42TkfYNVRknMDtABp4/MUTu7R3AnP
+dzRGULD4EfL+OHn3Bzn+UZKXC1sIXzSGAa2Il+tmzV7R/9x98oTaunet3IAIx6eH
+1lWfl2royBFkuucZKT8Rs3iQhCBSWxHveNCD9tVIkNAwHM+A+WD+eeSI8t0A65RF
+62WUaUC6wNW0uLp9BBGo6zEFlpROWCGOn9Bg/QIDAQABo4HRMIHOMB0GA1UdDgQW
+BBSBPjfYkrAfd59ctKtzquf2NGAv+jALBgNVHQ8EBAMCAQYwDwYDVR0TAQH/BAUw
+AwEB/zCBjgYDVR0jBIGGMIGDgBSBPjfYkrAfd59ctKtzquf2NGAv+qFopGYwZDEL
+MAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQLExRBZGRU
+cnVzdCBUVFAgTmV0d29yazEgMB4GA1UEAxMXQWRkVHJ1c3QgUHVibGljIENBIFJv
+b3SCAQEwDQYJKoZIhvcNAQEFBQADggEBAAP3FUr4JNojVhaTdt02KLmuG7jD8WS6
+IBh4lSknVwW8fCr0uVFV2ocC3g8WFzH4qnkuCRO7r7IgGRLlk/lL+YPoRNWyQSW/
+iHVv/xD8SlTQX/D67zZzfRs2RcYhbbQVuE7PnFylPVoAjgbjPGsye/Kf8Lb93/Ao
+GEjwxrzQvzSAlsJKsW2Ox5BF3i9nrEUEo3rcVZLJR2bYGozH7ZxOmuASu7VqTITh
+4SINhwBk/ox9Yjllpu9CtoAlEmEBqCQTcAARJl/6NVDFSMwGR+gn2HCNX2TmoUQm
+XiLsks3/QppEIW1cxeMiHV9HEufOX1362KqxMy3ZdvJOOjMMK7MtkAY=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1Ix
+RDBCBgNVBAoTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1
+dGlvbnMgQ2VydC4gQXV0aG9yaXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1p
+YyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIFJvb3RDQSAyMDExMB4XDTExMTIw
+NjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYTAkdSMUQwQgYDVQQK
+EztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIENl
+cnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl
+c2VhcmNoIEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBAKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPz
+dYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJ
+fel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa71HFK9+WXesyHgLacEns
+bgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u8yBRQlqD
+75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSP
+FEDH3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNV
+HRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp
+5dgTBCPuQSUwRwYDVR0eBEAwPqA8MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQu
+b3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQub3JnMA0GCSqGSIb3DQEBBQUA
+A4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVtXdMiKahsog2p
+6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8
+TqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7
+dIsXRSZMFpGD/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8Acys
+Nnq/onN694/BtZqhFLKPM58N7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXI
+l7WdmplNsDz4SgCbZN2fOUvRJ9e4
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl
+MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe
+U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX
+DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy
+dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj
+YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV
+OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr
+zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM
+VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ
+hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO
+ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw
+awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs
+OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3
+DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF
+coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc
+okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8
+t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy
+1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/
+SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEPzCCAyegAwIBAgIBATANBgkqhkiG9w0BAQUFADB+MQswCQYDVQQGEwJHQjEb
+MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow
+GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEkMCIGA1UEAwwbU2VjdXJlIENlcnRp
+ZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVow
+fjELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
+A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxJDAiBgNV
+BAMMG1NlY3VyZSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBAMBxM4KK0HDrc4eCQNUd5MvJDkKQ+d40uaG6EfQlhfPM
+cm3ye5drswfxdySRXyWP9nQ95IDC+DwN879A6vfIUtFyb+/Iq0G4bi4XKpVpDM3S
+HpR7LZQdqnXXs5jLrLxkU0C8j6ysNstcrbvd4JQX7NFc0L/vpZXJkMWwrPsbQ996
+CF23uPJAGysnnlDOXmWCiIxe004MeuoIkbY2qitC++rCoznl2yY4rYsK7hljxxwk
+3wN42ubqwUcaCwtGCd0C/N7Lh1/XMGNooa7cMqG6vv5Eq2i2pRcV/b3Vp6ea5EQz
+6YiO/O1R65NxTq0B50SOqy3LqP4BSUjwwN3HaNiS/j0CAwEAAaOBxzCBxDAdBgNV
+HQ4EFgQUPNiTiMLAggnMAZkGkyDpnnAJY08wDgYDVR0PAQH/BAQDAgEGMA8GA1Ud
+EwEB/wQFMAMBAf8wgYEGA1UdHwR6MHgwO6A5oDeGNWh0dHA6Ly9jcmwuY29tb2Rv
+Y2EuY29tL1NlY3VyZUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDmgN6A1hjNodHRw
+Oi8vY3JsLmNvbW9kby5uZXQvU2VjdXJlQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmww
+DQYJKoZIhvcNAQEFBQADggEBAIcBbSMdflsXfcFhMs+P5/OKlFlm4J4oqF7Tt/Q0
+5qo5spcWxYJvMqTpjOev/e/C6LlLqqP05tqNZSH7uoDrJiiFGv45jN5bBAS0VPmj
+Z55B+glSzAVIqMk/IQQezkhr/IXownuvf7fM+F86/TXGDe+X3EyrEeFryzHRbPtI
+gKvcnDe4IRRLDXE97IMzbtFuMhbsmMcWi1mmNKsFVy2T96oTy9IT4rcuO81rUBcJ
+aD61JlfutuC23bkpgHl9j6PwpCikFcSF9CfUa7/lXORlAnZUtOM3ZiTTGWHIUhDl
+izeauan5Hb/qmZJhlv8BzaFfDbxxvA6sCx1HRR3B7Hzs/Sk=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV
+BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu
+MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy
+MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx
+EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw
+ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe
+NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH
+PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I
+x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe
+QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR
+yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO
+QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912
+H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ
+QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD
+i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs
+nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1
+rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud
+DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI
+hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM
+tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf
+GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb
+lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka
++elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal
+TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i
+nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3
+gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr
+G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os
+zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x
+L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL
+MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
+BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT
+IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw
+MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy
+ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N
+T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv
+biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR
+FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J
+cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW
+BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/
+BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm
+fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv
+GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD
+VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0
+ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G
+CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y
+OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx
+FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp
+Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o
+dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP
+kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc
+cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U
+fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7
+N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC
+xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1
++rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G
+A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM
+Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG
+SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h
+mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk
+ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775
+tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c
+2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t
+HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEAjCCAuqgAwIBAgIFORFFEJQwDQYJKoZIhvcNAQEFBQAwgYUxCzAJBgNVBAYT
+AkZSMQ8wDQYDVQQIEwZGcmFuY2UxDjAMBgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQ
+TS9TR0ROMQ4wDAYDVQQLEwVEQ1NTSTEOMAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG
+9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2LmZyMB4XDTAyMTIxMzE0MjkyM1oXDTIw
+MTAxNzE0MjkyMlowgYUxCzAJBgNVBAYTAkZSMQ8wDQYDVQQIEwZGcmFuY2UxDjAM
+BgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQTS9TR0ROMQ4wDAYDVQQLEwVEQ1NTSTEO
+MAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2
+LmZyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsh/R0GLFMzvABIaI
+s9z4iPf930Pfeo2aSVz2TqrMHLmh6yeJ8kbpO0px1R2OLc/mratjUMdUC24SyZA2
+xtgv2pGqaMVy/hcKshd+ebUyiHDKcMCWSo7kVc0dJ5S/znIq7Fz5cyD+vfcuiWe4
+u0dzEvfRNWk68gq5rv9GQkaiv6GFGvm/5P9JhfejcIYyHF2fYPepraX/z9E0+X1b
+F8bc1g4oa8Ld8fUzaJ1O/Id8NhLWo4DoQw1VYZTqZDdH6nfK0LJYBcNdfrGoRpAx
+Vs5wKpayMLh35nnAvSk7/ZR3TL0gzUEl4C7HG7vupARB0l2tEmqKm0f7yd1GQOGd
+PDPQtQIDAQABo3cwdTAPBgNVHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBRjAVBgNV
+HSAEDjAMMAoGCCqBegF5AQEBMB0GA1UdDgQWBBSjBS8YYFDCiQrdKyFP/45OqDAx
+NjAfBgNVHSMEGDAWgBSjBS8YYFDCiQrdKyFP/45OqDAxNjANBgkqhkiG9w0BAQUF
+AAOCAQEABdwm2Pp3FURo/C9mOnTgXeQp/wYHE4RKq89toB9RlPhJy3Q2FLwV3duJ
+L92PoF189RLrn544pEfMs5bZvpwlqwN+Mw+VgQ39FuCIvjfwbF3QMZsyK10XZZOY
+YLxuj7GoPB7ZHPOpJkL5ZB3C55L29B5aqhlSXa/oovdgoPaN8In1buAKBQGVyYsg
+Crpa/JosPL3Dt8ldeCUFP1YUmwza+zpI/pdpXsoQhvdOlgQITeywvl3cO45Pwf2a
+NjSaTFR+FwNIlQgRHAdvhQh+XU3Endv7rs6y0bO4g2wdsrN58dhwmX7wEwLOXt1R
+0982gaEbeC9xs/FZTEYYKKuF0mBWWg==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFFzCCA/+gAwIBAgIBETANBgkqhkiG9w0BAQUFADCCASsxCzAJBgNVBAYTAlRS
+MRgwFgYDVQQHDA9HZWJ6ZSAtIEtvY2FlbGkxRzBFBgNVBAoMPlTDvHJraXllIEJp
+bGltc2VsIHZlIFRla25vbG9qaWsgQXJhxZ90xLFybWEgS3VydW11IC0gVMOcQsSw
+VEFLMUgwRgYDVQQLDD9VbHVzYWwgRWxla3Ryb25payB2ZSBLcmlwdG9sb2ppIEFy
+YcWfdMSxcm1hIEVuc3RpdMO8c8O8IC0gVUVLQUUxIzAhBgNVBAsMGkthbXUgU2Vy
+dGlmaWthc3lvbiBNZXJrZXppMUowSAYDVQQDDEFUw5xCxLBUQUsgVUVLQUUgS8O2
+ayBTZXJ0aWZpa2EgSGl6bWV0IFNhxJ9sYXnEsWPEsXPEsSAtIFPDvHLDvG0gMzAe
+Fw0wNzA4MjQxMTM3MDdaFw0xNzA4MjExMTM3MDdaMIIBKzELMAkGA1UEBhMCVFIx
+GDAWBgNVBAcMD0dlYnplIC0gS29jYWVsaTFHMEUGA1UECgw+VMO8cmtpeWUgQmls
+aW1zZWwgdmUgVGVrbm9sb2ppayBBcmHFn3TEsXJtYSBLdXJ1bXUgLSBUw5xCxLBU
+QUsxSDBGBgNVBAsMP1VsdXNhbCBFbGVrdHJvbmlrIHZlIEtyaXB0b2xvamkgQXJh
+xZ90xLFybWEgRW5zdGl0w7xzw7wgLSBVRUtBRTEjMCEGA1UECwwaS2FtdSBTZXJ0
+aWZpa2FzeW9uIE1lcmtlemkxSjBIBgNVBAMMQVTDnELEsFRBSyBVRUtBRSBLw7Zr
+IFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxIC0gU8O8csO8bSAzMIIB
+IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAim1L/xCIOsP2fpTo6iBkcK4h
+gb46ezzb8R1Sf1n68yJMlaCQvEhOEav7t7WNeoMojCZG2E6VQIdhn8WebYGHV2yK
+O7Rm6sxA/OOqbLLLAdsyv9Lrhc+hDVXDWzhXcLh1xnnRFDDtG1hba+818qEhTsXO
+fJlfbLm4IpNQp81McGq+agV/E5wrHur+R84EpW+sky58K5+eeROR6Oqeyjh1jmKw
+lZMq5d/pXpduIF9fhHpEORlAHLpVK/swsoHvhOPc7Jg4OQOFCKlUAwUp8MmPi+oL
+hmUZEdPpCSPeaJMDyTYcIW7OjGbxmTDY17PDHfiBLqi9ggtm/oLL4eAagsNAgQID
+AQABo0IwQDAdBgNVHQ4EFgQUvYiHyY/2pAoLquvF/pEjnatKijIwDgYDVR0PAQH/
+BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAB18+kmP
+NOm3JpIWmgV050vQbTlswyb2zrgxvMTfvCr4N5EY3ATIZJkrGG2AA1nJrvhY0D7t
+wyOfaTyGOBye79oneNGEN3GKPEs5z35FBtYt2IpNeBLWrcLTy9LQQfMmNkqblWwM
+7uXRQydmwYj3erMgbOqwaSvHIOgMA8RBBZniP+Rr+KCGgceExh/VS4ESshYhLBOh
+gLJeDEoTniDYYkCrkOpkSi+sDQESeUWoL4cZaMjihccwsnX5OD+ywJO0a+IDRM5n
+oN+J1q2MdqMTw5RhK2vZbMEHCiIHhWyFJEapvj+LeISCfiQMnf2BN+MlqO02TpUs
+yZyQ2uypQjyttgI=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEDzCCAvegAwIBAgIBATANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQGEwJTSzET
+MBEGA1UEBxMKQnJhdGlzbGF2YTETMBEGA1UEChMKRGlzaWcgYS5zLjERMA8GA1UE
+AxMIQ0EgRGlzaWcwHhcNMDYwMzIyMDEzOTM0WhcNMTYwMzIyMDEzOTM0WjBKMQsw
+CQYDVQQGEwJTSzETMBEGA1UEBxMKQnJhdGlzbGF2YTETMBEGA1UEChMKRGlzaWcg
+YS5zLjERMA8GA1UEAxMIQ0EgRGlzaWcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
+ggEKAoIBAQCS9jHBfYj9mQGp2HvycXXxMcbzdWb6UShGhJd4NLxs/LxFWYgmGErE
+Nx+hSkS943EE9UQX4j/8SFhvXJ56CbpRNyIjZkMhsDxkovhqFQ4/61HhVKndBpnX
+mjxUizkDPw/Fzsbrg3ICqB9x8y34dQjbYkzo+s7552oftms1grrijxaSfQUMbEYD
+XcDtab86wYqg6I7ZuUUohwjstMoVvoLdtUSLLa2GDGhibYVW8qwUYzrG0ZmsNHhW
+S8+2rT+MitcE5eN4TPWGqvWP+j1scaMtymfraHtuM6kMgiioTGohQBUgDCZbg8Kp
+FhXAJIJdKxatymP2dACw30PEEGBWZ2NFAgMBAAGjgf8wgfwwDwYDVR0TAQH/BAUw
+AwEB/zAdBgNVHQ4EFgQUjbJJaJ1yCCW5wCf1UJNWSEZx+Y8wDgYDVR0PAQH/BAQD
+AgEGMDYGA1UdEQQvMC2BE2Nhb3BlcmF0b3JAZGlzaWcuc2uGFmh0dHA6Ly93d3cu
+ZGlzaWcuc2svY2EwZgYDVR0fBF8wXTAtoCugKYYnaHR0cDovL3d3dy5kaXNpZy5z
+ay9jYS9jcmwvY2FfZGlzaWcuY3JsMCygKqAohiZodHRwOi8vY2EuZGlzaWcuc2sv
+Y2EvY3JsL2NhX2Rpc2lnLmNybDAaBgNVHSAEEzARMA8GDSuBHpGT5goAAAABAQEw
+DQYJKoZIhvcNAQEFBQADggEBAF00dGFMrzvY/59tWDYcPQuBDRIrRhCA/ec8J9B6
+yKm2fnQwM6M6int0wHl5QpNt/7EpFIKrIYwvF/k/Ji/1WcbvgAa3mkkp7M5+cTxq
+EEHA9tOasnxakZzArFvITV734VP/Q3f8nktnbNfzg9Gg4H8l37iYC5oyOGwwoPP/
+CBUz91BKez6jPiCp3C9WgArtQVCwyfTssuMmRAAOb54GvCKWU3BlxFAKRmukLyeB
+EicTXxChds6KezfqwzlhA5WYOudsiCUI/HloDYd9Yvi0X/vF2Ey9WLw/Q1vUHgFN
+PGO+I++MzVpQuGhU+QqZMxEA4Z7CRneC9VkGjCFMhwnN5ag=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT
+MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i
+YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG
+EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg
+R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9
+9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq
+fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv
+iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU
+1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+
+bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW
+MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA
+ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l
+uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn
+Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS
+tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF
+PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un
+hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV
+5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G
+A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp
+Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4
+MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG
+A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8
+RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT
+gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm
+KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd
+QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ
+XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw
+DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o
+LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU
+RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp
+jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK
+6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX
+mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs
+Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH
+WD9f
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFtTCCA52gAwIBAgIIYY3HhjsBggUwDQYJKoZIhvcNAQEFBQAwRDEWMBQGA1UE
+AwwNQUNFRElDT00gUm9vdDEMMAoGA1UECwwDUEtJMQ8wDQYDVQQKDAZFRElDT00x
+CzAJBgNVBAYTAkVTMB4XDTA4MDQxODE2MjQyMloXDTI4MDQxMzE2MjQyMlowRDEW
+MBQGA1UEAwwNQUNFRElDT00gUm9vdDEMMAoGA1UECwwDUEtJMQ8wDQYDVQQKDAZF
+RElDT00xCzAJBgNVBAYTAkVTMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC
+AgEA/5KV4WgGdrQsyFhIyv2AVClVYyT/kGWbEHV7w2rbYgIB8hiGtXxaOLHkWLn7
+09gtn70yN78sFW2+tfQh0hOR2QetAQXW8713zl9CgQr5auODAKgrLlUTY4HKRxx7
+XBZXehuDYAQ6PmXDzQHe3qTWDLqO3tkE7hdWIpuPY/1NFgu3e3eM+SW10W2ZEi5P
+Grjm6gSSrj0RuVFCPYewMYWveVqc/udOXpJPQ/yrOq2lEiZmueIM15jO1FillUAK
+t0SdE3QrwqXrIhWYENiLxQSfHY9g5QYbm8+5eaA9oiM/Qj9r+hwDezCNzmzAv+Yb
+X79nuIQZ1RXve8uQNjFiybwCq0Zfm/4aaJQ0PZCOrfbkHQl/Sog4P75n/TSW9R28
+MHTLOO7VbKvU/PQAtwBbhTIWdjPp2KOZnQUAqhbm84F9b32qhm2tFXTTxKJxqvQU
+fecyuB+81fFOvW8XAjnXDpVCOscAPukmYxHqC9FK/xidstd7LzrZlvvoHpKuE1XI
+2Sf23EgbsCTBheN3nZqk8wwRHQ3ItBTutYJXCb8gWH8vIiPYcMt5bMlL8qkqyPyH
+K9caUPgn6C9D4zq92Fdx/c6mUlv53U3t5fZvie27k5x2IXXwkkwp9y+cAS7+UEae
+ZAwUswdbxcJzbPEHXEUkFDWug/FqTYl6+rPYLWbwNof1K1MCAwEAAaOBqjCBpzAP
+BgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKaz4SsrSbbXc6GqlPUB53NlTKxQ
+MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUprPhKytJttdzoaqU9QHnc2VMrFAw
+RAYDVR0gBD0wOzA5BgRVHSAAMDEwLwYIKwYBBQUHAgEWI2h0dHA6Ly9hY2VkaWNv
+bS5lZGljb21ncm91cC5jb20vZG9jMA0GCSqGSIb3DQEBBQUAA4ICAQDOLAtSUWIm
+fQwng4/F9tqgaHtPkl7qpHMyEVNEskTLnewPeUKzEKbHDZ3Ltvo/Onzqv4hTGzz3
+gvoFNTPhNahXwOf9jU8/kzJPeGYDdwdY6ZXIfj7QeQCM8htRM5u8lOk6e25SLTKe
+I6RF+7YuE7CLGLHdztUdp0J/Vb77W7tH1PwkzQSulgUV1qzOMPPKC8W64iLgpq0i
+5ALudBF/TP94HTXa5gI06xgSYXcGCRZj6hitoocf8seACQl1ThCojz2GuHURwCRi
+ipZ7SkXp7FnFvmuD5uHorLUwHv4FB4D54SMNUI8FmP8sX+g7tq3PgbUhh8oIKiMn
+MCArz+2UW6yyetLHKKGKC5tNSixthT8Jcjxn4tncB7rrZXtaAWPWkFtPF2Y9fwsZ
+o5NjEFIqnxQWWOLcpfShFosOkYuByptZ+thrkQdlVV9SH686+5DdaaVbnG0OLLb6
+zqylfDJKZ0DcMDQj3dcEI2bw/FWAp/tmGYI1Z2JwOV5vx+qQQEQIHriy1tvuWacN
+GHk0vFQYXlPKNFHtRQrmjseCNj6nOGOpMCwXEGCSn1WHElkQwg9naRHMTh5+Spqt
+r0CodaxWkHS4oJyleW/c6RrIaQXpuvoDs3zk4E7Czp3otkYNbn5XOmeUwssfnHdK
+Z05phkOTOPu220+DkdRgfks+KzgHVZhepA==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIHTzCCBTegAwIBAgIJAKPaQn6ksa7aMA0GCSqGSIb3DQEBBQUAMIGuMQswCQYD
+VQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0
+IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3
+MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xKTAnBgNVBAMTIENoYW1iZXJz
+IG9mIENvbW1lcmNlIFJvb3QgLSAyMDA4MB4XDTA4MDgwMTEyMjk1MFoXDTM4MDcz
+MTEyMjk1MFowga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNlZSBj
+dXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29tL2FkZHJlc3MpMRIw
+EAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVyZmlybWEgUy5BLjEp
+MCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDgwggIiMA0G
+CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCvAMtwNyuAWko6bHiUfaN/Gh/2NdW9
+28sNRHI+JrKQUrpjOyhYb6WzbZSm891kDFX29ufyIiKAXuFixrYp4YFs8r/lfTJq
+VKAyGVn+H4vXPWCGhSRv4xGzdz4gljUha7MI2XAuZPeEklPWDrCQiorjh40G072Q
+DuKZoRuGDtqaCrsLYVAGUvGef3bsyw/QHg3PmTA9HMRFEFis1tPo1+XqxQEHd9ZR
+5gN/ikilTWh1uem8nk4ZcfUyS5xtYBkL+8ydddy/Js2Pk3g5eXNeJQ7KXOt3EgfL
+ZEFHcpOrUMPrCXZkNNI5t3YRCQ12RcSprj1qr7V9ZS+UWBDsXHyvfuK2GNnQm05a
+Sd+pZgvMPMZ4fKecHePOjlO+Bd5gD2vlGts/4+EhySnB8esHnFIbAURRPHsl18Tl
+UlRdJQfKFiC4reRB7noI/plvg6aRArBsNlVq5331lubKgdaX8ZSD6e2wsWsSaR6s
++12pxZjptFtYer49okQ6Y1nUCyXeG0+95QGezdIp1Z8XGQpvvwyQ0wlf2eOKNcx5
+Wk0ZN5K3xMGtr/R5JJqyAQuxr1yW84Ay+1w9mPGgP0revq+ULtlVmhduYJ1jbLhj
+ya6BXBg14JC7vjxPNyK5fuvPnnchpj04gftI2jE9K+OJ9dC1vX7gUMQSibMjmhAx
+hduub+84Mxh2EQIDAQABo4IBbDCCAWgwEgYDVR0TAQH/BAgwBgEB/wIBDDAdBgNV
+HQ4EFgQU+SSsD7K1+HnA+mCIG8TZTQKeFxkwgeMGA1UdIwSB2zCB2IAU+SSsD7K1
++HnA+mCIG8TZTQKeFxmhgbSkgbEwga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpN
+YWRyaWQgKHNlZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29t
+L2FkZHJlc3MpMRIwEAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVy
+ZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAt
+IDIwMDiCCQCj2kJ+pLGu2jAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRV
+HSAAMCowKAYIKwYBBQUHAgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20w
+DQYJKoZIhvcNAQEFBQADggIBAJASryI1wqM58C7e6bXpeHxIvj99RZJe6dqxGfwW
+PJ+0W2aeaufDuV2I6A+tzyMP3iU6XsxPpcG1Lawk0lgH3qLPaYRgM+gQDROpI9CF
+5Y57pp49chNyM/WqfcZjHwj0/gF/JM8rLFQJ3uIrbZLGOU8W6jx+ekbURWpGqOt1
+glanq6B8aBMz9p0w8G8nOSQjKpD9kCk18pPfNKXG9/jvjA9iSnyu0/VU+I22mlaH
+FoI6M6taIgj3grrqLuBHmrS1RaMFO9ncLkVAO+rcf+g769HsJtg1pDDFOqxXnrN2
+pSB7+R5KBWIBpih1YJeSDW4+TTdDDZIVnBgizVGZoCkaPF+KMjNbMMeJL0eYD6MD
+xvbxrN8y8NmBGuScvfaAFPDRLLmF9dijscilIeUcE5fuDr3fKanvNFNb0+RqE4QG
+tjICxFKuItLcsiFCGtpA8CnJ7AoMXOLQusxI0zcKzBIKinmwPQN/aUv0NCB9szTq
+jktk9T79syNnFQ0EuPAtwQlRPLJsFfClI9eDdOTlLsn+mCdCxqvGnrDQWzilm1De
+fhiYtUU79nm06PcaewaD+9CL2rvHvRirCG88gGtAPxkZumWK5r7VXNM21+9AUiRg
+OGcEMeyP84LG3rlV8zsxkVrctQgVrXYlCg17LofiDKYGvCYQbTed7N14jHyAxfDZ
+d0jQ
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
+BAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln
+biBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF
+MQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZT
+d2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
+CgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8
+76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+
+bbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c
+6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE
+emA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd
+MmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt
+MDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02y
+MszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y
+FGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi
+aG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM
+gI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCB
+qTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUWyV7
+lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn
+8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov
+L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe6
+45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO
+UYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5
+O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC
+bwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv
+GPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a
+77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC
+hdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3
+92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp
+Ld6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w
+ZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt
+Qc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv
+b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG
+EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl
+cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi
+MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c
+JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP
+mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+
+wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4
+VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/
+AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB
+AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW
+BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun
+pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC
+dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf
+fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm
+NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx
+H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe
++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB
+yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL
+ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp
+U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW
+ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0
+aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCByjEL
+MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW
+ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2ln
+biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp
+U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y
+aXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1
+nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex
+t0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz
+SdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG
+BO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+
+rCpSx4/VBEnkjWNHiDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/
+NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E
+BAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAH
+BgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy
+aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKv
+MzEzMA0GCSqGSIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE
+p6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y
+5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK
+WE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiCKm0oHw0LxOXnGiYZ
+4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N
+hnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg
+RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV
+UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu
+Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG
+SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y
+ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If
+xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV
+ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO
+DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ
+jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/
+CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi
+EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM
+fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY
+uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK
+chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t
+9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
+hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD
+ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2
+SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd
++SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc
+fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa
+sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N
+cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N
+0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie
+4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI
+r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1
+/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm
+gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD
+QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT
+MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
+b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB
+CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97
+nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt
+43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P
+T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4
+gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO
+BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR
+TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw
+DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr
+hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg
+06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF
+PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls
+YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk
+CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEPDCCAySgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBvjE/MD0GA1UEAww2VMOc
+UktUUlVTVCBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx
+c8SxMQswCQYDVQQGEwJUUjEPMA0GA1UEBwwGQW5rYXJhMV0wWwYDVQQKDFRUw5xS
+S1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kg
+SGl6bWV0bGVyaSBBLsWeLiAoYykgS2FzxLFtIDIwMDUwHhcNMDUxMTA3MTAwNzU3
+WhcNMTUwOTE2MTAwNzU3WjCBvjE/MD0GA1UEAww2VMOcUktUUlVTVCBFbGVrdHJv
+bmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxMQswCQYDVQQGEwJU
+UjEPMA0GA1UEBwwGQW5rYXJhMV0wWwYDVQQKDFRUw5xSS1RSVVNUIEJpbGdpIMSw
+bGV0acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWe
+LiAoYykgS2FzxLFtIDIwMDUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
+AQCpNn7DkUNMwxmYCMjHWHtPFoylzkkBH3MOrHUTpvqeLCDe2JAOCtFp0if7qnef
+J1Il4std2NiDUBd9irWCPwSOtNXwSadktx4uXyCcUHVPr+G1QRT0mJKIx+XlZEdh
+R3n9wFHxwZnn3M5q+6+1ATDcRhzviuyV79z/rxAc653YsKpqhRgNF8k+v/Gb0AmJ
+Qv2gQrSdiVFVKc8bcLyEVK3BEx+Y9C52YItdP5qtygy/p1Zbj3e41Z55SZI/4PGX
+JHpsmxcPbe9TmJEr5A++WXkHeLuXlfSfadRYhwqp48y2WBmfJiGxxFmNskF1wK1p
+zpwACPI2/z7woQ8arBT9pmAPAgMBAAGjQzBBMB0GA1UdDgQWBBTZN7NOBf3Zz58S
+Fq62iS/rJTqIHDAPBgNVHQ8BAf8EBQMDBwYAMA8GA1UdEwEB/wQFMAMBAf8wDQYJ
+KoZIhvcNAQEFBQADggEBAHJglrfJ3NgpXiOFX7KzLXb7iNcX/nttRbj2hWyfIvwq
+ECLsqrkw9qtY1jkQMZkpAL2JZkH7dN6RwRgLn7Vhy506vvWolKMiVW4XSf/SKfE4
+Jl3vpao6+XF75tpYHdN0wgH6PmlYX63LaL4ULptswLbcoCb6dxriJNoaN+BnrdFz
+gw2lGh1uEpJ+hGIAF728JRhX8tepb1mIvDS3LoV4nZbcFMMsilKbloxSZj2GFotH
+uFEJjOp9zYhys2AzsfAKRO8P9Qk3iCQOLGsgOqL6EfJANZxEaGM7rDNvY7wsu/LS
+y3Z9fYjYHcgFHW68lKlmjHdxx/qR+i9Rnuk5UrbnBEI=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEXjCCA0agAwIBAgIQRL4Mi1AAIbQR0ypoBqmtaTANBgkqhkiG9w0BAQUFADCB
+kzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug
+Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho
+dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xGzAZBgNVBAMTElVUTiAtIERBVEFDb3Jw
+IFNHQzAeFw05OTA2MjQxODU3MjFaFw0xOTA2MjQxOTA2MzBaMIGTMQswCQYDVQQG
+EwJVUzELMAkGA1UECBMCVVQxFzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5MR4wHAYD
+VQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxITAfBgNVBAsTGGh0dHA6Ly93d3cu
+dXNlcnRydXN0LmNvbTEbMBkGA1UEAxMSVVROIC0gREFUQUNvcnAgU0dDMIIBIjAN
+BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3+5YEKIrblXEjr8uRgnn4AgPLit6
+E5Qbvfa2gI5lBZMAHryv4g+OGQ0SR+ysraP6LnD43m77VkIVni5c7yPeIbkFdicZ
+D0/Ww5y0vpQZY/KmEQrrU0icvvIpOxboGqBMpsn0GFlowHDyUwDAXlCCpVZvNvlK
+4ESGoE1O1kduSUrLZ9emxAW5jh70/P/N5zbgnAVssjMiFdC04MwXwLLA9P4yPykq
+lXvY8qdOD1R8oQ2AswkDwf9c3V6aPryuvEeKaq5xyh+xKrhfQgUL7EYw0XILyulW
+bfXv33i+Ybqypa4ETLyorGkVl73v67SMvzX41MPRKA5cOp9wGDMgd8SirwIDAQAB
+o4GrMIGoMAsGA1UdDwQEAwIBxjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRT
+MtGzz3/64PGgXYVOktKeRR20TzA9BgNVHR8ENjA0MDKgMKAuhixodHRwOi8vY3Js
+LnVzZXJ0cnVzdC5jb20vVVROLURBVEFDb3JwU0dDLmNybDAqBgNVHSUEIzAhBggr
+BgEFBQcDAQYKKwYBBAGCNwoDAwYJYIZIAYb4QgQBMA0GCSqGSIb3DQEBBQUAA4IB
+AQAnNZcAiosovcYzMB4p/OL31ZjUQLtgyr+rFywJNn9Q+kHcrpY6CiM+iVnJowft
+Gzet/Hy+UUla3joKVAgWRcKZsYfNjGjgaQPpxE6YsjuMFrMOoAyYUJuTqXAJyCyj
+j98C5OBxOvG0I3KgqgHf35g+FFCgMSa9KOlaMCZ1+XtgHI3zzVAmbQQnmt/VDUVH
+KWss5nbZqSl9Mt3JNjy9rjXxEZ4du5A/EkdOjtd+D2JzHVImOBwYSf0wdJrE5SIv
+2MCN7ZF6TACPcn9d2t0bi0Vr591pl6jFVkwPDPafepE39peC4N1xaf92P2BNPM/3
+mfnGV/TJVTl4uix5yaaIK/QI
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBi
+MQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu
+MTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3Jp
+dHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMxMjM1OTU5WjBiMQswCQYDVQQGEwJV
+UzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydO
+ZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0GCSqG
+SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwz
+c7MEL7xxjOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPP
+OCwGJgl6cvf6UDL4wpPTaaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rl
+mGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXTcrA/vGp97Eh/jcOrqnErU2lBUzS1sLnF
+BgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc/Qzpf14Dl847ABSHJ3A4
+qY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMBAAGjgZcw
+gZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIB
+BjAPBgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwu
+bmV0c29sc3NsLmNvbS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3Jp
+dHkuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc8
+6fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q4LqILPxFzBiwmZVRDuwduIj/
+h1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/GGUsyfJj4akH
+/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv
+wKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHN
+pGxlaKFJdlxDydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G
+A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp
+Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1
+MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG
+A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL
+v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8
+eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq
+tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd
+C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa
+zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB
+mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH
+V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n
+bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG
+3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs
+J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO
+291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS
+ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd
+AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7
+TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDpDCCAoygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEc
+MBoGA1UEChMTQW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBP
+bmxpbmUgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAxMB4XDTAyMDUyODA2
+MDAwMFoXDTM3MTExOTIwNDMwMFowYzELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0Ft
+ZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2EgT25saW5lIFJvb3Qg
+Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMTCCASIwDQYJKoZIhvcNAQEBBQADggEP
+ADCCAQoCggEBAKgv6KRpBgNHw+kqmP8ZonCaxlCyfqXfaE0bfA+2l2h9LaaLl+lk
+hsmj76CGv2BlnEtUiMJIxUo5vxTjWVXlGbR0yLQFOVwWpeKVBeASrlmLojNoWBym
+1BW32J/X3HGrfpq/m44zDyL9Hy7nBzbvYjnF3cu6JRQj3gzGPTzOggjmZj7aUTsW
+OqMFf6Dch9Wc/HKpoH145LcxVR5lu9RhsCFg7RAycsWSJR74kEoYeEfffjA3PlAb
+2xzTa5qGUwew76wGePiEmf4hjUyAtgyC9mZweRrTT6PP8c9GsEsPPt2IYriMqQko
+O3rHl+Ee5fSfwMCuJKDIodkP1nsmgmkyPacCAwEAAaNjMGEwDwYDVR0TAQH/BAUw
+AwEB/zAdBgNVHQ4EFgQUAK3Zo/Z59m50qX8zPYEX10zPM94wHwYDVR0jBBgwFoAU
+AK3Zo/Z59m50qX8zPYEX10zPM94wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB
+BQUAA4IBAQB8itEfGDeC4Liwo+1WlchiYZwFos3CYiZhzRAW18y0ZTTQEYqtqKkF
+Zu90821fnZmv9ov761KyBZiibyrFVL0lvV+uyIbqRizBs73B6UlwGBaXCBOMIOAb
+LjpHyx7kADCVW/RFo8AasAFOq73AI25jP4BKxQft3OJvx8Fi8eNy1gTIdGcL+oir
+oQHIb/AUr9KZzVGTfu0uOMe9zkZQPXLjeSWdm4grECDdpbgyn43gKd8hdIaC2y+C
+MMbHNYaz+ZZfRtsMRf3zUMNvxsNIrUam4SdHCh0Om7bCd39j8uB9Gr784N/Xx6ds
+sPmuujz9dLQR6FgNgLzTqIA6me11zEZ7
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICWjCCAcMCAgGlMA0GCSqGSIb3DQEBBAUAMHUxCzAJBgNVBAYTAlVTMRgwFgYD
+VQQKEw9HVEUgQ29ycG9yYXRpb24xJzAlBgNVBAsTHkdURSBDeWJlclRydXN0IFNv
+bHV0aW9ucywgSW5jLjEjMCEGA1UEAxMaR1RFIEN5YmVyVHJ1c3QgR2xvYmFsIFJv
+b3QwHhcNOTgwODEzMDAyOTAwWhcNMTgwODEzMjM1OTAwWjB1MQswCQYDVQQGEwJV
+UzEYMBYGA1UEChMPR1RFIENvcnBvcmF0aW9uMScwJQYDVQQLEx5HVEUgQ3liZXJU
+cnVzdCBTb2x1dGlvbnMsIEluYy4xIzAhBgNVBAMTGkdURSBDeWJlclRydXN0IEds
+b2JhbCBSb290MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVD6C28FCc6HrH
+iM3dFw4usJTQGz0O9pTAipTHBsiQl8i4ZBp6fmw8U+E3KHNgf7KXUwefU/ltWJTS
+r41tiGeA5u2ylc9yMcqlHHK6XALnZELn+aks1joNrI1CqiQBOeacPwGFVw1Yh0X4
+04Wqk2kmhXBIgD8SFcd5tB8FLztimQIDAQABMA0GCSqGSIb3DQEBBAUAA4GBAG3r
+GwnpXtlR22ciYaQqPEh346B8pt5zohQDhT37qw4wxYMWM4ETCJ57NE7fQMh017l9
+3PR2VX2bY1QY6fDq81yx2YtCHrnAlU66+tXifPVoYb+O7AWXX1uw16OFNMQkpw0P
+lZPvy5TYnh+dXIVtx6quTx8itc2VrbqnzPmrC3p/
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDJzCCApCgAwIBAgIBATANBgkqhkiG9w0BAQQFADCBzjELMAkGA1UEBhMCWkEx
+FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD
+VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv
+biBTZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UEAxMYVGhhd3RlIFByZW1pdW0gU2Vy
+dmVyIENBMSgwJgYJKoZIhvcNAQkBFhlwcmVtaXVtLXNlcnZlckB0aGF3dGUuY29t
+MB4XDTk2MDgwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgc4xCzAJBgNVBAYTAlpB
+MRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEdMBsG
+A1UEChMUVGhhd3RlIENvbnN1bHRpbmcgY2MxKDAmBgNVBAsTH0NlcnRpZmljYXRp
+b24gU2VydmljZXMgRGl2aXNpb24xITAfBgNVBAMTGFRoYXd0ZSBQcmVtaXVtIFNl
+cnZlciBDQTEoMCYGCSqGSIb3DQEJARYZcHJlbWl1bS1zZXJ2ZXJAdGhhd3RlLmNv
+bTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0jY2aovXwlue2oFBYo847kkE
+VdbQ7xwblRZH7xhINTpS9CtqBo87L+pW46+GjZ4X9560ZXUCTe/LCaIhUdib0GfQ
+ug2SBhRz1JPLlyoAnFxODLz6FVL88kRu2hFKbgifLy3j+ao6hnO2RlNYyIkFvYMR
+uHM/qgeN9EJN50CdHDcCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG
+9w0BAQQFAAOBgQAmSCwWwlj66BZ0DKqqX1Q/8tfJeGBeXm43YyJ3Nn6yF8Q0ufUI
+hfzJATj/Tb7yFkJD57taRvvBxhEf8UqwKEbJw8RCfbz6q1lu1bdRiBHjpIUZa4JM
+pAwSremkrj/xw0llmozFyD4lt5SZu5IycQfwhl7tUCemDaYj+bvLpgcUQg==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICkDCCAfmgAwIBAgIBATANBgkqhkiG9w0BAQQFADBaMQswCQYDVQQGEwJVUzEc
+MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEtMCsGA1UEAxMkRXF1aWZheCBT
+ZWN1cmUgR2xvYmFsIGVCdXNpbmVzcyBDQS0xMB4XDTk5MDYyMTA0MDAwMFoXDTIw
+MDYyMTA0MDAwMFowWjELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0VxdWlmYXggU2Vj
+dXJlIEluYy4xLTArBgNVBAMTJEVxdWlmYXggU2VjdXJlIEdsb2JhbCBlQnVzaW5l
+c3MgQ0EtMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuucXkAJlsTRVPEnC
+UdXfp9E3j9HngXNBUmCbnaEXJnitx7HoJpQytd4zjTov2/KaelpzmKNc6fuKcxtc
+58O/gGzNqfTWK8D3+ZmqY6KxRwIP1ORROhI8bIpaVIRw28HFkM9yRcuoWcDNM50/
+o5brhTMhHD4ePmBudpxnhcXIw2ECAwEAAaNmMGQwEQYJYIZIAYb4QgEBBAQDAgAH
+MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUvqigdHJQa0S3ySPY+6j/s1dr
+aGwwHQYDVR0OBBYEFL6ooHRyUGtEt8kj2Puo/7NXa2hsMA0GCSqGSIb3DQEBBAUA
+A4GBADDiAVGqx+pf2rnQZQ8w1j7aDRRJbpGTJxQx78T3LUX47Me/okENI7SS+RkA
+Z70Br83gcfxaz2TE4JaY0KNA4gGK7ycH8WUBikQtBmV1UsCGECAhX2xrD2yuCRyv
+8qIYNMR1pHMc8Y3c7635s3a0kr/clRAevsvIO1qEYBlWlKlV
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw
+NzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv
+b3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD
+VQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2
+MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F
+VRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1
+7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X
+Z75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+
+/jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs
+81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm
+dtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe
+Oh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu
+sDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4
+pgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs
+slESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ
+arMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD
+VR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG
+9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl
+dxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx
+0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj
+TQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed
+Y2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7
+Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI
+OylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7
+vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW
+t88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn
+HL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx
+SK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICiDCCAg2gAwIBAgIQNfwmXNmET8k9Jj1Xm67XVjAKBggqhkjOPQQDAzCBhDEL
+MAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjE4MDYGA1UECxMvKGMp
+IDIwMDcgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAi
+BgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMjAeFw0wNzExMDUwMDAw
+MDBaFw0zODAxMTgyMzU5NTlaMIGEMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhh
+d3RlLCBJbmMuMTgwNgYDVQQLEy8oYykgMjAwNyB0aGF3dGUsIEluYy4gLSBGb3Ig
+YXV0aG9yaXplZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9v
+dCBDQSAtIEcyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEotWcgnuVnfFSeIf+iha/
+BebfowJPDQfGAFG6DAJSLSKkQjnE/o/qycG+1E3/n3qe4rF8mq2nhglzh9HnmuN6
+papu+7qzcMBniKI11KOasf2twu8x+qi58/sIxpHR+ymVo0IwQDAPBgNVHRMBAf8E
+BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUmtgAMADna3+FGO6Lts6K
+DPgR4bswCgYIKoZIzj0EAwMDaQAwZgIxAN344FdHW6fmCsO99YCKlzUNG4k8VIZ3
+KMqh9HneteY4sPBlcIx/AlTCv//YoT7ZzwIxAMSNlPzcU9LcnXgWHxUzI1NS41ox
+XZ3Krr0TKUQNJ1uo52icEvdYPy5yAlejj6EULg==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEKjCCAxKgAwIBAgIQYAGXt0an6rS0mtZLL/eQ+zANBgkqhkiG9w0BAQsFADCB
+rjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf
+Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw
+MDggdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAiBgNV
+BAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMzAeFw0wODA0MDIwMDAwMDBa
+Fw0zNzEyMDEyMzU5NTlaMIGuMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhhd3Rl
+LCBJbmMuMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9uIFNlcnZpY2VzIERpdmlzaW9u
+MTgwNgYDVQQLEy8oYykgMjAwOCB0aGF3dGUsIEluYy4gLSBGb3IgYXV0aG9yaXpl
+ZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAtIEcz
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsr8nLPvb2FvdeHsbnndm
+gcs+vHyu86YnmjSjaDFxODNi5PNxZnmxqWWjpYvVj2AtP0LMqmsywCPLLEHd5N/8
+YZzic7IilRFDGF/Eth9XbAoFWCLINkw6fKXRz4aviKdEAhN0cXMKQlkC+BsUa0Lf
+b1+6a4KinVvnSr0eAXLbS3ToO39/fR8EtCab4LRarEc9VbjXsCZSKAExQGbY2SS9
+9irY7CFJXJv2eul/VTV+lmuNk5Mny5K76qxAwJ/C+IDPXfRa3M50hqY+bAtTyr2S
+zhkGcuYMXDhpxwTWvGzOW/b3aJzcJRVIiKHpqfiYnODz1TEoYRFsZ5aNOZnLwkUk
+OQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNV
+HQ4EFgQUrWyqlGCc7eT/+j4KdCtjA/e2Wb8wDQYJKoZIhvcNAQELBQADggEBABpA
+2JVlrAmSicY59BDlqQ5mU1143vokkbvnRFHfxhY0Cu9qRFHqKweKA3rD6z8KLFIW
+oCtDuSWQP3CpMyVtRRooOyfPqsMpQhvfO0zAMzRbQYi/aytlryjvsvXDqmbOe1bu
+t8jLZ8HJnBoYuMTDSQPxYA5QzUbF83d597YV4Djbxy8ooAw/dyZ02SUS2jHaGh7c
+KUGRIjxpp7sC8rZcJwOJ9Abqm+RyguOhCcHpABnTPtRwa7pxpqpYrvS76Wy274fM
+m7v/OeZWYdMKp8RcTGB7BXcmer/YB1IsYvdwY9k5vG8cwnncdimvzsUsZAReiDZu
+MdRAGmI0Nj81Aa6sY6A=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIHhzCCBW+gAwIBAgIBLTANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJJTDEW
+MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg
+Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh
+dGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM3WhcNMzYwOTE3MTk0NjM2WjB9
+MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi
+U2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh
+cnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA
+A4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk
+pMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf
+OQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C
+Ji/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT
+Kqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi
+HzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM
+Av+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w
++2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+
+Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3
+Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B
+26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID
+AQABo4ICEDCCAgwwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD
+VR0OBBYEFE4L7xqkQFulF2mHMMo0aEPQQa7yMB8GA1UdIwQYMBaAFE4L7xqkQFul
+F2mHMMo0aEPQQa7yMIIBWgYDVR0gBIIBUTCCAU0wggFJBgsrBgEEAYG1NwEBATCC
+ATgwLgYIKwYBBQUHAgEWImh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL3BvbGljeS5w
+ZGYwNAYIKwYBBQUHAgEWKGh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL2ludGVybWVk
+aWF0ZS5wZGYwgc8GCCsGAQUFBwICMIHCMCcWIFN0YXJ0IENvbW1lcmNpYWwgKFN0
+YXJ0Q29tKSBMdGQuMAMCAQEagZZMaW1pdGVkIExpYWJpbGl0eSwgcmVhZCB0aGUg
+c2VjdGlvbiAqTGVnYWwgTGltaXRhdGlvbnMqIG9mIHRoZSBTdGFydENvbSBDZXJ0
+aWZpY2F0aW9uIEF1dGhvcml0eSBQb2xpY3kgYXZhaWxhYmxlIGF0IGh0dHA6Ly93
+d3cuc3RhcnRzc2wuY29tL3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgG
+CWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1
+dGhvcml0eTANBgkqhkiG9w0BAQsFAAOCAgEAjo/n3JR5fPGFf59Jb2vKXfuM/gTF
+wWLRfUKKvFO3lANmMD+x5wqnUCBVJX92ehQN6wQOQOY+2IirByeDqXWmN3PH/UvS
+Ta0XQMhGvjt/UfzDtgUx3M2FIk5xt/JxXrAaxrqTi3iSSoX4eA+D/i+tLPfkpLst
+0OcNOrg+zvZ49q5HJMqjNTbOx8aHmNrs++myziebiMMEofYLWWivydsQD032ZGNc
+pRJvkrKTlMeIFw6Ttn5ii5B/q06f/ON1FE8qMt9bDeD1e5MNq6HPh+GlBEXoPBKl
+CcWw0bdT82AUuoVpaiF8H3VhFyAXe2w7QSlc4axa0c2Mm+tgHRns9+Ww2vl5GKVF
+P0lDV9LdJNUso/2RjSe15esUBppMeyG7Oq0wBhjA2MFrLH9ZXF2RsXAiV+uKa0hK
+1Q8p7MZAwC+ITGgBF3f0JBlPvfrhsiAhS90a2Cl9qrjeVOwhVYBsHvUwyKMQ5bLm
+KhQxw4UtjJixhlpPiVktucf3HMiKf8CdBUrmQk9io20ppB+Fq9vlgcitKj1MXVuE
+JnHEhV5xJMqlG2zYYdMa4FTbzrqpMrUi9nNBCV24F10OD5mQ1kfabwo6YigUZ4LZ
+8dCAWZvLMdibD4x3TrVoivJs9iQOLWxwxXPR3hTQcY+203sC9uO41Alua551hDnm
+fyWl8kgAwKQB2j8=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDfTCCAmWgAwIBAgIBADANBgkqhkiG9w0BAQUFADBgMQswCQYDVQQGEwJKUDEl
+MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEqMCgGA1UECxMh
+U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBFViBSb290Q0ExMB4XDTA3MDYwNjAyMTIz
+MloXDTM3MDYwNjAyMTIzMlowYDELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09N
+IFRydXN0IFN5c3RlbXMgQ08uLExURC4xKjAoBgNVBAsTIVNlY3VyaXR5IENvbW11
+bmljYXRpb24gRVYgUm9vdENBMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
+ggEBALx/7FebJOD+nLpCeamIivqA4PUHKUPqjgo0No0c+qe1OXj/l3X3L+SqawSE
+RMqm4miO/VVQYg+kcQ7OBzgtQoVQrTyWb4vVog7P3kmJPdZkLjjlHmy1V4qe70gO
+zXppFodEtZDkBp2uoQSXWHnvIEqCa4wiv+wfD+mEce3xDuS4GBPMVjZd0ZoeUWs5
+bmB2iDQL87PRsJ3KYeJkHcFGB7hj3R4zZbOOCVVSPbW9/wfrrWFVGCypaZhKqkDF
+MxRldAD5kd6vA0jFQFTcD4SQaCDFkpbcLuUCRarAX1T4bepJz11sS6/vmsJWXMY1
+VkJqMF/Cq/biPT+zyRGPMUzXn0kCAwEAAaNCMEAwHQYDVR0OBBYEFDVK9U2vP9eC
+OKyrcWUXdYydVZPmMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0G
+CSqGSIb3DQEBBQUAA4IBAQCoh+ns+EBnXcPBZsdAS5f8hxOQWsTvoMpfi7ent/HW
+tWS3irO4G8za+6xmiEHO6Pzk2x6Ipu0nUBsCMCRGef4Eh3CXQHPRwMFXGZpppSeZ
+q51ihPZRwSzJIxXYKLerJRO1RuGGAv8mjMSIkh1W/hln8lXkgKNrnKt34VFxDSDb
+EJrbvXZ5B3eZKK2aXtqxT0QsNY6llsf9g/BYxnnWmHyojf6GPgcWkuF75x3sM3Z+
+Qi5KhfmRiWiEA4Glm5q+4zfFVKtWOxgtQaQM+ELbmaDgcm+7XeEWT1MKZPlO9L9O
+VL14bIjqv5wTJMJwaaJ/D8g8rQjJsJhAoyrniIPtd490
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL
+BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc
+BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00
+MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM
+aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG
+SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV
+wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe
+rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341
+68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh
+4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp
+UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o
+abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc
+3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G
+KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt
+hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO
+Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt
+zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
+BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD
+ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC
+MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2
+cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN
+qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5
+YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv
+b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2
+8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k
+NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj
+ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp
+q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt
+nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw
+CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu
+ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg
+RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV
+UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu
+Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq
+hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf
+Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q
+RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/
+BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD
+AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY
+JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv
+6pZjamVFkpUBtA==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx
+EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT
+VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5
+NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT
+B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG
+SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF
+10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz
+0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh
+MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH
+zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc
+46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2
+yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi
+laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP
+oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA
+BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE
+qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm
+4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB
+/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL
+1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn
+LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF
+H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo
+RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+
+nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh
+15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW
+6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW
+nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j
+wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz
+aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy
+KwbQBM0=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDoDCCAoigAwIBAgIBMTANBgkqhkiG9w0BAQUFADBDMQswCQYDVQQGEwJKUDEc
+MBoGA1UEChMTSmFwYW5lc2UgR292ZXJubWVudDEWMBQGA1UECxMNQXBwbGljYXRp
+b25DQTAeFw0wNzEyMTIxNTAwMDBaFw0xNzEyMTIxNTAwMDBaMEMxCzAJBgNVBAYT
+AkpQMRwwGgYDVQQKExNKYXBhbmVzZSBHb3Zlcm5tZW50MRYwFAYDVQQLEw1BcHBs
+aWNhdGlvbkNBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp23gdE6H
+j6UG3mii24aZS2QNcfAKBZuOquHMLtJqO8F6tJdhjYq+xpqcBrSGUeQ3DnR4fl+K
+f5Sk10cI/VBaVuRorChzoHvpfxiSQE8tnfWuREhzNgaeZCw7NCPbXCbkcXmP1G55
+IrmTwcrNwVbtiGrXoDkhBFcsovW8R0FPXjQilbUfKW1eSvNNcr5BViCH/OlQR9cw
+FO5cjFW6WY2H/CPek9AEjP3vbb3QesmlOmpyM8ZKDQUXKi17safY1vC+9D/qDiht
+QWEjdnjDuGWk81quzMKq2edY3rZ+nYVunyoKb58DKTCXKB28t89UKU5RMfkntigm
+/qJj5kEW8DOYRwIDAQABo4GeMIGbMB0GA1UdDgQWBBRUWssmP3HMlEYNllPqa0jQ
+k/5CdTAOBgNVHQ8BAf8EBAMCAQYwWQYDVR0RBFIwUKROMEwxCzAJBgNVBAYTAkpQ
+MRgwFgYDVQQKDA/ml6XmnKzlm73mlL/lupwxIzAhBgNVBAsMGuOCouODl+ODquOC
+seODvOOCt+ODp+ODs0NBMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD
+ggEBADlqRHZ3ODrso2dGD/mLBqj7apAxzn7s2tGJfHrrLgy9mTLnsCTWw//1sogJ
+hyzjVOGjprIIC8CFqMjSnHH2HZ9g/DgzE+Ge3Atf2hZQKXsvcJEPmbo0NI2VdMV+
+eKlmXb3KIXdCEKxmJj3ekav9FfBv7WxfEPjzFvYDio+nEhEMy/0/ecGc/WLuo89U
+DNErXxc+4z6/wCs+CZv+iKZ+tJIX/COUgb1up8WMwusRRdv4QcmWdupwX3kSa+Sj
+B1oF7ydJzyGfikwJcGapJsErEU4z0g781mzSDjJkaP+tBXhfAx2o45CsJOAPQKdL
+rosot4LKGAfmt1t06SAZf7IbiVQ=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIID8TCCAtmgAwIBAgIQQT1yx/RrH4FDffHSKFTfmjANBgkqhkiG9w0BAQUFADCB
+ijELMAkGA1UEBhMCQ0gxEDAOBgNVBAoTB1dJU2VLZXkxGzAZBgNVBAsTEkNvcHly
+aWdodCAoYykgMjAwNTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl
+ZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQSBDQTAeFw0w
+NTEyMTExNjAzNDRaFw0zNzEyMTExNjA5NTFaMIGKMQswCQYDVQQGEwJDSDEQMA4G
+A1UEChMHV0lTZUtleTEbMBkGA1UECxMSQ29weXJpZ2h0IChjKSAyMDA1MSIwIAYD
+VQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBX
+SVNlS2V5IEdsb2JhbCBSb290IEdBIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEAy0+zAJs9Nt350UlqaxBJH+zYK7LG+DKBKUOVTJoZIyEVRd7jyBxR
+VVuuk+g3/ytr6dTqvirdqFEr12bDYVxgAsj1znJ7O7jyTmUIms2kahnBAbtzptf2
+w93NvKSLtZlhuAGio9RN1AU9ka34tAhxZK9w8RxrfvbDd50kc3vkDIzh2TbhmYsF
+mQvtRTEJysIA2/dyoJaqlYfQjse2YXMNdmaM3Bu0Y6Kff5MTMPGhJ9vZ/yxViJGg
+4E8HsChWjBgbl0SOid3gF27nKu+POQoxhILYQBRJLnpB5Kf+42TMwVlxSywhp1t9
+4B3RLoGbw9ho972WG6xwsRYUC9tguSYBBQIDAQABo1EwTzALBgNVHQ8EBAMCAYYw
+DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUswN+rja8sHnR3JQmthG+IbJphpQw
+EAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBAEuh/wuHbrP5wUOx
+SPMowB0uyQlB+pQAHKSkq0lPjz0e701vvbyk9vImMMkQyh2I+3QZH4VFvbBsUfk2
+ftv1TDI6QU9bR8/oCy22xBmddMVHxjtqD6wU2zz0c5ypBd8A3HR4+vg1YFkCExh8
+vPtNsCBtQ7tgMHpnM1zFmdH4LTlSc/uMqpclXHLZCB6rTjzjgTGfA6b7wP4piFXa
+hNVQA7bihKOmNqoROgHhGEvWRGizPflTdISzRpFGlgC3gCy24eMQ4tui5yiPAZZi
+Fj4A4xylNoEYokxSdsARo27mHbrjWr42U8U+dY+GaSlYU7Wcu2+fXMUY7N0v4ZjJ
+/L7fCg0=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw
+CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl
+cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu
+LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT
+aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp
+dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD
+VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT
+aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ
+bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu
+IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg
+LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b
+N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t
+KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu
+kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm
+CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ
+Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu
+imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te
+2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe
+DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC
+/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p
+F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt
+TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDtjCCAp6gAwIBAgIQRJmNPMADJ72cdpW56tustTANBgkqhkiG9w0BAQUFADB1
+MQswCQYDVQQGEwJUUjEoMCYGA1UEChMfRWxla3Ryb25payBCaWxnaSBHdXZlbmxp
+Z2kgQS5TLjE8MDoGA1UEAxMzZS1HdXZlbiBLb2sgRWxla3Ryb25payBTZXJ0aWZp
+a2EgSGl6bWV0IFNhZ2xheWljaXNpMB4XDTA3MDEwNDExMzI0OFoXDTE3MDEwNDEx
+MzI0OFowdTELMAkGA1UEBhMCVFIxKDAmBgNVBAoTH0VsZWt0cm9uaWsgQmlsZ2kg
+R3V2ZW5saWdpIEEuUy4xPDA6BgNVBAMTM2UtR3V2ZW4gS29rIEVsZWt0cm9uaWsg
+U2VydGlmaWthIEhpem1ldCBTYWdsYXlpY2lzaTCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBAMMSIJ6wXgBljU5Gu4Bc6SwGl9XzcslwuedLZYDBS75+PNdU
+MZTe1RK6UxYC6lhj71vY8+0qGqpxSKPcEC1fX+tcS5yWCEIlKBHMilpiAVDV6wlT
+L/jDj/6z/P2douNffb7tC+Bg62nsM+3YjfsSSYMAyYuXjDtzKjKzEve5TfL0TW3H
+5tYmNwjy2f1rXKPlSFxYvEK+A1qBuhw1DADT9SN+cTAIJjjcJRFHLfO6IxClv7wC
+90Nex/6wN1CZew+TzuZDLMN+DfIcQ2Zgy2ExR4ejT669VmxMvLz4Bcpk9Ok0oSy1
+c+HCPujIyTQlCFzz7abHlJ+tiEMl1+E5YP6sOVkCAwEAAaNCMEAwDgYDVR0PAQH/
+BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJ/uRLOU1fqRTy7ZVZoE
+VtstxNulMA0GCSqGSIb3DQEBBQUAA4IBAQB/X7lTW2M9dTLn+sR0GstG30ZpHFLP
+qk/CaOv/gKlR6D1id4k9CnU58W5dF4dvaAXBlGzZXd/aslnLpRCKysw5zZ/rTt5S
+/wzw9JKp8mxTq5vSR6AfdPebmvEvFZ96ZDAYBzwqD2fK/A+JYZ1lpTzlvBNbCNvj
+/+27BrtqBrF6T2XGgv0enIu1De5Iu7i9qgi0+6N8y5/NkHZchpZ4Vwpm+Vganf2X
+KWDeEaaQHBkc7gGWIjQ0LpH5t8Qn0Xvmv/uARFoW5evg1Ao4vOSR49XrXMGs3xtq
+fJ7lddK2l4fbzIcrQzqECK+rPNv3PGYxhrCdU3nt+CPeQuMtgvEP5fqX
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE
+BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w
+MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290
+IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC
+SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1
+ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv
+UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX
+4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9
+KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/
+gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb
+rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ
+51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F
+be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe
+KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F
+v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn
+fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7
+jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz
+ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt
+ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL
+e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70
+jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz
+WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V
+SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j
+pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX
+X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok
+fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R
+K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU
+ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU
+LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT
+LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE
+BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz
+dCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG
+A1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U
+cnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf
+qV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ
+JG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ
++jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS
+s8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5
+HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7
+70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG
+V+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S
+qHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S
+5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia
+C1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX
+OwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE
+FJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/
+BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2
+KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg
+Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B
+8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ
+MKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc
+0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ
+u4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF
+u+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH
+YoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8
+GKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO
+RtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e
+KeC2uAloGRwYQw==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEQzCCAyugAwIBAgIBATANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJHQjEb
+MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow
+GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDElMCMGA1UEAwwcVHJ1c3RlZCBDZXJ0
+aWZpY2F0ZSBTZXJ2aWNlczAeFw0wNDAxMDEwMDAwMDBaFw0yODEyMzEyMzU5NTla
+MH8xCzAJBgNVBAYTAkdCMRswGQYDVQQIDBJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO
+BgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoMEUNvbW9kbyBDQSBMaW1pdGVkMSUwIwYD
+VQQDDBxUcnVzdGVkIENlcnRpZmljYXRlIFNlcnZpY2VzMIIBIjANBgkqhkiG9w0B
+AQEFAAOCAQ8AMIIBCgKCAQEA33FvNlhTWvI2VFeAxHQIIO0Yfyod5jWaHiWsnOWW
+fnJSoBVC21ndZHoa0Lh73TkVvFVIxO06AOoxEbrycXQaZ7jPM8yoMa+j49d/vzMt
+TGo87IvDktJTdyR0nAducPy9C1t2ul/y/9c3S0pgePfw+spwtOpZqqPOSC+pw7IL
+fhdyFgymBwwbOM/JYrc/oJOlh0Hyt3BAd9i+FHzjqMB6juljatEPmsbS9Is6FARW
+1O24zG71++IsWL1/T2sr92AkWCTOJu80kTrV44HQsvAEAtdbtz6SrGsSivnkBbA7
+kUlcsutT6vifR4buv5XAwAaf0lteERv0xwQ1KdJVXOTt6wIDAQABo4HJMIHGMB0G
+A1UdDgQWBBTFe1i97doladL3WRaoszLAeydb9DAOBgNVHQ8BAf8EBAMCAQYwDwYD
+VR0TAQH/BAUwAwEB/zCBgwYDVR0fBHwwejA8oDqgOIY2aHR0cDovL2NybC5jb21v
+ZG9jYS5jb20vVHJ1c3RlZENlcnRpZmljYXRlU2VydmljZXMuY3JsMDqgOKA2hjRo
+dHRwOi8vY3JsLmNvbW9kby5uZXQvVHJ1c3RlZENlcnRpZmljYXRlU2VydmljZXMu
+Y3JsMA0GCSqGSIb3DQEBBQUAA4IBAQDIk4E7ibSvuIQSTI3S8NtwuleGFTQQuS9/
+HrCoiWChisJ3DFBKmwCL2Iv0QeLQg4pKHBQGsKNoBXAxMKdTmw7pSqBYaWcOrp32
+pSxBvzwGa+RZzG0Q8ZZvH9/0BAKkn0U+yNj6NkZEUD+Cl5EfKNsYEYwq5GWDVxIS
+jBc/lDb+XbDABHcTuPQV1T84zJQ6VdCsmPW6AF/ghhmBeC8owH7TzEIK9a5QoNE+
+xqFx7D+gIIxmOom0jtTYsU0lR+4viMi14QVFwL4Ucd56/Y57fU0IlqUSc/Atyjcn
+dBInTMu2l+nZrghtWjlA3QVHdWpaIbOjGM9O9y5Xt5hwXsjEeLBi
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIECTCCAvGgAwIBAgIQDV6ZCtadt3js2AdWO4YV2TANBgkqhkiG9w0BAQUFADBb
+MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3Qx
+ETAPBgNVBAsTCERTVCBBQ0VTMRcwFQYDVQQDEw5EU1QgQUNFUyBDQSBYNjAeFw0w
+MzExMjAyMTE5NThaFw0xNzExMjAyMTE5NThaMFsxCzAJBgNVBAYTAlVTMSAwHgYD
+VQQKExdEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdDERMA8GA1UECxMIRFNUIEFDRVMx
+FzAVBgNVBAMTDkRTVCBBQ0VTIENBIFg2MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEAuT31LMmU3HWKlV1j6IR3dma5WZFcRt2SPp/5DgO0PWGSvSMmtWPu
+ktKe1jzIDZBfZIGxqAgNTNj50wUoUrQBJcWVHAx+PhCEdc/BGZFjz+iokYi5Q1K7
+gLFViYsx+tC3dr5BPTCapCIlF3PoHuLTrCq9Wzgh1SpL11V94zpVvddtawJXa+ZH
+fAjIgrrep4c9oW24MFbCswKBXy314powGCi4ZtPLAZZv6opFVdbgnf9nKxcCpk4a
+ahELfrd755jWjHZvwTvbUJN+5dCOHze4vbrGn2zpfDPyMjwmR/onJALJfh1biEIT
+ajV8fTXpLmaRcpPVMibEdPVTo7NdmvYJywIDAQABo4HIMIHFMA8GA1UdEwEB/wQF
+MAMBAf8wDgYDVR0PAQH/BAQDAgHGMB8GA1UdEQQYMBaBFHBraS1vcHNAdHJ1c3Rk
+c3QuY29tMGIGA1UdIARbMFkwVwYKYIZIAWUDAgEBATBJMEcGCCsGAQUFBwIBFjto
+dHRwOi8vd3d3LnRydXN0ZHN0LmNvbS9jZXJ0aWZpY2F0ZXMvcG9saWN5L0FDRVMt
+aW5kZXguaHRtbDAdBgNVHQ4EFgQUCXIGThhDD+XWzMNqizF7eI+og7gwDQYJKoZI
+hvcNAQEFBQADggEBAKPYjtay284F5zLNAdMEA+V25FYrnJmQ6AgwbN99Pe7lv7Uk
+QIRJ4dEorsTCOlMwiPH1d25Ryvr/ma8kXxug/fKshMrfqfBfBC6tFr8hlxCBPeP/
+h40y3JTlR4peahPJlJU90u7INJXQgNStMgiAVDzgvVJT11J8smk/f3rPanTK+gQq
+nExaBqXpIK1FZg9p8d2/6eMyi/rgwYZNcjwu2JN4Cir42NInPRmJX1p7ijvMDNpR
+rscL9yuwNwXsvFcj4jjSm2jzVhKIT0J8uDHEtdvkyCE06UgRNe76x5JXxZ805Mf2
+9w4LTJxoeHtxMcfrHuBnQfO3oKfN5XozNmr6mis=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe
+MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0
+ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe
+Fw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw
+IQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL
+SSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF
+AAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH
+SyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh
+ijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X
+DZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1
+TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ
+fzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA
+sgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU
+WH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS
+nT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH
+dmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip
+NiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC
+AwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF
+MAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH
+ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB
+uvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl
+PwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP
+JXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/
+gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2
+j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6
+5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB
+o2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS
+/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z
+Gp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE
+W9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D
+hNQ+IIX3Sj0rnP0qCglN6oH4EZw=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYG
+A1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2Jh
+bCBSb290MB4XDTA2MTIxNTA4MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UE
+ChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBS
+b290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+Mi8vRRQZhP/8NN5
+7CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW0ozS
+J8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2y
+HLtgwEZLAfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iP
+t3sMpTjr3kfb1V05/Iin89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNz
+FtApD0mpSPCzqrdsxacwOUBdrsTiXSZT8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAY
+XSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/
+MB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2MDSgMqAw
+hi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3Js
+MB8GA1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUA
+A4IBAQBW7wojoFROlZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMj
+Wqd8BfP9IjsO0QbE2zZMcwSO5bAi5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUx
+XOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2hO0j9n0Hq0V+09+zv+mKts2o
+omcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+TX3EJIrduPuoc
+A06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW
+WL1WMRJOEcgh4LMRkWXbtKaIOM5V
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICgjCCAeugAwIBAgIBBDANBgkqhkiG9w0BAQQFADBTMQswCQYDVQQGEwJVUzEc
+MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEmMCQGA1UEAxMdRXF1aWZheCBT
+ZWN1cmUgZUJ1c2luZXNzIENBLTEwHhcNOTkwNjIxMDQwMDAwWhcNMjAwNjIxMDQw
+MDAwWjBTMQswCQYDVQQGEwJVUzEcMBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5j
+LjEmMCQGA1UEAxMdRXF1aWZheCBTZWN1cmUgZUJ1c2luZXNzIENBLTEwgZ8wDQYJ
+KoZIhvcNAQEBBQADgY0AMIGJAoGBAM4vGbwXt3fek6lfWg0XTzQaDJj0ItlZ1MRo
+RvC0NcWFAyDGr0WlIVFFQesWWDYyb+JQYmT5/VGcqiTZ9J2DKocKIdMSODRsjQBu
+WqDZQu4aIZX5UkxVWsUPOE9G+m34LjXWHXzr4vCwdYDIqROsvojvOm6rXyo4YgKw
+Env+j6YDAgMBAAGjZjBkMBEGCWCGSAGG+EIBAQQEAwIABzAPBgNVHRMBAf8EBTAD
+AQH/MB8GA1UdIwQYMBaAFEp4MlIR21kWNl7fwRQ2QGpHfEyhMB0GA1UdDgQWBBRK
+eDJSEdtZFjZe38EUNkBqR3xMoTANBgkqhkiG9w0BAQQFAAOBgQB1W6ibAxHm6VZM
+zfmpTMANmvPMZWnmJXbMWbfWVMMdzZmsGd20hdXgPfxiIKeES1hl8eL5lSE/9dR+
+WB5Hh1Q+WKG1tfgq73HnvMP2sUlG4tega+VWeponmHxGYhTnyfxuAxJ5gDgdSIKN
+/Bf+KpYrtWKmpj29f5JZzVoqgrI3eQ==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEvTCCA6WgAwIBAgIBADANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJFVTEn
+MCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQL
+ExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEiMCAGA1UEAxMZQ2hhbWJlcnMg
+b2YgQ29tbWVyY2UgUm9vdDAeFw0wMzA5MzAxNjEzNDNaFw0zNzA5MzAxNjEzNDRa
+MH8xCzAJBgNVBAYTAkVVMScwJQYDVQQKEx5BQyBDYW1lcmZpcm1hIFNBIENJRiBB
+ODI3NDMyODcxIzAhBgNVBAsTGmh0dHA6Ly93d3cuY2hhbWJlcnNpZ24ub3JnMSIw
+IAYDVQQDExlDaGFtYmVycyBvZiBDb21tZXJjZSBSb290MIIBIDANBgkqhkiG9w0B
+AQEFAAOCAQ0AMIIBCAKCAQEAtzZV5aVdGDDg2olUkfzIx1L4L1DZ77F1c2VHfRtb
+unXF/KGIJPov7coISjlUxFF6tdpg6jg8gbLL8bvZkSM/SAFwdakFKq0fcfPJVD0d
+BmpAPrMMhe5cG3nCYsS4No41XQEMIwRHNaqbYE6gZj3LJgqcQKH0XZi/caulAGgq
+7YN6D6IUtdQis4CwPAxaUWktWBiP7Zme8a7ileb2R6jWDA+wWFjbw2Y3npuRVDM3
+0pQcakjJyfKl2qUMI/cjDpwyVV5xnIQFUZot/eZOKjRa3spAN2cMVCFVd9oKDMyX
+roDclDZK9D7ONhMeU+SsTjoF7Nuucpw4i9A5O4kKPnf+dQIBA6OCAUQwggFAMBIG
+A1UdEwEB/wQIMAYBAf8CAQwwPAYDVR0fBDUwMzAxoC+gLYYraHR0cDovL2NybC5j
+aGFtYmVyc2lnbi5vcmcvY2hhbWJlcnNyb290LmNybDAdBgNVHQ4EFgQU45T1sU3p
+26EpW1eLTXYGduHRooowDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIA
+BzAnBgNVHREEIDAegRxjaGFtYmVyc3Jvb3RAY2hhbWJlcnNpZ24ub3JnMCcGA1Ud
+EgQgMB6BHGNoYW1iZXJzcm9vdEBjaGFtYmVyc2lnbi5vcmcwWAYDVR0gBFEwTzBN
+BgsrBgEEAYGHLgoDATA+MDwGCCsGAQUFBwIBFjBodHRwOi8vY3BzLmNoYW1iZXJz
+aWduLm9yZy9jcHMvY2hhbWJlcnNyb290Lmh0bWwwDQYJKoZIhvcNAQEFBQADggEB
+AAxBl8IahsAifJ/7kPMa0QOx7xP5IV8EnNrJpY0nbJaHkb5BkAFyk+cefV/2icZd
+p0AJPaxJRUXcLo0waLIJuvvDL8y6C98/d3tGfToSJI6WjzwFCm/SlCgdbQzALogi
+1djPHRPH8EjX1wWnz8dHnjs8NMiAT9QUu/wNUPf6s+xCX6ndbcj0dc97wXImsQEc
+XCz9ek60AcUFV7nnPKoF2YjpB0ZBzu9Bga5Y34OirsrXdx/nADydb47kMgkdTXg0
+eDQ8lJsm7U9xxhl6vSAiSFr+S30Dt+dYvsYyTnQeaN2oaFuzPu5ifdmA6Ap1erfu
+tGWaIZDgqtCYvDi1czyL+Nw=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFdjCCA16gAwIBAgIQXmjWEXGUY1BWAGjzPsnFkTANBgkqhkiG9w0BAQUFADBV
+MQswCQYDVQQGEwJDTjEaMBgGA1UEChMRV29TaWduIENBIExpbWl0ZWQxKjAoBgNV
+BAMTIUNlcnRpZmljYXRpb24gQXV0aG9yaXR5IG9mIFdvU2lnbjAeFw0wOTA4MDgw
+MTAwMDFaFw0zOTA4MDgwMTAwMDFaMFUxCzAJBgNVBAYTAkNOMRowGAYDVQQKExFX
+b1NpZ24gQ0EgTGltaXRlZDEqMCgGA1UEAxMhQ2VydGlmaWNhdGlvbiBBdXRob3Jp
+dHkgb2YgV29TaWduMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvcqN
+rLiRFVaXe2tcesLea9mhsMMQI/qnobLMMfo+2aYpbxY94Gv4uEBf2zmoAHqLoE1U
+fcIiePyOCbiohdfMlZdLdNiefvAA5A6JrkkoRBoQmTIPJYhTpA2zDxIIFgsDcScc
+f+Hb0v1naMQFXQoOXXDX2JegvFNBmpGN9J42Znp+VsGQX+axaCA2pIwkLCxHC1l2
+ZjC1vt7tj/id07sBMOby8w7gLJKA84X5KIq0VC6a7fd2/BVoFutKbOsuEo/Uz/4M
+x1wdC34FMr5esAkqQtXJTpCzWQ27en7N1QhatH/YHGkR+ScPewavVIMYe+HdVHpR
+aG53/Ma/UkpmRqGyZxq7o093oL5d//xWC0Nyd5DKnvnyOfUNqfTq1+ezEC8wQjch
+zDBwyYaYD8xYTYO7feUapTeNtqwylwA6Y3EkHp43xP901DfA4v6IRmAR3Qg/UDar
+uHqklWJqbrDKaiFaafPz+x1wOZXzp26mgYmhiMU7ccqjUu6Du/2gd/Tkb+dC221K
+mYo0SLwX3OSACCK28jHAPwQ+658geda4BmRkAjHXqc1S+4RFaQkAKtxVi8QGRkvA
+Sh0JWzko/amrzgD5LkhLJuYwTKVYyrREgk/nkR4zw7CT/xH8gdLKH3Ep3XZPkiWv
+HYG3Dy+MwwbMLyejSuQOmbp8HkUff6oZRZb9/D0CAwEAAaNCMEAwDgYDVR0PAQH/
+BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOFmzw7R8bNLtwYgFP6H
+EtX2/vs+MA0GCSqGSIb3DQEBBQUAA4ICAQCoy3JAsnbBfnv8rWTjMnvMPLZdRtP1
+LOJwXcgu2AZ9mNELIaCJWSQBnfmvCX0KI4I01fx8cpm5o9dU9OpScA7F9dY74ToJ
+MuYhOZO9sxXqT2r09Ys/L3yNWC7F4TmgPsc9SnOeQHrAK2GpZ8nzJLmzbVUsWh2e
+JXLOC62qx1ViC777Y7NhRCOjy+EaDveaBk3e1CNOIZZbOVtXHS9dCF4Jef98l7VN
+g64N1uajeeAz0JmWAjCnPv/So0M/BVoG6kQC2nz4SNAzqfkHx5Xh9T71XXG68pWp
+dIhhWeO/yloTunK0jF02h+mmxTwTv97QRCbut+wucPrXnbes5cVAWubXbHssw1ab
+R80LzvobtCHXt2a49CUwi1wNuepnsvRtrtWhnk/Yn+knArAdBtaP4/tIEp9/EaEQ
+PkxROpaw0RPxx9gmrjrKkcRpnd8BKWRRb2jaFOwIQZeQjdCygPLPwj2/kWjFgGce
+xGATVdVhmVd8upUPYUk6ynW8yQqTP2cOEvIo4jEbwFcW3wh8GcF+Dx+FHgo2fFt+
+J7x6v+Db9NpSvd4MVHAxkUOVyLzwPt0JfjBkUO1/AaQzZ01oT74V77D2AhGiGxMl
+OtzCWfHjXEa7ZywCRuoeSKbmW9m1vFGikpbbqsY3Iqb+zCB0oy2pLmvLwIIRIbWT
+ee5Ehr7XHuQe+w==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDAjCCAmsCEH3Z/gfPqB63EHln+6eJNMYwDQYJKoZIhvcNAQEFBQAwgcExCzAJ
+BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh
+c3MgMyBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy
+MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp
+emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X
+DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw
+FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMg
+UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo
+YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5
+MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB
+AQUAA4GNADCBiQKBgQDMXtERXVxp0KvTuWpMmR9ZmDCOFoUgRm1HP9SFIIThbbP4
+pO0M8RcPO/mn+SXXwc+EY/J8Y8+iR/LGWzOOZEAEaMGAuWQcRXfH2G71lSk8UOg0
+13gfqLptQ5GVj0VXXn7F+8qkBOvqlzdUMG+7AUcyM83cV5tkaWH4mx0ciU9cZwID
+AQABMA0GCSqGSIb3DQEBBQUAA4GBAFFNzb5cy5gZnBWyATl4Lk0PZ3BwmcYQWpSk
+U01UbSuvDV1Ai2TT1+7eVmGSX6bEHRBhNtMsJzzoKQm5EWR0zLVznxxIqbxhAe7i
+F6YM40AIOw7n60RzKprxaZLvcRTDOaxxp5EJb+RxBrO6WVcmeQD2+A2iMzAo1KpY
+oJ2daZH9
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx
+EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT
+HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs
+ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5
+MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD
+VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy
+ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy
+dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p
+OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2
+8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K
+Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe
+hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk
+6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw
+DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q
+AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI
+bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB
+ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z
+qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd
+iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn
+0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN
+sSi6
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICrjCCAjWgAwIBAgIQPLL0SAoA4v7rJDteYD7DazAKBggqhkjOPQQDAzCBmDEL
+MAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChj
+KSAyMDA3IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2
+MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0
+eSAtIEcyMB4XDTA3MTEwNTAwMDAwMFoXDTM4MDExODIzNTk1OVowgZgxCzAJBgNV
+BAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykgMjAw
+NyBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNV
+BAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH
+MjB2MBAGByqGSM49AgEGBSuBBAAiA2IABBWx6P0DFUPlrOuHNxFi79KDNlJ9RVcL
+So17VDs6bl8VAsBQps8lL33KSLjHUGMcKiEIfJo22Av+0SbFWDEwKCXzXV2juLal
+tJLtbCyf691DiaI8S0iRHVDsJt/WYC69IaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO
+BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBVfNVdRVfslsq0DafwBo/q+EVXVMAoG
+CCqGSM49BAMDA2cAMGQCMGSWWaboCd6LuvpaiIjwH5HTRqjySkwCY/tsXzjbLkGT
+qQ7mndwxHLKgpxgceeHHNgIwOlavmnRs9vuD4DPTCF+hnMJbn0bWtsuRBmOiBucz
+rD6ogRLQy7rQkgu2npaqBA+K
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFaTCCA1GgAwIBAgIJAMMDmu5QkG4oMA0GCSqGSIb3DQEBBQUAMFIxCzAJBgNV
+BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu
+MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIxMB4XDTEyMDcxOTA5MDY1NloXDTQy
+MDcxOTA5MDY1NlowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx
+EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjEw
+ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCqw3j33Jijp1pedxiy3QRk
+D2P9m5YJgNXoqqXinCaUOuiZc4yd39ffg/N4T0Dhf9Kn0uXKE5Pn7cZ3Xza1lK/o
+OI7bm+V8u8yN63Vz4STN5qctGS7Y1oprFOsIYgrY3LMATcMjfF9DCCMyEtztDK3A
+fQ+lekLZWnDZv6fXARz2m6uOt0qGeKAeVjGu74IKgEH3G8muqzIm1Cxr7X1r5OJe
+IgpFy4QxTaz+29FHuvlglzmxZcfe+5nkCiKxLU3lSCZpq+Kq8/v8kiky6bM+TR8n
+oc2OuRf7JT7JbvN32g0S9l3HuzYQ1VTW8+DiR0jm3hTaYVKvJrT1cU/J19IG32PK
+/yHoWQbgCNWEFVP3Q+V8xaCJmGtzxmjOZd69fwX3se72V6FglcXM6pM6vpmumwKj
+rckWtc7dXpl4fho5frLABaTAgqWjR56M6ly2vGfb5ipN0gTco65F97yLnByn1tUD
+3AjLLhbKXEAz6GfDLuemROoRRRw1ZS0eRWEkG4IupZ0zXWX4Qfkuy5Q/H6MMMSRE
+7cderVC6xkGbrPAXZcD4XW9boAo0PO7X6oifmPmvTiT6l7Jkdtqr9O3jw2Dv1fkC
+yC2fg69naQanMVXVz0tv/wQFx1isXxYb5dKj6zHbHzMVTdDypVP1y+E9Tmgt2BLd
+qvLmTZtJ5cUoobqwWsagtQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud
+DwEB/wQEAwIBBjAdBgNVHQ4EFgQUiQq0OJMa5qvum5EY+fU8PjXQ04IwDQYJKoZI
+hvcNAQEFBQADggIBADKL9p1Kyb4U5YysOMo6CdQbzoaz3evUuii+Eq5FLAR0rBNR
+xVgYZk2C2tXck8An4b58n1KeElb21Zyp9HWc+jcSjxyT7Ff+Bw+r1RL3D65hXlaA
+SfX8MPWbTx9BLxyE04nH4toCdu0Jz2zBuByDHBb6lM19oMgY0sidbvW9adRtPTXo
+HqJPYNcHKfyyo6SdbhWSVhlMCrDpfNIZTUJG7L399ldb3Zh+pE3McgODWF3vkzpB
+emOqfDqo9ayk0d2iLbYq/J8BjuIQscTK5GfbVSUZP/3oNn6z4eGBrxEWi1CXYBmC
+AMBrTXO40RMHPuq2MU/wQppt4hF05ZSsjYSVPCGvxdpHyN85YmLLW1AL14FABZyb
+7bq2ix4Eb5YgOe2kfSnbSM6C3NQCjR0EMVrHS/BsYVLXtFHCgWzN4funodKSds+x
+DzdYpPJScWc/DIh4gInByLUfkmO+p3qKViwaqKactV2zY9ATIKHrkWzQjX2v3wvk
+F7mGnjixlAxYjOBVqjtjbZqJYLhkKpLGN/R+Q0O3c+gB53+XD9fyexn9GtePyfqF
+a3qdnom2piiZk4hA9z7NUaPK6u95RyG1/jLix8NRb76AdPCkwzryT+lf3xkK8jsT
+Q6wxpLPn6/wY1gGp8yqPNg7rtLG8t0zJa7+h89n07eLw4+1knj0vllJPgFOL
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCB
+qTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf
+Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw
+MDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNV
+BAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3MDAwMDAwWhcNMzYw
+NzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5j
+LjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYG
+A1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl
+IG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqG
+SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsoPD7gFnUnMekz52hWXMJEEUMDSxuaPFs
+W0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ1CRfBsDMRJSUjQJib+ta
+3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGcq/gcfomk
+6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6
+Sk/KaAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94J
+NqR32HuHUETVPm4pafs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBA
+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XP
+r87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUFAAOCAQEAeRHAS7ORtvzw6WfU
+DW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeEuzLlQRHAd9mz
+YJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX
+xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2
+/qxAeeWsEG89jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/
+LHbTY5xZ3Y+m4Q6gLkH3LpVHz7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7
+jVaMaA==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDkjCCAnqgAwIBAgIRAIW9S/PY2uNp9pTXX8OlRCMwDQYJKoZIhvcNAQEFBQAw
+PTELMAkGA1UEBhMCRlIxETAPBgNVBAoTCENlcnRwbHVzMRswGQYDVQQDExJDbGFz
+cyAyIFByaW1hcnkgQ0EwHhcNOTkwNzA3MTcwNTAwWhcNMTkwNzA2MjM1OTU5WjA9
+MQswCQYDVQQGEwJGUjERMA8GA1UEChMIQ2VydHBsdXMxGzAZBgNVBAMTEkNsYXNz
+IDIgUHJpbWFyeSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANxQ
+ltAS+DXSCHh6tlJw/W/uz7kRy1134ezpfgSN1sxvc0NXYKwzCkTsA18cgCSR5aiR
+VhKC9+Ar9NuuYS6JEI1rbLqzAr3VNsVINyPi8Fo3UjMXEuLRYE2+L0ER4/YXJQyL
+kcAbmXuZVg2v7tK8R1fjeUl7NIknJITesezpWE7+Tt9avkGtrAjFGA7v0lPubNCd
+EgETjdyAYveVqUSISnFOYFWe2yMZeVYHDD9jC1yw4r5+FfyUM1hBOHTE4Y+L3yas
+H7WLO7dDWWuwJKZtkIvEcupdM5i3y95ee++U8Rs+yskhwcWYAqqi9lt3m/V+llU0
+HGdpwPFC40es/CgcZlUCAwEAAaOBjDCBiTAPBgNVHRMECDAGAQH/AgEKMAsGA1Ud
+DwQEAwIBBjAdBgNVHQ4EFgQU43Mt38sOKAze3bOkynm4jrvoMIkwEQYJYIZIAYb4
+QgEBBAQDAgEGMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly93d3cuY2VydHBsdXMu
+Y29tL0NSTC9jbGFzczIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCnVM+IRBnL39R/
+AN9WM2K191EBkOvDP9GIROkkXe/nFL0gt5o8AP5tn9uQ3Nf0YtaLcF3n5QRIqWh8
+yfFC82x/xXp8HVGIutIKPidd3i1RTtMTZGnkLuPT55sJmabglZvOGtd/vjzOUrMR
+FcEPF80Du5wlFbqidon8BvEY0JNLDnyCt6X09l/+7UCmnYR0ObncHoUW2ikbhiMA
+ybuJfm6AiB4vFLQDJKgybwOaRywwvlbGp0ICcBvqQNi6BQNwB6SW//1IMwrh3KWB
+kJtN3X3n57LNXMhqlfil9o3EXXgIvnsG1knPGTZQIy4I5p4FTUcY1Rbpsda2ENW7
+l7+ijrRU
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJTRTEU
+MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3
+b3JrMSMwIQYDVQQDExpBZGRUcnVzdCBRdWFsaWZpZWQgQ0EgUm9vdDAeFw0wMDA1
+MzAxMDQ0NTBaFw0yMDA1MzAxMDQ0NTBaMGcxCzAJBgNVBAYTAlNFMRQwEgYDVQQK
+EwtBZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5ldHdvcmsxIzAh
+BgNVBAMTGkFkZFRydXN0IFF1YWxpZmllZCBDQSBSb290MIIBIjANBgkqhkiG9w0B
+AQEFAAOCAQ8AMIIBCgKCAQEA5B6a/twJWoekn0e+EV+vhDTbYjx5eLfpMLXsDBwq
+xBb/4Oxx64r1EW7tTw2R0hIYLUkVAcKkIhPHEWT/IhKauY5cLwjPcWqzZwFZ8V1G
+87B4pfYOQnrjfxvM0PC3KP0q6p6zsLkEqv32x7SxuCqg+1jxGaBvcCV+PmlKfw8i
+2O+tCBGaKZnhqkRFmhJePp1tUvznoD1oL/BLcHwTOK28FSXx1s6rosAx1i+f4P8U
+WfyEk9mHfExUE+uf0S0R+Bg6Ot4l2ffTQO2kBhLEO+GRwVY18BTcZTYJbqukB8c1
+0cIDMzZbdSZtQvESa0NvS3GU+jQd7RNuyoB/mC9suWXY6QIDAQABo4HUMIHRMB0G
+A1UdDgQWBBQ5lYtii1zJ1IC6WA+XPxUIQ8yYpzALBgNVHQ8EBAMCAQYwDwYDVR0T
+AQH/BAUwAwEB/zCBkQYDVR0jBIGJMIGGgBQ5lYtii1zJ1IC6WA+XPxUIQ8yYp6Fr
+pGkwZzELMAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQL
+ExRBZGRUcnVzdCBUVFAgTmV0d29yazEjMCEGA1UEAxMaQWRkVHJ1c3QgUXVhbGlm
+aWVkIENBIFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBABmrder4i2VhlRO6aQTv
+hsoToMeqT2QbPxj2qC0sVY8FtzDqQmodwCVRLae/DLPt7wh/bDxGGuoYQ992zPlm
+hpwsaPXpF/gxsxjE1kh9I0xowX67ARRvxdlu3rsEQmr49lx95dr6h+sNNVJn0J6X
+dgWTP5XHAeZpVTh/EGGZyeNfpso+gmNIquIISD6q8rKFYqa0p9m9N5xotS1WfbC3
+P6CxB9bpT9zeRXEwMn8bLgn5v1Kh7sKAPgZcLlVAwRv1cEWw3F369nJad9Jjzc9Y
+iQBCYz95OdBEsIJuQRno3eDBiFrRHnGTHyQwdOUeqN48Jzd/g66ed8/wMLH/S5no
+xqE=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC
+VVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ
+cmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ
+BgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt
+VHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D
+0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9
+ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G
+A1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G
+A1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs
+aobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I
+flc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL
+BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc
+BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00
+MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM
+aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG
+SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf
+qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW
+n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym
+c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+
+O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1
+o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j
+IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq
+IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz
+8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh
+vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l
+7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG
+cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
+BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD
+ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66
+AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC
+roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga
+W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n
+lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE
++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV
+csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd
+dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg
+KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM
+HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4
+WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF
+MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD
+bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw
+NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV
+BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn
+ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0
+3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z
+qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR
+p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8
+HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw
+ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea
+HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw
+Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh
+c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E
+RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt
+dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku
+Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp
+3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05
+nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF
+CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na
+xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX
+KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML
+RW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp
+bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5
+IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENlcnRp
+ZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQxNzUwNTFaFw0yOTA3
+MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3
+LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp
+YWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG
+A1UEAxMqRW50cnVzdC5uZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgp
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU1LqRKGsuqjIAcVFmQq
+K0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOLGp18EzoOH1u3Hs/lJBQe
+sYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSrhRSGlVuX
+MlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVT
+XTzWnLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/
+HoZdenoVve8AjhUiVBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH
+4QIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV
+HQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJKoZIhvcNAQEFBQADggEBADub
+j1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPyT/4xmf3IDExo
+U8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf
+zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5b
+u/8j72gZyxKTJ1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+
+bYQLCIt+jerXmCHG8+c8eS9enNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/Er
+fF6adulZkMV8gzURZVE=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDojCCAoqgAwIBAgIQE4Y1TR0/BvLB+WUF1ZAcYjANBgkqhkiG9w0BAQUFADBr
+MQswCQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRl
+cm5hdGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNv
+bW1lcmNlIFJvb3QwHhcNMDIwNjI2MDIxODM2WhcNMjIwNjI0MDAxNjEyWjBrMQsw
+CQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRlcm5h
+dGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNvbW1l
+cmNlIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvV95WHm6h
+2mCxlCfLF9sHP4CFT8icttD0b0/Pmdjh28JIXDqsOTPHH2qLJj0rNfVIsZHBAk4E
+lpF7sDPwsRROEW+1QK8bRaVK7362rPKgH1g/EkZgPI2h4H3PVz4zHvtH8aoVlwdV
+ZqW1LS7YgFmypw23RuwhY/81q6UCzyr0TP579ZRdhE2o8mCP2w4lPJ9zcc+U30rq
+299yOIzzlr3xF7zSujtFWsan9sYXiwGd/BmoKoMWuDpI/k4+oKsGGelT84ATB+0t
+vz8KPFUgOSwsAGl0lUq8ILKpeeUYiZGo3BxN77t+Nwtd/jmliFKMAGzsGHxBvfaL
+dXe6YJ2E5/4tAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD
+AgEGMB0GA1UdDgQWBBQVOIMPPyw/cDMezUb+B4wg4NfDtzANBgkqhkiG9w0BAQUF
+AAOCAQEAX/FBfXxcCLkr4NWSR/pnXKUTwwMhmytMiUbPWU3J/qVAtmPN3XEolWcR
+zCSs00Rsca4BIGsDoo8Ytyk6feUWYFN4PMCvFYP3j1IzJL1kk5fui/fbGKhtcbP3
+LBfQdCVp9/5rPJS+TUtBjE7ic9DjkCJzQ83z7+pzzkWKsKZJ/0x9nXGIxHYdkFsd
+7v3M9+79YKWxehZx0RbQfBI8bGmX265fOZpwLwU8GUYEmSA20GBuYQa7FkKMcPcw
+++DbZqMAAb3mLNqRX6BGi01qnD093QVG/na/oAo85ADmJ7f/hC3euiInlhBx6yLt
+398znM/jra6O1I7mT1GvFpLgXPYHDw==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx
+EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT
+EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp
+ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz
+NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH
+EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE
+AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw
+DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD
+E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH
+/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy
+DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh
+GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR
+tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA
+AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE
+FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX
+WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu
+9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr
+gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo
+2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO
+LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI
+4uJEvlz36hz1
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb
+MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow
+GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj
+YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL
+MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
+BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM
+GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP
+ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua
+BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe
+3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4
+YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR
+rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm
+ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU
+oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF
+MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v
+QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t
+b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF
+AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q
+GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz
+Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2
+G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi
+l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3
+smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx
+KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd
+BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl
+YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1
+OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy
+aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50
+ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G
+CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd
+AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC
+FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi
+1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq
+jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ
+wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj
+QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/
+WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy
+NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC
+uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw
+IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6
+g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN
+9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP
+BSeOE6Fuwg==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG
+A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv
+b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw
+MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i
+YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT
+aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ
+jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp
+xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp
+1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG
+snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ
+U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8
+9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E
+BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B
+AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz
+yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE
+38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP
+AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad
+DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME
+HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4
+MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6
+ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD
+VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j
+b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq
+scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO
+xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H
+LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX
+uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD
+yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+
+JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q
+rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN
+BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L
+hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB
+QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+
+HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu
+Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg
+QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB
+BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx
+MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
+AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA
+A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb
+laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56
+awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo
+JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw
+LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT
+VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk
+LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb
+UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/
+QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+
+naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls
+QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIF4DCCA8igAwIBAgIRAPL6ZOJ0Y9ON/RAdBB92ylgwDQYJKoZIhvcNAQELBQAw
+ZzELMAkGA1UEBhMCY2gxETAPBgNVBAoTCFN3aXNzY29tMSUwIwYDVQQLExxEaWdp
+dGFsIENlcnRpZmljYXRlIFNlcnZpY2VzMR4wHAYDVQQDExVTd2lzc2NvbSBSb290
+IEVWIENBIDIwHhcNMTEwNjI0MDk0NTA4WhcNMzEwNjI1MDg0NTA4WjBnMQswCQYD
+VQQGEwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0YWwgQ2Vy
+dGlmaWNhdGUgU2VydmljZXMxHjAcBgNVBAMTFVN3aXNzY29tIFJvb3QgRVYgQ0Eg
+MjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMT3HS9X6lds93BdY7Bx
+UglgRCgzo3pOCvrY6myLURYaVa5UJsTMRQdBTxB5f3HSek4/OE6zAMaVylvNwSqD
+1ycfMQ4jFrclyxy0uYAyXhqdk/HoPGAsp15XGVhRXrwsVgu42O+LgrQ8uMIkqBPH
+oCE2G3pXKSinLr9xJZDzRINpUKTk4RtiGZQJo/PDvO/0vezbE53PnUgJUmfANykR
+HvvSEaeFGHR55E+FFOtSN+KxRdjMDUN/rhPSays/p8LiqG12W0OfvrSdsyaGOx9/
+5fLoZigWJdBLlzin5M8J0TbDC77aO0RYjb7xnglrPvMyxyuHxuxenPaHZa0zKcQv
+idm5y8kDnftslFGXEBuGCxobP/YCfnvUxVFkKJ3106yDgYjTdLRZncHrYTNaRdHL
+OdAGalNgHa/2+2m8atwBz735j9m9W8E6X47aD0upm50qKGsaCnw8qyIL5XctcfaC
+NYGu+HuB5ur+rPQam3Rc6I8k9l2dRsQs0h4rIWqDJ2dVSqTjyDKXZpBy2uPUZC5f
+46Fq9mDU5zXNysRojddxyNMkM3OxbPlq4SjbX8Y96L5V5jcb7STZDxmPX2MYWFCB
+UWVv8p9+agTnNCRxunZLWB4ZvRVgRaoMEkABnRDixzgHcgplwLa7JSnaFp6LNYth
+7eVxV4O1PHGf40+/fh6Bn0GXAgMBAAGjgYYwgYMwDgYDVR0PAQH/BAQDAgGGMB0G
+A1UdIQQWMBQwEgYHYIV0AVMCAgYHYIV0AVMCAjASBgNVHRMBAf8ECDAGAQH/AgED
+MB0GA1UdDgQWBBRF2aWBbj2ITY1x0kbBbkUe88SAnTAfBgNVHSMEGDAWgBRF2aWB
+bj2ITY1x0kbBbkUe88SAnTANBgkqhkiG9w0BAQsFAAOCAgEAlDpzBp9SSzBc1P6x
+XCX5145v9Ydkn+0UjrgEjihLj6p7jjm02Vj2e6E1CqGdivdj5eu9OYLU43otb98T
+PLr+flaYC/NUn81ETm484T4VvwYmneTwkLbUwp4wLh/vx3rEUMfqe9pQy3omywC0
+Wqu1kx+AiYQElY2NfwmTv9SoqORjbdlk5LgpWgi/UOGED1V7XwgiG/W9mR4U9s70
+WBCCswo9GcG/W6uqmdjyMb3lOGbcWAXH7WMaLgqXfIeTK7KK4/HsGOV1timH59yL
+Gn602MnTihdsfSlEvoqq9X46Lmgxk7lq2prg2+kupYTNHAq4Sgj5nPFhJpiTt3tm
+7JFe3VE/23MPrQRYCd0EApUKPtN236YQHoA96M2kZNEzx5LH4k5E4wnJTsJdhw4S
+nr8PyQUQ3nqjsTzyP6WqJ3mtMX0f/fwZacXduT98zca0wjAefm6S139hdlqP65VN
+vBFuIXxZN5nQBrz5Bm0yFqXZaajh3DyAHmBR3NdUIR7KYndP+tiPsys6DXhyyWhB
+WkdKwqPrGtcKqzwyVcgKEZzfdNbwQBUdyLmPtTbFr/giuMod89a2GQ+fYWVq6nTI
+fI/DT11lgh/ZDYnadXL77/FHZxOzyNEZiCcmmpl5fx7kLD977vHeTYuWl8PVP3wb
+I+2ksx0WckNLIOFZfsLorSa/ovc=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK
+MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x
+GTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx
+MjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg
+Q29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG
+SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ
+iQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa
+/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ
+jnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI
+HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7
+sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w
+gZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF
+MAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw
+KaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG
+AQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L
+URYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO
+H0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm
+I50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY
+iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc
+f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd
+MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg
+Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow
+TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw
+HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB
+BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y
+ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E
+N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9
+tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX
+0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c
+/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X
+KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY
+zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS
+O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D
+34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP
+K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3
+AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv
+Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj
+QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV
+cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS
+IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2
+HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa
+O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv
+033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u
+dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE
+kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41
+3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD
+u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq
+4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE
+BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz
+dCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL
+MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp
+cm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y
+YJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua
+kCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL
+QESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp
+6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG
+yH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i
+QLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ
+KoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO
+tDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu
+QY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ
+Lgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u
+olu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48
+x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEY
+MBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21t
+dW5pY2F0aW9uIFJvb3RDQTEwHhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5
+WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYD
+VQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEwggEiMA0GCSqGSIb3
+DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw8yl8
+9f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJ
+DKaVv0uMDPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9
+Ms+k2Y7CI9eNqPPYJayX5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/N
+QV3Is00qVUarH9oe4kA92819uZKAnDfdDJZkndwi92SL32HeFZRSFaB9UslLqCHJ
+xrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2JChzAgMBAAGjPzA9MB0G
+A1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYwDwYDVR0T
+AQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vG
+kl3g0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfr
+Uj94nK9NrvjVT8+amCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5
+Bw+SUEmK3TGXX8npN6o7WWWXlDLJs58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJU
+JRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ6rBK+1YWc26sTfcioU+tHXot
+RSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAiFL39vmwLAw==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x
+GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv
+b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV
+BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W
+YWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa
+GMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg
+Fyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J
+WpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB
+rrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp
++ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1
+ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i
+Ucw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz
+PtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og
+/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH
+oycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI
+yV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud
+EwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2
+A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL
+MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT
+ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f
+BluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn
+g/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl
+fF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K
+WWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha
+B0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc
+hLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR
+TUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD
+mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z
+ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y
+4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza
+8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIID/jCCAuagAwIBAgIQFaxulBmyeUtB9iepwxgPHzANBgkqhkiG9w0BAQsFADCB
+mDELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsT
+MChjKSAyMDA4IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s
+eTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhv
+cml0eSAtIEczMB4XDTA4MDQwMjAwMDAwMFoXDTM3MTIwMTIzNTk1OVowgZgxCzAJ
+BgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykg
+MjAwOCBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0
+BgNVBAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg
+LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANziXmJYHTNXOTIz
++uvLh4yn1ErdBojqZI4xmKU4kB6Yzy5jK/BGvESyiaHAKAxJcCGVn2TAppMSAmUm
+hsalifD614SgcK9PGpc/BkTVyetyEH3kMSj7HGHmKAdEc5IiaacDiGydY8hS2pgn
+5whMcD60yRLBxWeDXTPzAxHsatBT4tG6NmCUgLthY2xbF37fQJQeqw3CIShwiP/W
+JmxsYAQlTlV+fe+/lEjetx3dcI0FX4ilm/LC7urRQEFtYjgdVgbFA0dRIBn8exAL
+DmKudlW/X3e+PkkBUz2YJQN2JFodtNuJ6nnltrM7P7pMKEF/BqxqjsHQ9gUdfeZC
+huOl1UcCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw
+HQYDVR0OBBYEFMR5yo6hTgMdHNxr2zFblD4/MH8tMA0GCSqGSIb3DQEBCwUAA4IB
+AQAtxRPPVoB7eni9n64smefv2t+UXglpp+duaIy9cr5HqQ6XErhK8WTTOd8lNNTB
+zU6B8A8ExCSzNJbGpqow32hhc9f5joWJ7w5elShKKiePEI4ufIbEAp7aDHdlDkQN
+kv39sxY2+hENHYwOB4lqKVb3cvTdFZx3NWZXqxNT2I7BQMXXExZacse3aQHEerGD
+AWh9jUGhlBjBJVz88P6DAod8DQ3PLghcSkANPuyBYeYk28rgDi0Hsj5W3I31QYUH
+SJsMC8tJP33st/3LjWeJGqvtux6jAAgIFyqCXDFdRootD4abdNlF+9RAsXqqaC2G
+spki4cErx5z481+oghLrGREt
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFWDCCA0CgAwIBAgIQUHBrzdgT/BtOOzNy0hFIjTANBgkqhkiG9w0BAQsFADBG
+MQswCQYDVQQGEwJDTjEaMBgGA1UEChMRV29TaWduIENBIExpbWl0ZWQxGzAZBgNV
+BAMMEkNBIOayg+mAmuagueivgeS5pjAeFw0wOTA4MDgwMTAwMDFaFw0zOTA4MDgw
+MTAwMDFaMEYxCzAJBgNVBAYTAkNOMRowGAYDVQQKExFXb1NpZ24gQ0EgTGltaXRl
+ZDEbMBkGA1UEAwwSQ0Eg5rKD6YCa5qC56K+B5LmmMIICIjANBgkqhkiG9w0BAQEF
+AAOCAg8AMIICCgKCAgEA0EkhHiX8h8EqwqzbdoYGTufQdDTc7WU1/FDWiD+k8H/r
+D195L4mx/bxjWDeTmzj4t1up+thxx7S8gJeNbEvxUNUqKaqoGXqW5pWOdO2XCld1
+9AXbbQs5uQF/qvbW2mzmBeCkTVL829B0txGMe41P/4eDrv8FAxNXUDf+jJZSEExf
+v5RxadmWPgxDT74wwJ85dE8GRV2j1lY5aAfMh09Qd5Nx2UQIsYo06Yms25tO4dnk
+UkWMLhQfkWsZHWgpLFbE4h4TV2TwYeO5Ed+w4VegG63XX9Gv2ystP9Bojg/qnw+L
+NVgbExz03jWhCl3W6t8Sb8D7aQdGctyB9gQjF+BNdeFyb7Ao65vh4YOhn0pdr8yb
++gIgthhid5E7o9Vlrdx8kHccREGkSovrlXLp9glk3Kgtn3R46MGiCWOc76DbT52V
+qyBPt7D3h1ymoOQ3OMdc4zUPLK2jgKLsLl3Az+2LBcLmc272idX10kaO6m1jGx6K
+yX2m+Jzr5dVjhU1zZmkR/sgO9MHHZklTfuQZa/HpelmjbX7FF+Ynxu8b22/8DU0G
+AbQOXDBGVWCvOGU6yke6rCzMRh+yRpY/8+0mBe53oWprfi1tWFxK1I5nuPHa1UaK
+J/kR8slC/k7e3x9cxKSGhxYzoacXGKUN5AXlK8IrC6KVkLn9YDxOiT7nnO4fuwEC
+AwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O
+BBYEFOBNv9ybQV0T6GTwp+kVpOGBwboxMA0GCSqGSIb3DQEBCwUAA4ICAQBqinA4
+WbbaixjIvirTthnVZil6Xc1bL3McJk6jfW+rtylNpumlEYOnOXOvEESS5iVdT2H6
+yAa+Tkvv/vMx/sZ8cApBWNromUuWyXi8mHwCKe0JgOYKOoICKuLJL8hWGSbueBwj
+/feTZU7n85iYr83d2Z5AiDEoOqsuC7CsDCT6eiaY8xJhEPRdF/d+4niXVOKM6Cm6
+jBAyvd0zaziGfjk9DgNyp115j0WKWa5bIW4xRtVZjc8VX90xJc/bYNaBRHIpAlf2
+ltTW/+op2znFuCyKGo3Oy+dCMYYFaA6eFN0AkLppRQjbbpCBhqcqBT/mhDn4t/lX
+X0ykeVoQDF7Va/81XwVRHmyjdanPUIPTfPRm94KNPQx96N97qA4bLJyuQHCH2u2n
+FoJavjVsIE4iYdm8UXrNemHcSxH5/mc0zy4EZmFcV5cjjPOGG0jfKq+nwf/Yjj4D
+u9gqsPoUJbJRa4ZDhS4HIxaAjUz7tGM7zMN07RujHv41D198HRaG9Q7DlfEvr10l
+O1Hm13ZBONFLAzkopR6RctR9q5czxNM+4Gm2KHmgCY0c0f9BckgG/Jou5yD5m6Le
+ie2uPAmvylezkolwQOQvT8Jwg0DXJCxr5wkf09XHwQj02w47HAcLQxGEIYbpgNR1
+2KvxAmLBsX5VYc8T1yaw15zLKYs4SgsOkI26oQ==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDMDCCAhigAwIBAgICA+gwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCSEsx
+FjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3Qg
+Um9vdCBDQSAxMB4XDTAzMDUxNTA1MTMxNFoXDTIzMDUxNTA0NTIyOVowRzELMAkG
+A1UEBhMCSEsxFjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdr
+b25nIFBvc3QgUm9vdCBDQSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEArP84tulmAknjorThkPlAj3n54r15/gK97iSSHSL22oVyaf7XPwnU3ZG1ApzQ
+jVrhVcNQhrkpJsLj2aDxaQMoIIBFIi1WpztUlVYiWR8o3x8gPW2iNr4joLFutbEn
+PzlTCeqrauh0ssJlXI6/fMN4hM2eFvz1Lk8gKgifd/PFHsSaUmYeSF7jEAaPIpjh
+ZY4bXSNmO7ilMlHIhqqhqZ5/dpTCpmy3QfDVyAY45tQM4vM7TG1QjMSDJ8EThFk9
+nnV0ttgCXjqQesBCNnLsak3c78QA3xMYV18meMjWCnl3v/evt3a5pQuEF10Q6m/h
+q5URX208o1xNg1vysxmKgIsLhwIDAQABoyYwJDASBgNVHRMBAf8ECDAGAQH/AgED
+MA4GA1UdDwEB/wQEAwIBxjANBgkqhkiG9w0BAQUFAAOCAQEADkbVPK7ih9legYsC
+mEEIjEy82tvuJxuC52pF7BaLT4Wg87JwvVqWuspube5Gi27nKi6Wsxkz67SfqLI3
+7piol7Yutmcn1KZJ/RyTZXaeQi/cImyaT/JaFTmxcdcrUehtHJjA2Sr0oYJ71clB
+oiMBdDhViw+5LmeiIAQ32pwL0xch4I+XeTRvhEgCIDMb5jREn5Fw9IBehEPCKdJs
+EhTkYY2sEJCehFC78JZvRZ+K88psT/oROhUVRsPNH4NbLUES7VBnQRM9IauUiqpO
+fMGx+6fWtScvl6tu4B3i0RwsH0Ti/L6RoZz71ilTc4afU9hDDl3WY4JxHYB0yvbi
+AmvZWg==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG
+EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3
+MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl
+cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR
+dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB
+pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM
+b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm
+aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz
+IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT
+lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz
+AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5
+VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG
+ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2
+BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG
+AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M
+U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh
+bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C
++C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC
+bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F
+uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2
+XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIGizCCBXOgAwIBAgIEO0XlaDANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJF
+UzEfMB0GA1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UECxMGUEtJ
+R1ZBMScwJQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmEwHhcN
+MDEwNzA2MTYyMjQ3WhcNMjEwNzAxMTUyMjQ3WjBoMQswCQYDVQQGEwJFUzEfMB0G
+A1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UECxMGUEtJR1ZBMScw
+JQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmEwggEiMA0GCSqG
+SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGKqtXETcvIorKA3Qdyu0togu8M1JAJke+
+WmmmO3I2F0zo37i7L3bhQEZ0ZQKQUgi0/6iMweDHiVYQOTPvaLRfX9ptI6GJXiKj
+SgbwJ/BXufjpTjJ3Cj9BZPPrZe52/lSqfR0grvPXdMIKX/UIKFIIzFVd0g/bmoGl
+u6GzwZTNVOAydTGRGmKy3nXiz0+J2ZGQD0EbtFpKd71ng+CT516nDOeB0/RSrFOy
+A8dEJvt55cs0YFAQexvba9dHq198aMpunUEDEO5rmXteJajCq+TA81yc477OMUxk
+Hl6AovWDfgzWyoxVjr7gvkkHD6MkQXpYHYTqWBLI4bft75PelAgxAgMBAAGjggM7
+MIIDNzAyBggrBgEFBQcBAQQmMCQwIgYIKwYBBQUHMAGGFmh0dHA6Ly9vY3NwLnBr
+aS5ndmEuZXMwEgYDVR0TAQH/BAgwBgEB/wIBAjCCAjQGA1UdIASCAiswggInMIIC
+IwYKKwYBBAG/VQIBADCCAhMwggHoBggrBgEFBQcCAjCCAdoeggHWAEEAdQB0AG8A
+cgBpAGQAYQBkACAAZABlACAAQwBlAHIAdABpAGYAaQBjAGEAYwBpAPMAbgAgAFIA
+YQDtAHoAIABkAGUAIABsAGEAIABHAGUAbgBlAHIAYQBsAGkAdABhAHQAIABWAGEA
+bABlAG4AYwBpAGEAbgBhAC4ADQAKAEwAYQAgAEQAZQBjAGwAYQByAGEAYwBpAPMA
+bgAgAGQAZQAgAFAAcgDhAGMAdABpAGMAYQBzACAAZABlACAAQwBlAHIAdABpAGYA
+aQBjAGEAYwBpAPMAbgAgAHEAdQBlACAAcgBpAGcAZQAgAGUAbAAgAGYAdQBuAGMA
+aQBvAG4AYQBtAGkAZQBuAHQAbwAgAGQAZQAgAGwAYQAgAHAAcgBlAHMAZQBuAHQA
+ZQAgAEEAdQB0AG8AcgBpAGQAYQBkACAAZABlACAAQwBlAHIAdABpAGYAaQBjAGEA
+YwBpAPMAbgAgAHMAZQAgAGUAbgBjAHUAZQBuAHQAcgBhACAAZQBuACAAbABhACAA
+ZABpAHIAZQBjAGMAaQDzAG4AIAB3AGUAYgAgAGgAdAB0AHAAOgAvAC8AdwB3AHcA
+LgBwAGsAaQAuAGcAdgBhAC4AZQBzAC8AYwBwAHMwJQYIKwYBBQUHAgEWGWh0dHA6
+Ly93d3cucGtpLmd2YS5lcy9jcHMwHQYDVR0OBBYEFHs100DSHHgZZu90ECjcPk+y
+eAT8MIGVBgNVHSMEgY0wgYqAFHs100DSHHgZZu90ECjcPk+yeAT8oWykajBoMQsw
+CQYDVQQGEwJFUzEfMB0GA1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0G
+A1UECxMGUEtJR1ZBMScwJQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVu
+Y2lhbmGCBDtF5WgwDQYJKoZIhvcNAQEFBQADggEBACRhTvW1yEICKrNcda3Fbcrn
+lD+laJWIwVTAEGmiEi8YPyVQqHxK6sYJ2fR1xkDar1CdPaUWu20xxsdzCkj+IHLt
+b8zog2EWRpABlUt9jppSCS/2bxzkoXHPjCpaF3ODR00PNvsETUlR4hTJZGH71BTg
+9J63NI8KJr2XXPR5OkowGcytT6CYirQxlyric21+eLj4iIlPsSKRZEv1UN4D2+XF
+ducTZnV+ZfsBn5OHiJ35Rld8TWCvmHMTI6QgkYH60GFmuH3Rr9ZvHmw96RH9qfmC
+IoaZM3Fa6hlXPZHNqcCjbgcTpsnt+GijnsNacgmHKNHEc8RzGF9QdRYxn7fofMM=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIGfTCCBWWgAwIBAgICAQMwDQYJKoZIhvcNAQEEBQAwga8xCzAJBgNVBAYTAkhV
+MRAwDgYDVQQIEwdIdW5nYXJ5MREwDwYDVQQHEwhCdWRhcGVzdDEnMCUGA1UEChMe
+TmV0TG9jayBIYWxvemF0Yml6dG9uc2FnaSBLZnQuMRowGAYDVQQLExFUYW51c2l0
+dmFueWtpYWRvazE2MDQGA1UEAxMtTmV0TG9jayBLb3pqZWd5em9pIChDbGFzcyBB
+KSBUYW51c2l0dmFueWtpYWRvMB4XDTk5MDIyNDIzMTQ0N1oXDTE5MDIxOTIzMTQ0
+N1owga8xCzAJBgNVBAYTAkhVMRAwDgYDVQQIEwdIdW5nYXJ5MREwDwYDVQQHEwhC
+dWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6dG9uc2FnaSBLZnQu
+MRowGAYDVQQLExFUYW51c2l0dmFueWtpYWRvazE2MDQGA1UEAxMtTmV0TG9jayBL
+b3pqZWd5em9pIChDbGFzcyBBKSBUYW51c2l0dmFueWtpYWRvMIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvHSMD7tM9DceqQWC2ObhbHDqeLVu0ThEDaiD
+zl3S1tWBxdRL51uUcCbbO51qTGL3cfNk1mE7PetzozfZz+qMkjvN9wfcZnSX9EUi
+3fRc4L9t875lM+QVOr/bmJBVOMTtplVjC7B4BPTjbsE/jvxReB+SnoPC/tmwqcm8
+WgD/qaiYdPv2LD4VOQ22BFWoDpggQrOxJa1+mm9dU7GrDPzr4PN6s6iz/0b2Y6LY
+Oph7tqyF/7AlT3Rj5xMHpQqPBffAZG9+pyeAlt7ULoZgx2srXnN7F+eRP2QM2Esi
+NCubMvJIH5+hCoR64sKtlz2O1cH5VqNQ6ca0+pii7pXmKgOM3wIDAQABo4ICnzCC
+ApswDgYDVR0PAQH/BAQDAgAGMBIGA1UdEwEB/wQIMAYBAf8CAQQwEQYJYIZIAYb4
+QgEBBAQDAgAHMIICYAYJYIZIAYb4QgENBIICURaCAk1GSUdZRUxFTSEgRXplbiB0
+YW51c2l0dmFueSBhIE5ldExvY2sgS2Z0LiBBbHRhbGFub3MgU3pvbGdhbHRhdGFz
+aSBGZWx0ZXRlbGVpYmVuIGxlaXJ0IGVsamFyYXNvayBhbGFwamFuIGtlc3p1bHQu
+IEEgaGl0ZWxlc2l0ZXMgZm9seWFtYXRhdCBhIE5ldExvY2sgS2Z0LiB0ZXJtZWtm
+ZWxlbG9zc2VnLWJpenRvc2l0YXNhIHZlZGkuIEEgZGlnaXRhbGlzIGFsYWlyYXMg
+ZWxmb2dhZGFzYW5hayBmZWx0ZXRlbGUgYXogZWxvaXJ0IGVsbGVub3J6ZXNpIGVs
+amFyYXMgbWVndGV0ZWxlLiBBeiBlbGphcmFzIGxlaXJhc2EgbWVndGFsYWxoYXRv
+IGEgTmV0TG9jayBLZnQuIEludGVybmV0IGhvbmxhcGphbiBhIGh0dHBzOi8vd3d3
+Lm5ldGxvY2submV0L2RvY3MgY2ltZW4gdmFneSBrZXJoZXRvIGF6IGVsbGVub3J6
+ZXNAbmV0bG9jay5uZXQgZS1tYWlsIGNpbWVuLiBJTVBPUlRBTlQhIFRoZSBpc3N1
+YW5jZSBhbmQgdGhlIHVzZSBvZiB0aGlzIGNlcnRpZmljYXRlIGlzIHN1YmplY3Qg
+dG8gdGhlIE5ldExvY2sgQ1BTIGF2YWlsYWJsZSBhdCBodHRwczovL3d3dy5uZXRs
+b2NrLm5ldC9kb2NzIG9yIGJ5IGUtbWFpbCBhdCBjcHNAbmV0bG9jay5uZXQuMA0G
+CSqGSIb3DQEBBAUAA4IBAQBIJEb3ulZv+sgoA0BO5TE5ayZrU3/b39/zcT0mwBQO
+xmd7I6gMc90Bu8bKbjc5VdXHjFYgDigKDtIqpLBJUsY4B/6+CgmM0ZjPytoUMaFP
+0jn8DxEsQ8Pdq5PHVT5HfBgaANzze9jyf1JsIPQLX2lS9O74silg6+NJMSEN1rUQ
+QeJBCWziGppWS3cC9qCbmieH6FUpccKQn0V4GuEVZD3QDtigdp+uxdAu6tYPVuxk
+f1qbFFgBJ34TUMdrKuZoPL9coAob4Q566eKAw+np9v1sEZ7Q5SgnK1QyQhSCdeZK
+8CtmdWOMovsEPoMOmzbwGOQmIMOM8CgHrTwXZoi1/baI
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIJhjCCB26gAwIBAgIBCzANBgkqhkiG9w0BAQsFADCCAR4xPjA8BgNVBAMTNUF1
+dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIFJhaXogZGVsIEVzdGFkbyBWZW5lem9s
+YW5vMQswCQYDVQQGEwJWRTEQMA4GA1UEBxMHQ2FyYWNhczEZMBcGA1UECBMQRGlz
+dHJpdG8gQ2FwaXRhbDE2MDQGA1UEChMtU2lzdGVtYSBOYWNpb25hbCBkZSBDZXJ0
+aWZpY2FjaW9uIEVsZWN0cm9uaWNhMUMwQQYDVQQLEzpTdXBlcmludGVuZGVuY2lh
+IGRlIFNlcnZpY2lvcyBkZSBDZXJ0aWZpY2FjaW9uIEVsZWN0cm9uaWNhMSUwIwYJ
+KoZIhvcNAQkBFhZhY3JhaXpAc3VzY2VydGUuZ29iLnZlMB4XDTEwMTIyODE2NTEw
+MFoXDTIwMTIyNTIzNTk1OVowgdExJjAkBgkqhkiG9w0BCQEWF2NvbnRhY3RvQHBy
+b2NlcnQubmV0LnZlMQ8wDQYDVQQHEwZDaGFjYW8xEDAOBgNVBAgTB01pcmFuZGEx
+KjAoBgNVBAsTIVByb3ZlZWRvciBkZSBDZXJ0aWZpY2Fkb3MgUFJPQ0VSVDE2MDQG
+A1UEChMtU2lzdGVtYSBOYWNpb25hbCBkZSBDZXJ0aWZpY2FjaW9uIEVsZWN0cm9u
+aWNhMQswCQYDVQQGEwJWRTETMBEGA1UEAxMKUFNDUHJvY2VydDCCAiIwDQYJKoZI
+hvcNAQEBBQADggIPADCCAgoCggIBANW39KOUM6FGqVVhSQ2oh3NekS1wwQYalNo9
+7BVCwfWMrmoX8Yqt/ICV6oNEolt6Vc5Pp6XVurgfoCfAUFM+jbnADrgV3NZs+J74
+BCXfgI8Qhd19L3uA3VcAZCP4bsm+lU/hdezgfl6VzbHvvnpC2Mks0+saGiKLt38G
+ieU89RLAu9MLmV+QfI4tL3czkkohRqipCKzx9hEC2ZUWno0vluYC3XXCFCpa1sl9
+JcLB/KpnheLsvtF8PPqv1W7/U0HU9TI4seJfxPmOEO8GqQKJ/+MMbpfg353bIdD0
+PghpbNjU5Db4g7ayNo+c7zo3Fn2/omnXO1ty0K+qP1xmk6wKImG20qCZyFSTXai2
+0b1dCl53lKItwIKOvMoDKjSuc/HUtQy9vmebVOvh+qBa7Dh+PsHMosdEMXXqP+UH
+0quhJZb25uSgXTcYOWEAM11G1ADEtMo88aKjPvM6/2kwLkDd9p+cJsmWN63nOaK/
+6mnbVSKVUyqUtd+tFjiBdWbjxywbk5yqjKPK2Ww8F22c3HxT4CAnQzb5EuE8XL1m
+v6JpIzi4mWCZDlZTOpx+FIywBm/xhnaQr/2v/pDGj59/i5IjnOcVdo/Vi5QTcmn7
+K2FjiO/mpF7moxdqWEfLcU8UC17IAggmosvpr2uKGcfLFFb14dq12fy/czja+eev
+bqQ34gcnAgMBAAGjggMXMIIDEzASBgNVHRMBAf8ECDAGAQH/AgEBMDcGA1UdEgQw
+MC6CD3N1c2NlcnRlLmdvYi52ZaAbBgVghl4CAqASDBBSSUYtRy0yMDAwNDAzNi0w
+MB0GA1UdDgQWBBRBDxk4qpl/Qguk1yeYVKIXTC1RVDCCAVAGA1UdIwSCAUcwggFD
+gBStuyIdxuDSAaj9dlBSk+2YwU2u06GCASakggEiMIIBHjE+MDwGA1UEAxM1QXV0
+b3JpZGFkIGRlIENlcnRpZmljYWNpb24gUmFpeiBkZWwgRXN0YWRvIFZlbmV6b2xh
+bm8xCzAJBgNVBAYTAlZFMRAwDgYDVQQHEwdDYXJhY2FzMRkwFwYDVQQIExBEaXN0
+cml0byBDYXBpdGFsMTYwNAYDVQQKEy1TaXN0ZW1hIE5hY2lvbmFsIGRlIENlcnRp
+ZmljYWNpb24gRWxlY3Ryb25pY2ExQzBBBgNVBAsTOlN1cGVyaW50ZW5kZW5jaWEg
+ZGUgU2VydmljaW9zIGRlIENlcnRpZmljYWNpb24gRWxlY3Ryb25pY2ExJTAjBgkq
+hkiG9w0BCQEWFmFjcmFpekBzdXNjZXJ0ZS5nb2IudmWCAQowDgYDVR0PAQH/BAQD
+AgEGME0GA1UdEQRGMESCDnByb2NlcnQubmV0LnZloBUGBWCGXgIBoAwMClBTQy0w
+MDAwMDKgGwYFYIZeAgKgEgwQUklGLUotMzE2MzUzNzMtNzB2BgNVHR8EbzBtMEag
+RKBChkBodHRwOi8vd3d3LnN1c2NlcnRlLmdvYi52ZS9sY3IvQ0VSVElGSUNBRE8t
+UkFJWi1TSEEzODRDUkxERVIuY3JsMCOgIaAfhh1sZGFwOi8vYWNyYWl6LnN1c2Nl
+cnRlLmdvYi52ZTA3BggrBgEFBQcBAQQrMCkwJwYIKwYBBQUHMAGGG2h0dHA6Ly9v
+Y3NwLnN1c2NlcnRlLmdvYi52ZTBBBgNVHSAEOjA4MDYGBmCGXgMBAjAsMCoGCCsG
+AQUFBwIBFh5odHRwOi8vd3d3LnN1c2NlcnRlLmdvYi52ZS9kcGMwDQYJKoZIhvcN
+AQELBQADggIBACtZ6yKZu4SqT96QxtGGcSOeSwORR3C7wJJg7ODU523G0+1ng3dS
+1fLld6c2suNUvtm7CpsR72H0xpkzmfWvADmNg7+mvTV+LFwxNG9s2/NkAZiqlCxB
+3RWGymspThbASfzXg0gTB1GEMVKIu4YXx2sviiCtxQuPcD4quxtxj7mkoP3Yldmv
+Wb8lK5jpY5MvYB7Eqvh39YtsL+1+LrVPQA3uvFd359m21D+VJzog1eWuq2w1n8Gh
+HVnchIHuTQfiSLaeS5UtQbHh6N5+LwUeaO6/u5BlOsju6rEYNxxik6SgMexxbJHm
+pHmJWhSnFFAFTKQAVzAswbVhltw+HoSvOULP5dAssSS830DD7X9jSr3hTxJkhpXz
+sOfIt+FTvZLm8wyWuevo5pLtp4EJFAv8lXrPj9Y0TzYS3F7RNHXGRoAvlQSMx4bE
+qCaJqD8Zm4G7UaRKhqsLEQ+xrmNTbSjq3TNWOByyrYDT13K9mmyZY+gAu0F2Bbdb
+mRiKw7gSXFbPVgx96OLP7bx0R/vu0xdOIk9W/1DzLuY5poLWccret9W6aAjtmcz9
+opLLabid+Qqkpj5PkygqYWwHJgD/ll9ohri4zspV4KuxPX+Y1zMOWj3YeMLEYC/H
+YvBhkdI4sPaeVdtAgAUSM84dkpvRabP/v/GSCmE1P93+hvS84Bpxs2Km
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIID3TCCAsWgAwIBAgIOHaIAAQAC7LdggHiNtgYwDQYJKoZIhvcNAQEFBQAweTEL
+MAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxJDAiBgNV
+BAsTG1RDIFRydXN0Q2VudGVyIFVuaXZlcnNhbCBDQTEmMCQGA1UEAxMdVEMgVHJ1
+c3RDZW50ZXIgVW5pdmVyc2FsIENBIEkwHhcNMDYwMzIyMTU1NDI4WhcNMjUxMjMx
+MjI1OTU5WjB5MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1c3RDZW50ZXIg
+R21iSDEkMCIGA1UECxMbVEMgVHJ1c3RDZW50ZXIgVW5pdmVyc2FsIENBMSYwJAYD
+VQQDEx1UQyBUcnVzdENlbnRlciBVbml2ZXJzYWwgQ0EgSTCCASIwDQYJKoZIhvcN
+AQEBBQADggEPADCCAQoCggEBAKR3I5ZEr5D0MacQ9CaHnPM42Q9e3s9B6DGtxnSR
+JJZ4Hgmgm5qVSkr1YnwCqMqs+1oEdjneX/H5s7/zA1hV0qq34wQi0fiU2iIIAI3T
+fCZdzHd55yx4Oagmcw6iXSVphU9VDprvxrlE4Vc93x9UIuVvZaozhDrzznq+VZeu
+jRIPFDPiUHDDSYcTvFHe15gSWu86gzOSBnWLknwSaHtwag+1m7Z3W0hZneTvWq3z
+wZ7U10VOylY0Ibw+F1tvdwxIAUMpsN0/lm7mlaoMwCC2/T42J5zjXM9OgdwZu5GQ
+fezmlwQek8wiSdeXhrYTCjxDI3d+8NzmzSQfO4ObNDqDNOMCAwEAAaNjMGEwHwYD
+VR0jBBgwFoAUkqR1LKSevoFE63n8isWVpesQdXMwDwYDVR0TAQH/BAUwAwEB/zAO
+BgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFJKkdSyknr6BROt5/IrFlaXrEHVzMA0G
+CSqGSIb3DQEBBQUAA4IBAQAo0uCG1eb4e/CX3CJrO5UUVg8RMKWaTzqwOuAGy2X1
+7caXJ/4l8lfmXpWMPmRgFVp/Lw0BxbFg/UU1z/CyvwbZ71q+s2IhtNerNXxTPqYn
+8aEt2hojnczd7Dwtnic0XQ/CNnm8yUpiLe1r2X1BQ3y2qsrtYbE3ghUJGooWMNjs
+ydZHcnhLEEYUjl8Or+zHL6sQ17bxbuyGssLoDZJz3KL0Dzq/YSMQiZxIQG5wALPT
+ujdEWBF6AmqI8Dc08BnprNRlc/ZpjGSUOnmFKbAWKwyCPwacx/0QK54PLLae4xW/
+2TYcuiUaUj0a7CIMHOCkoj3w6DnPgcB77V0fb8XQC9eY
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIF2TCCA8GgAwIBAgIQHp4o6Ejy5e/DfEoeWhhntjANBgkqhkiG9w0BAQsFADBk
+MQswCQYDVQQGEwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0
+YWwgQ2VydGlmaWNhdGUgU2VydmljZXMxGzAZBgNVBAMTElN3aXNzY29tIFJvb3Qg
+Q0EgMjAeFw0xMTA2MjQwODM4MTRaFw0zMTA2MjUwNzM4MTRaMGQxCzAJBgNVBAYT
+AmNoMREwDwYDVQQKEwhTd2lzc2NvbTElMCMGA1UECxMcRGlnaXRhbCBDZXJ0aWZp
+Y2F0ZSBTZXJ2aWNlczEbMBkGA1UEAxMSU3dpc3Njb20gUm9vdCBDQSAyMIICIjAN
+BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAlUJOhJ1R5tMJ6HJaI2nbeHCOFvEr
+jw0DzpPMLgAIe6szjPTpQOYXTKueuEcUMncy3SgM3hhLX3af+Dk7/E6J2HzFZ++r
+0rk0X2s682Q2zsKwzxNoysjL67XiPS4h3+os1OD5cJZM/2pYmLcX5BtS5X4HAB1f
+2uY+lQS3aYg5oUFgJWFLlTloYhyxCwWJwDaCFCE/rtuh/bxvHGCGtlOUSbkrRsVP
+ACu/obvLP+DHVxxX6NZp+MEkUp2IVd3Chy50I9AU/SpHWrumnf2U5NGKpV+GY3aF
+y6//SSj8gO1MedK75MDvAe5QQQg1I3ArqRa0jG6F6bYRzzHdUyYb3y1aSgJA/MTA
+tukxGggo5WDDH8SQjhBiYEQN7Aq+VRhxLKX0srwVYv8c474d2h5Xszx+zYIdkeNL
+6yxSNLCK/RJOlrDrcH+eOfdmQrGrrFLadkBXeyq96G4DsguAhYidDMfCd7Camlf0
+uPoTXGiTOmekl9AbmbeGMktg2M7v0Ax/lZ9vh0+Hio5fCHyqW/xavqGRn1V9TrAL
+acywlKinh/LTSlDcX3KwFnUey7QYYpqwpzmqm59m2I2mbJYV4+by+PGDYmy7Velh
+k6M99bFXi08jsJvllGov34zflVEpYKELKeRcVVi3qPyZ7iVNTA6z00yPhOgpD/0Q
+VAKFyPnlw4vP5w8CAwEAAaOBhjCBgzAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0hBBYw
+FDASBgdghXQBUwIBBgdghXQBUwIBMBIGA1UdEwEB/wQIMAYBAf8CAQcwHQYDVR0O
+BBYEFE0mICKJS9PVpAqhb97iEoHF8TwuMB8GA1UdIwQYMBaAFE0mICKJS9PVpAqh
+b97iEoHF8TwuMA0GCSqGSIb3DQEBCwUAA4ICAQAyCrKkG8t9voJXiblqf/P0wS4R
+fbgZPnm3qKhyN2abGu2sEzsOv2LwnN+ee6FTSA5BesogpxcbtnjsQJHzQq0Qw1zv
+/2BZf82Fo4s9SBwlAjxnffUy6S8w5X2lejjQ82YqZh6NM4OKb3xuqFp1mrjX2lhI
+REeoTPpMSQpKwhI3qEAMw8jh0FcNlzKVxzqfl9NX+Ave5XLzo9v/tdhZsnPdTSpx
+srpJ9csc1fV5yJmz/MFMdOO0vSk3FQQoHt5FRnDsr7p4DooqzgB53MBfGWcsa0vv
+aGgLQ+OswWIJ76bdZWGgr4RVSJFSHMYlkSrQwSIjYVmvRRGFHQEkNI/Ps/8XciAT
+woCqISxxOQ7Qj1zB09GOInJGTB2Wrk9xseEFKZZZ9LuedT3PDTcNYtsmjGOpI99n
+Bjx8Oto0QuFmtEYE3saWmA9LSHokMnWRn6z3aOkquVVlzl1h0ydw2Df+n7mvoC5W
+t6NlUe07qxS/TFED6F+KBZvuim6c779o+sjaC+NCydAXFJy3SuCvkychVSa1ZC+N
+8f+mQAWFBVzKBxlcCxMoTFh/wqXvRdpg065lYZ1Tg3TCrvJcwhbtkj6EPnNgiLx2
+9CzP0H1907he0ZESEOnN3col49XtmS++dYFLJPlFRpTJKSFTnCZFqhMX5OfNeOI5
+wSsSnqaeG8XmDtkx2Q==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBY
+MQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMo
+R2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEx
+MjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgxCzAJBgNVBAYTAlVTMRYwFAYDVQQK
+Ew1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQcmltYXJ5IENlcnRp
+ZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9
+AWbK7hWNb6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjA
+ZIVcFU2Ix7e64HXprQU9nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE0
+7e9GceBrAqg1cmuXm2bgyxx5X9gaBGgeRwLmnWDiNpcB3841kt++Z8dtd1k7j53W
+kBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGttm/81w7a4DSwDRp35+MI
+mO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G
+A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJ
+KoZIhvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ1
+6CePbJC/kRYkRj5KTs4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl
+4b7UVXGYNTq+k+qurUKykG/g/CFNNWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6K
+oKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHaFloxt/m0cYASSJlyc1pZU8Fj
+UjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG1riR/aYNKxoU
+AT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx
+EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT
+HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs
+ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw
+MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6
+b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj
+aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp
+Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
+ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg
+nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1
+HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N
+Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN
+dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0
+HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO
+BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G
+CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU
+sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3
+4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg
+8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K
+pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1
+mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIHqDCCBpCgAwIBAgIRAMy4579OKRr9otxmpRwsDxEwDQYJKoZIhvcNAQEFBQAw
+cjELMAkGA1UEBhMCSFUxETAPBgNVBAcTCEJ1ZGFwZXN0MRYwFAYDVQQKEw1NaWNy
+b3NlYyBMdGQuMRQwEgYDVQQLEwtlLVN6aWdubyBDQTEiMCAGA1UEAxMZTWljcm9z
+ZWMgZS1Temlnbm8gUm9vdCBDQTAeFw0wNTA0MDYxMjI4NDRaFw0xNzA0MDYxMjI4
+NDRaMHIxCzAJBgNVBAYTAkhVMREwDwYDVQQHEwhCdWRhcGVzdDEWMBQGA1UEChMN
+TWljcm9zZWMgTHRkLjEUMBIGA1UECxMLZS1Temlnbm8gQ0ExIjAgBgNVBAMTGU1p
+Y3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
+ggEKAoIBAQDtyADVgXvNOABHzNuEwSFpLHSQDCHZU4ftPkNEU6+r+ICbPHiN1I2u
+uO/TEdyB5s87lozWbxXGd36hL+BfkrYn13aaHUM86tnsL+4582pnS4uCzyL4ZVX+
+LMsvfUh6PXX5qqAnu3jCBspRwn5mS6/NoqdNAoI/gqyFxuEPkEeZlApxcpMqyabA
+vjxWTHOSJ/FrtfX9/DAFYJLG65Z+AZHCabEeHXtTRbjcQR/Ji3HWVBTji1R4P770
+Yjtb9aPs1ZJ04nQw7wHb4dSrmZsqa/i9phyGI0Jf7Enemotb9HI6QMVJPqW+jqpx
+62z69Rrkav17fVVA71hu5tnVvCSrwe+3AgMBAAGjggQ3MIIEMzBnBggrBgEFBQcB
+AQRbMFkwKAYIKwYBBQUHMAGGHGh0dHBzOi8vcmNhLmUtc3ppZ25vLmh1L29jc3Aw
+LQYIKwYBBQUHMAKGIWh0dHA6Ly93d3cuZS1zemlnbm8uaHUvUm9vdENBLmNydDAP
+BgNVHRMBAf8EBTADAQH/MIIBcwYDVR0gBIIBajCCAWYwggFiBgwrBgEEAYGoGAIB
+AQEwggFQMCgGCCsGAQUFBwIBFhxodHRwOi8vd3d3LmUtc3ppZ25vLmh1L1NaU1ov
+MIIBIgYIKwYBBQUHAgIwggEUHoIBEABBACAAdABhAG4A+gBzAO0AdAB2AOEAbgB5
+ACAA6QByAHQAZQBsAG0AZQB6AOkAcwDpAGgAZQB6ACAA6QBzACAAZQBsAGYAbwBn
+AGEAZADhAHMA4QBoAG8AegAgAGEAIABTAHoAbwBsAGcA4QBsAHQAYQB0APMAIABT
+AHoAbwBsAGcA4QBsAHQAYQB0AOEAcwBpACAAUwB6AGEAYgDhAGwAeQB6AGEAdABh
+ACAAcwB6AGUAcgBpAG4AdAAgAGsAZQBsAGwAIABlAGwAagDhAHIAbgBpADoAIABo
+AHQAdABwADoALwAvAHcAdwB3AC4AZQAtAHMAegBpAGcAbgBvAC4AaAB1AC8AUwBa
+AFMAWgAvMIHIBgNVHR8EgcAwgb0wgbqggbeggbSGIWh0dHA6Ly93d3cuZS1zemln
+bm8uaHUvUm9vdENBLmNybIaBjmxkYXA6Ly9sZGFwLmUtc3ppZ25vLmh1L0NOPU1p
+Y3Jvc2VjJTIwZS1Temlnbm8lMjBSb290JTIwQ0EsT1U9ZS1Temlnbm8lMjBDQSxP
+PU1pY3Jvc2VjJTIwTHRkLixMPUJ1ZGFwZXN0LEM9SFU/Y2VydGlmaWNhdGVSZXZv
+Y2F0aW9uTGlzdDtiaW5hcnkwDgYDVR0PAQH/BAQDAgEGMIGWBgNVHREEgY4wgYuB
+EGluZm9AZS1zemlnbm8uaHWkdzB1MSMwIQYDVQQDDBpNaWNyb3NlYyBlLVN6aWdu
+w7MgUm9vdCBDQTEWMBQGA1UECwwNZS1TemlnbsOzIEhTWjEWMBQGA1UEChMNTWlj
+cm9zZWMgS2Z0LjERMA8GA1UEBxMIQnVkYXBlc3QxCzAJBgNVBAYTAkhVMIGsBgNV
+HSMEgaQwgaGAFMegSXUWYYTbMUuE0vE3QJDvTtz3oXakdDByMQswCQYDVQQGEwJI
+VTERMA8GA1UEBxMIQnVkYXBlc3QxFjAUBgNVBAoTDU1pY3Jvc2VjIEx0ZC4xFDAS
+BgNVBAsTC2UtU3ppZ25vIENBMSIwIAYDVQQDExlNaWNyb3NlYyBlLVN6aWdubyBS
+b290IENBghEAzLjnv04pGv2i3GalHCwPETAdBgNVHQ4EFgQUx6BJdRZhhNsxS4TS
+8TdAkO9O3PcwDQYJKoZIhvcNAQEFBQADggEBANMTnGZjWS7KXHAM/IO8VbH0jgds
+ZifOwTsgqRy7RlRw7lrMoHfqaEQn6/Ip3Xep1fvj1KcExJW4C+FEaGAHQzAxQmHl
+7tnlJNUb3+FKG6qfx1/4ehHqE5MAyopYse7tDk2016g2JnzgOsHVV4Lxdbb9iV/a
+86g4nzUGCM4ilb7N1fy+W955a9x6qWVmvrElWl/tftOsRm1M9DKHtCAE4Gx4sHfR
+hUZLphK3dehKyVZs15KrnfVJONJPU+NVkBHbmJbGSfI+9J8b4PeI3CVimUTYc78/
+MPMMNz7UwiiAc7EBt51alhQBS6kRnSlqLtBdgcDPsiBDxwPgN05dCtxZICU=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB
+gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
+A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV
+BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw
+MDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl
+YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P
+RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0
+aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3
+UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI
+2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8
+Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp
++2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+
+DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O
+nKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW
+/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g
+PKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u
+QXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY
+SdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv
+IC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/
+RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4
+zJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd
+BA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB
+ZQ==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x
+GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv
+b3QgQ0EgMzAeFw0wNjExMjQxOTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNV
+BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W
+YWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDM
+V0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNggDhoB
+4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUr
+H556VOijKTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd
+8lyyBTNvijbO0BNO/79KDDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9Cabwv
+vWhDFlaJKjdhkf2mrk7AyxRllDdLkgbvBNDInIjbC3uBr7E9KsRlOni27tyAsdLT
+mZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwpp5ijJUMv7/FfJuGITfhe
+btfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8nT8KKdjc
+T5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDt
+WAEXMJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZ
+c6tsgLjoC2SToJyMGf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A
+4iLItLRkT9a6fUg+qGkM17uGcclzuD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYD
+VR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHTBgkrBgEEAb5YAAMwgcUwgZMG
+CCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmljYXRlIGNvbnN0
+aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0
+aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVu
+dC4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2Nw
+czALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4G
+A1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4ywLQoUmkRzBFMQswCQYDVQQGEwJC
+TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UEAxMSUXVvVmFkaXMg
+Um9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZVqyM0
+7ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSem
+d1o417+shvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd
++LJ2w/w4E6oM3kJpK27zPOuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B
+4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadN
+t54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp8kokUvd0/bpO5qgdAm6x
+DYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBCbjPsMZ57
+k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6s
+zHXug/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0j
+Wy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeT
+mJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK
+4SVhM7JZG+Ju1zdXtg2pEto=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIF2TCCA8GgAwIBAgIQXAuFXAvnWUHfV8w/f52oNjANBgkqhkiG9w0BAQUFADBk
+MQswCQYDVQQGEwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0
+YWwgQ2VydGlmaWNhdGUgU2VydmljZXMxGzAZBgNVBAMTElN3aXNzY29tIFJvb3Qg
+Q0EgMTAeFw0wNTA4MTgxMjA2MjBaFw0yNTA4MTgyMjA2MjBaMGQxCzAJBgNVBAYT
+AmNoMREwDwYDVQQKEwhTd2lzc2NvbTElMCMGA1UECxMcRGlnaXRhbCBDZXJ0aWZp
+Y2F0ZSBTZXJ2aWNlczEbMBkGA1UEAxMSU3dpc3Njb20gUm9vdCBDQSAxMIICIjAN
+BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0LmwqAzZuz8h+BvVM5OAFmUgdbI9
+m2BtRsiMMW8Xw/qabFbtPMWRV8PNq5ZJkCoZSx6jbVfd8StiKHVFXqrWW/oLJdih
+FvkcxC7mlSpnzNApbjyFNDhhSbEAn9Y6cV9Nbc5fuankiX9qUvrKm/LcqfmdmUc/
+TilftKaNXXsLmREDA/7n29uj/x2lzZAeAR81sH8A25Bvxn570e56eqeqDFdvpG3F
+EzuwpdntMhy0XmeLVNxzh+XTF3xmUHJd1BpYwdnP2IkCb6dJtDZd0KTeByy2dbco
+kdaXvij1mB7qWybJvbCXc9qukSbraMH5ORXWZ0sKbU/Lz7DkQnGMU3nn7uHbHaBu
+HYwadzVcFh4rUx80i9Fs/PJnB3r1re3WmquhsUvhzDdf/X/NTa64H5xD+SpYVUNF
+vJbNcA78yeNmuk6NO4HLFWR7uZToXTNShXEuT46iBhFRyePLoW4xCGQMwtI89Tbo
+19AOeCMgkckkKmUpWyL3Ic6DXqTz3kvTaI9GdVyDCW4pa8RwjPWd1yAv/0bSKzjC
+L3UcPX7ape8eYIVpQtPM+GP+HkM5haa2Y0EQs3MevNP6yn0WR+Kn1dCjigoIlmJW
+bjTb2QK5MHXjBNLnj8KwEUAKrNVxAmKLMb7dxiNYMUJDLXT5xp6mig/p/r+D5kNX
+JLrvRjSq1xIBOO0CAwEAAaOBhjCBgzAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0hBBYw
+FDASBgdghXQBUwABBgdghXQBUwABMBIGA1UdEwEB/wQIMAYBAf8CAQcwHwYDVR0j
+BBgwFoAUAyUv3m+CATpcLNwroWm1Z9SM0/0wHQYDVR0OBBYEFAMlL95vggE6XCzc
+K6FptWfUjNP9MA0GCSqGSIb3DQEBBQUAA4ICAQA1EMvspgQNDQ/NwNurqPKIlwzf
+ky9NfEBWMXrrpA9gzXrzvsMnjgM+pN0S734edAY8PzHyHHuRMSG08NBsl9Tpl7Ik
+Vh5WwzW9iAUPWxAaZOHHgjD5Mq2eUCzneAXQMbFamIp1TpBcahQq4FJHgmDmHtqB
+sfsUC1rxn9KVuj7QG9YVHaO+htXbD8BJZLsuUBlL0iT43R4HVtA4oJVwIHaM190e
+3p9xxCPvgxNcoyQVTSlAPGrEqdi3pkSlDfTgnXceQHAm/NrZNuR55LU/vJtlvrsR
+ls/bxig5OgjOR1tTWsWZ/l2p3e9M1MalrQLmjAcSHm8D0W+go/MpvRLHUKKwf4ip
+mXeascClOS5cfGniLLDqN2qk4Vrh9VDlg++luyqI54zb/W1elxmofmZ1a3Hqv7HH
+b6D0jqTsNFFbjCYDcKF31QESVwA12yPeDooomf2xEG9L/zgtYE4snOtnta1J7ksf
+rK/7DZBaZmBwXarNeNQk7shBoJMBkpxqnvy5JMWzFYJ+vq6VK+uxwNrjAWALXmms
+hFZhvnEX/h0TD/7Gh0Xp/jKgGg0TpJRVcaUWi7rKibCyx/yP2FS1k2Kdzs9Z+z0Y
+zirLNRWCXf9UIltxUvu3yf5gmwBBZPCqKuy2QkPOiWaByIufOVQDJdMWNY6E0F/6
+MBr1mmz0DlP5OlvRHA==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC
+VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0
+Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW
+KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl
+cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw
+NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw
+NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy
+ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV
+BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ
+KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo
+Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4
+4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9
+KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI
+rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi
+94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB
+sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi
+gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo
+kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE
+vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA
+A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t
+O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua
+AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP
+9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/
+eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m
+0vdXcDazv/wor3ElhVsT/h5/WrQ8
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDujCCAqKgAwIBAgIEAJiWijANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQGEwJO
+TDEeMBwGA1UEChMVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSYwJAYDVQQDEx1TdGFh
+dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQTAeFw0wMjEyMTcwOTIzNDlaFw0xNTEy
+MTYwOTE1MzhaMFUxCzAJBgNVBAYTAk5MMR4wHAYDVQQKExVTdGFhdCBkZXIgTmVk
+ZXJsYW5kZW4xJjAkBgNVBAMTHVN0YWF0IGRlciBOZWRlcmxhbmRlbiBSb290IENB
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmNK1URF6gaYUmHFtvszn
+ExvWJw56s2oYHLZhWtVhCb/ekBPHZ+7d89rFDBKeNVU+LCeIQGv33N0iYfXCxw71
+9tV2U02PjLwYdjeFnejKScfST5gTCaI+Ioicf9byEGW07l8Y1Rfj+MX94p2i71MO
+hXeiD+EwR+4A5zN9RGcaC1Hoi6CeUJhoNFIfLm0B8mBF8jHrqTFoKbt6QZ7GGX+U
+tFE5A3+y3qcym7RHjm+0Sq7lr7HcsBthvJly3uSJt3omXdozSVtSnA71iq3DuD3o
+BmrC1SoLbHuEvVYFy4ZlkuxEK7COudxwC0barbxjiDn622r+I/q85Ej0ZytqERAh
+SQIDAQABo4GRMIGOMAwGA1UdEwQFMAMBAf8wTwYDVR0gBEgwRjBEBgRVHSAAMDww
+OgYIKwYBBQUHAgEWLmh0dHA6Ly93d3cucGtpb3ZlcmhlaWQubmwvcG9saWNpZXMv
+cm9vdC1wb2xpY3kwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSofeu8Y6R0E3QA
+7Jbg0zTBLL9s+DANBgkqhkiG9w0BAQUFAAOCAQEABYSHVXQ2YcG70dTGFagTtJ+k
+/rvuFbQvBgwp8qiSpGEN/KtcCFtREytNwiphyPgJWPwtArI5fZlmgb9uXJVFIGzm
+eafR2Bwp/MIgJ1HI8XxdNGdphREwxgDS1/PTfLbwMVcoEoJz6TMvplW0C5GUR5z6
+u3pCMuiufi3IvKwUv9kP2Vv8wfl6leF9fpb8cbDCTMjfRTTJzg3ynGQI0DvDKcWy
+7ZAEwbEpkcUwb8GpcjPM/l0WFywRaed+/sWDCN+83CI6LiBpIzlWYGeQiy52OfsR
+iJf2fL1LuCAWZwWN4jvBcj+UlTfHXbme2JOhF4//DGYVwSR8MnwDHTuhWEUykw==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UE
+BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h
+cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEy
+MzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg
+Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi
+MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9
+thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM
+cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG
+L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i
+NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h
+X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b
+m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy
+Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja
+EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T
+KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF
+6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh
+OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYD
+VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNHDhpkLzCBpgYD
+VR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp
+cm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBv
+ACAAZABlACAAbABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBl
+AGwAbwBuAGEAIAAwADgAMAAxADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF
+661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx51tkljYyGOylMnfX40S2wBEqgLk9
+am58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qkR71kMrv2JYSiJ0L1
+ILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaPT481
+PyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS
+3a/DTg4fJl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5k
+SeTy36LssUzAKh3ntLFlosS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF
+3dvd6qJ2gHN99ZwExEWN57kci57q13XRcrHedUTnQn3iV2t93Jm8PYMo6oCTjcVM
+ZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoRsaS8I8nkvof/uZS2+F0g
+StRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTDKCOM/icz
+Q0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQB
+jLMi6Et8Vcad+qMUu2WFbm5PEn4KPJ2V
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT
+AlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD
+QTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP
+MREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC
+ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do
+0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ
+UySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d
+RdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ
+OA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv
+JoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C
+AwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O
+BBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ
+LjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY
+MnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ
+44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I
+Jd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw
+i/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN
+9u6wWk5JRFRYX0KD
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl
+MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp
+U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw
+NjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE
+ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp
+ZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3
+DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf
+8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN
++lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0
+X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa
+K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA
+1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G
+A1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR
+zt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0
+YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD
+bGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w
+DQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3
+L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D
+eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl
+xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp
+VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY
+WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDEr
+MCkGA1UEChMiSmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoG
+A1UEAxMTU2VjdXJlU2lnbiBSb290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0
+MDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSswKQYDVQQKEyJKYXBhbiBDZXJ0aWZp
+Y2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1cmVTaWduIFJvb3RD
+QTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvLTJsz
+i1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8
+h9uuywGOwvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOV
+MdrAG/LuYpmGYz+/3ZMqg6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9
+UK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rPO7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni
+8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitAbpSACW22s293bzUIUPsC
+h8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZXt94wDgYD
+VR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEB
+AKChOBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xm
+KbabfSVSSUOrTC4rbnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQ
+X5Ucv+2rIrVls4W6ng+4reV6G4pQOh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWr
+QbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01y8hSyn+B/tlr0/cR7SXf+Of5
+pPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061lgeLKBObjBmN
+QSdJQO7e5iNEOdyhIta6A/I=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFyjCCA7KgAwIBAgIEAJiWjDANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO
+TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFh
+dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQSAtIEcyMB4XDTA4MDMyNjExMTgxN1oX
+DTIwMDMyNTExMDMxMFowWjELMAkGA1UEBhMCTkwxHjAcBgNVBAoMFVN0YWF0IGRl
+ciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5lZGVybGFuZGVuIFJv
+b3QgQ0EgLSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMVZ5291
+qj5LnLW4rJ4L5PnZyqtdj7U5EILXr1HgO+EASGrP2uEGQxGZqhQlEq0i6ABtQ8Sp
+uOUfiUtnvWFI7/3S4GCI5bkYYCjDdyutsDeqN95kWSpGV+RLufg3fNU254DBtvPU
+Z5uW6M7XxgpT0GtJlvOjCwV3SPcl5XCsMBQgJeN/dVrlSPhOewMHBPqCYYdu8DvE
+pMfQ9XQ+pV0aCPKbJdL2rAQmPlU6Yiile7Iwr/g3wtG61jj99O9JMDeZJiFIhQGp
+5Rbn3JBV3w/oOM2ZNyFPXfUib2rFEhZgF1XyZWampzCROME4HYYEhLoaJXhena/M
+UGDWE4dS7WMfbWV9whUYdMrhfmQpjHLYFhN9C0lK8SgbIHRrxT3dsKpICT0ugpTN
+GmXZK4iambwYfp/ufWZ8Pr2UuIHOzZgweMFvZ9C+X+Bo7d7iscksWXiSqt8rYGPy
+5V6548r6f1CGPqI0GAwJaCgRHOThuVw+R7oyPxjMW4T182t0xHJ04eOLoEq9jWYv
+6q012iDTiIJh8BIitrzQ1aTsr1SIJSQ8p22xcik/Plemf1WvbibG/ufMQFxRRIEK
+eN5KzlW/HdXZt1bv8Hb/C3m1r737qWmRRpdogBQ2HbN/uymYNqUg+oJgYjOk7Na6
+B6duxc8UpufWkjTYgfX8HV2qXB72o007uPc5AgMBAAGjgZcwgZQwDwYDVR0TAQH/
+BAUwAwEB/zBSBgNVHSAESzBJMEcGBFUdIAAwPzA9BggrBgEFBQcCARYxaHR0cDov
+L3d3dy5wa2lvdmVyaGVpZC5ubC9wb2xpY2llcy9yb290LXBvbGljeS1HMjAOBgNV
+HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJFoMocVHYnitfGsNig0jQt8YojrMA0GCSqG
+SIb3DQEBCwUAA4ICAQCoQUpnKpKBglBu4dfYszk78wIVCVBR7y29JHuIhjv5tLyS
+CZa59sCrI2AGeYwRTlHSeYAz+51IvuxBQ4EffkdAHOV6CMqqi3WtFMTC6GY8ggen
+5ieCWxjmD27ZUD6KQhgpxrRW/FYQoAUXvQwjf/ST7ZwaUb7dRUG/kSS0H4zpX897
+IZmflZ85OkYcbPnNe5yQzSipx6lVu6xiNGI1E0sUOlWDuYaNkqbG9AclVMwWVxJK
+gnjIFNkXgiYtXSAfea7+1HAWFpWD2DU5/1JddRwWxRNVz0fMdWVSSt7wsKfkCpYL
++63C4iWEst3kvX5ZbJvw8NjnyvLplzh+ib7M+zkXYT9y2zqR2GUBGR2tUKRXCnxL
+vJxxcypFURmFzI79R6d0lR2o0a9OF7FpJsKqeFdbxU2n5Z4FF5TKsl+gSRiNNOkm
+bEgeqmiSBeGCc1qb3AdbCG19ndeNIdn8FCCqwkXfP+cAslHkwvgFuXkajDTznlvk
+N1trSt8sV4pAWja63XVECDdCcAz+3F4hoKOKwJCcaNpQ5kUQR3i2TtJlycM33+FC
+Y7BXN0Ute4qcvwXqZVUz9zkQxSgqIXobisQk+T8VyJoVIPVVYpbtbZNQvOSqeK3Z
+ywplh6ZmwcSBo3c6WB4L7oOLnR7SUqTMHW+wmG2UMbX4cQrcufx9MmDm66+KAQ==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF
+MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD
+bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha
+ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM
+HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03
+UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42
+tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R
+ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM
+lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp
+/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G
+A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G
+A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj
+dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy
+MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl
+cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js
+L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL
+BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni
+acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0
+o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K
+zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8
+PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y
+Johw1+qRzT65ysCQblrGXnRl11z+o+I=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEvTCCA6WgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBhTELMAkGA1UEBhMCVVMx
+IDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJlMRwwGgYDVQQLDBNXZWxs
+cyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMgUm9v
+dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDcxMjEzMTcwNzU0WhcNMjIxMjE0
+MDAwNzU0WjCBhTELMAkGA1UEBhMCVVMxIDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdl
+bGxzU2VjdXJlMRwwGgYDVQQLDBNXZWxscyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQD
+DC1XZWxsc1NlY3VyZSBQdWJsaWMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkw
+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDub7S9eeKPCCGeOARBJe+r
+WxxTkqxtnt3CxC5FlAM1iGd0V+PfjLindo8796jE2yljDpFoNoqXjopxaAkH5OjU
+Dk/41itMpBb570OYj7OeUt9tkTmPOL13i0Nj67eT/DBMHAGTthP796EfvyXhdDcs
+HqRePGj4S78NuR4uNuip5Kf4D8uCdXw1LSLWwr8L87T8bJVhHlfXBIEyg1J55oNj
+z7fLY4sR4r1e6/aN7ZVyKLSsEmLpSjPmgzKuBXWVvYSV2ypcm44uDLiBK0HmOFaf
+SZtsdvqKXfcBeYF8wYNABf5x/Qw/zE5gCQ5lRxAvAcAFP4/4s0HvWkJ+We/Slwxl
+AgMBAAGjggE0MIIBMDAPBgNVHRMBAf8EBTADAQH/MDkGA1UdHwQyMDAwLqAsoCqG
+KGh0dHA6Ly9jcmwucGtpLndlbGxzZmFyZ28uY29tL3dzcHJjYS5jcmwwDgYDVR0P
+AQH/BAQDAgHGMB0GA1UdDgQWBBQmlRkQ2eihl5H/3BnZtQQ+0nMKajCBsgYDVR0j
+BIGqMIGngBQmlRkQ2eihl5H/3BnZtQQ+0nMKaqGBi6SBiDCBhTELMAkGA1UEBhMC
+VVMxIDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJlMRwwGgYDVQQLDBNX
+ZWxscyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMg
+Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHmCAQEwDQYJKoZIhvcNAQEFBQADggEB
+ALkVsUSRzCPIK0134/iaeycNzXK7mQDKfGYZUMbVmO2rvwNa5U3lHshPcZeG1eMd
+/ZDJPHV3V3p9+N701NX3leZ0bh08rnyd2wIDBSxxSyU+B+NemvVmFymIGjifz6pB
+A4SXa5M4esowRBskRDPQ5NHcKDj0E0M1NSljqHyita04pO2t/caaH/+Xc/77szWn
+k4bGdpEA5qxRFsQnMlzbc9qlk1eOPm01JghZ1edE13YgY+esE2fDbbFwRnzVlhE9
+iW9dqKHrjQrawx0zbKPqZxmamX9LPYNRKh3KL4YMon4QLSvUFpULB6ouFJJJtylv
+2G0xffX8oRAHh84vWdw+WNs=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIGSzCCBDOgAwIBAgIIamg+nFGby1MwDQYJKoZIhvcNAQELBQAwgbIxCzAJBgNV
+BAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBC
+aWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhpem1ldGxlcmkgQS7Fni4xJjAkBgNV
+BAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBNZXJrZXppMSgwJgYDVQQDDB9FLVR1
+Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTEzMDMwNTEyMDk0OFoXDTIz
+MDMwMzEyMDk0OFowgbIxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHDAZBbmthcmExQDA+
+BgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhp
+em1ldGxlcmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBN
+ZXJrZXppMSgwJgYDVQQDDB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4vU/kwVRHoViVF56C/UY
+B4Oufq9899SKa6VjQzm5S/fDxmSJPZQuVIBSOTkHS0vdhQd2h8y/L5VMzH2nPbxH
+D5hw+IyFHnSOkm0bQNGZDbt1bsipa5rAhDGvykPL6ys06I+XawGb1Q5KCKpbknSF
+Q9OArqGIW66z6l7LFpp3RMih9lRozt6Plyu6W0ACDGQXwLWTzeHxE2bODHnv0ZEo
+q1+gElIwcxmOj+GMB6LDu0rw6h8VqO4lzKRG+Bsi77MOQ7osJLjFLFzUHPhdZL3D
+k14opz8n8Y4e0ypQBaNV2cvnOVPAmJ6MVGKLJrD3fY185MaeZkJVgkfnsliNZvcH
+fC425lAcP9tDJMW/hkd5s3kc91r0E+xs+D/iWR+V7kI+ua2oMoVJl0b+SzGPWsut
+dEcf6ZG33ygEIqDUD13ieU/qbIWGvaimzuT6w+Gzrt48Ue7LE3wBf4QOXVGUnhMM
+ti6lTPk5cDZvlsouDERVxcr6XQKj39ZkjFqzAQqptQpHF//vkUAqjqFGOjGY5RH8
+zLtJVor8udBhmm9lbObDyz51Sf6Pp+KJxWfXnUYTTjF2OySznhFlhqt/7x3U+Lzn
+rFpct1pHXFXOVbQicVtbC/DP3KBhZOqp12gKY6fgDT+gr9Oq0n7vUaDmUStVkhUX
+U8u3Zg5mTPj5dUyQ5xJwx0UCAwEAAaNjMGEwHQYDVR0OBBYEFC7j27JJ0JxUeVz6
+Jyr+zE7S6E5UMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAULuPbsknQnFR5
+XPonKv7MTtLoTlQwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAF
+Nzr0TbdF4kV1JI+2d1LoHNgQk2Xz8lkGpD4eKexd0dCrfOAKkEh47U6YA5n+KGCR
+HTAduGN8qOY1tfrTYXbm1gdLymmasoR6d5NFFxWfJNCYExL/u6Au/U5Mh/jOXKqY
+GwXgAEZKgoClM4so3O0409/lPun++1ndYYRP0lSWE2ETPo+Aab6TR7U1Q9Jauz1c
+77NCR807VRMGsAnb/WP2OogKmW9+4c4bU2pEZiNRCHu8W1Ki/QY3OEBhj0qWuJA3
++GbHeJAAFS6LrVE1Uweoa2iu+U48BybNCAVwzDk/dr2l02cmAYamU9JgO3xDf1WK
+vJUawSg5TB9D0pH0clmKuVb8P7Sd2nCcdlqMQ1DujjByTd//SffGqWfZbawCEeI6
+FiWnWAjLb1NBnEg4R2gz0dfHj9R0IdTDBZB6/86WiLEVKV0jq9BgoRJP3vQXzTLl
+yb/IQ639Lo7xr+L0mPoSHyDYwKcMhcWQ9DstliaxLL5Mq+ux0orJ23gTDx4JnW2P
+AJ8C2sH6H3p6CcRK5ogql5+Ji/03X186zjhZhkuvcQu02PJwT58yE+Owp1fl2tpD
+y4Q08ijE6m30Ku/Ba3ba+367hTzSU8JNvnHhRdH9I2cNE3X7z2VnIp2usAnRCf8d
+NL/+I5c30jn6PQ0GC7TbO6Orb1wdtn7os4I07QZcJA==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEGDCCAwCgAwIBAgIBATANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQGEwJTRTEU
+MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3
+b3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwHhcNMDAwNTMw
+MTAzODMxWhcNMjAwNTMwMTAzODMxWjBlMQswCQYDVQQGEwJTRTEUMBIGA1UEChML
+QWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYD
+VQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUA
+A4IBDwAwggEKAoIBAQCWltQhSWDia+hBBwzexODcEyPNwTXH+9ZOEQpnXvUGW2ul
+CDtbKRY654eyNAbFvAWlA3yCyykQruGIgb3WntP+LVbBFc7jJp0VLhD7Bo8wBN6n
+tGO0/7Gcrjyvd7ZWxbWroulpOj0OM3kyP3CCkplhbY0wCI9xP6ZIVxn4JdxLZlyl
+dI+Yrsj5wAYi56xz36Uu+1LcsRVlIPo1Zmne3yzxbrww2ywkEtvrNTVokMsAsJch
+PXQhI2U0K7t4WaPW4XY5mqRJjox0r26kmqPZm9I4XJuiGMx1I4S+6+JNM3GOGvDC
++Mcdoq0Dlyz4zyXG9rgkMbFjXZJ/Y/AlyVMuH79NAgMBAAGjgdIwgc8wHQYDVR0O
+BBYEFJWxtPCUtr3H2tERCSG+wa9J/RB7MAsGA1UdDwQEAwIBBjAPBgNVHRMBAf8E
+BTADAQH/MIGPBgNVHSMEgYcwgYSAFJWxtPCUtr3H2tERCSG+wa9J/RB7oWmkZzBl
+MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFk
+ZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENB
+IFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBACxtZBsfzQ3duQH6lmM0MkhHma6X
+7f1yFqZzR1r0693p9db7RcwpiURdv0Y5PejuvE1Uhh4dbOMXJ0PhiVYrqW9yTkkz
+43J8KiOavD7/KCrto/8cI7pDVwlnTUtiBi34/2ydYB7YHEt9tTEv2dB8Xfjea4MY
+eDdXL+gzB2ffHsdrKpV2ro9Xo/D0UrSpUwjP4E/TelOL/bscVjby/rK25Xa71SJl
+pz/+0WatC7xrmYbvP33zGDLKe8bjq2RGlfgmadlVg3sslgf/WSxEo8bl6ancoWOA
+WiFeIc9TVPC6b4nbqKqVz4vjccweGyBECMB6tkD9xOQ14R0WHNC8K47Wcdk=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j
+ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL
+MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
+LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug
+RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm
++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW
+PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM
+xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB
+Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3
+hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg
+EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF
+MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA
+FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec
+nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z
+eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF
+hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2
+Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe
+vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep
++OkuE6N36B9K
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIHSTCCBTGgAwIBAgIJAMnN0+nVfSPOMA0GCSqGSIb3DQEBBQUAMIGsMQswCQYD
+VQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0
+IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3
+MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAlBgNVBAMTHkdsb2JhbCBD
+aGFtYmVyc2lnbiBSb290IC0gMjAwODAeFw0wODA4MDExMjMxNDBaFw0zODA3MzEx
+MjMxNDBaMIGsMQswCQYDVQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3Vy
+cmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAG
+A1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAl
+BgNVBAMTHkdsb2JhbCBDaGFtYmVyc2lnbiBSb290IC0gMjAwODCCAiIwDQYJKoZI
+hvcNAQEBBQADggIPADCCAgoCggIBAMDfVtPkOpt2RbQT2//BthmLN0EYlVJH6xed
+KYiONWwGMi5HYvNJBL99RDaxccy9Wglz1dmFRP+RVyXfXjaOcNFccUMd2drvXNL7
+G706tcuto8xEpw2uIRU/uXpbknXYpBI4iRmKt4DS4jJvVpyR1ogQC7N0ZJJ0YPP2
+zxhPYLIj0Mc7zmFLmY/CDNBAspjcDahOo7kKrmCgrUVSY7pmvWjg+b4aqIG7HkF4
+ddPB/gBVsIdU6CeQNR1MM62X/JcumIS/LMmjv9GYERTtY/jKmIhYF5ntRQOXfjyG
+HoiMvvKRhI9lNNgATH23MRdaKXoKGCQwoze1eqkBfSbW+Q6OWfH9GzO1KTsXO0G2
+Id3UwD2ln58fQ1DJu7xsepeY7s2MH/ucUa6LcL0nn3HAa6x9kGbo1106DbDVwo3V
+yJ2dwW3Q0L9R5OP4wzg2rtandeavhENdk5IMagfeOx2YItaswTXbo6Al/3K1dh3e
+beksZixShNBFks4c5eUzHdwHU1SjqoI7mjcv3N2gZOnm3b2u/GSFHTynyQbehP9r
+6GsaPMWis0L7iwk+XwhSx2LE1AVxv8Rk5Pihg+g+EpuoHtQ2TS9x9o0o9oOpE9Jh
+wZG7SMA0j0GMS0zbaRL/UJScIINZc+18ofLx/d33SdNDWKBWY8o9PeU1VlnpDsog
+zCtLkykPAgMBAAGjggFqMIIBZjASBgNVHRMBAf8ECDAGAQH/AgEMMB0GA1UdDgQW
+BBS5CcqcHtvTbDprru1U8VuTBjUuXjCB4QYDVR0jBIHZMIHWgBS5CcqcHtvTbDpr
+ru1U8VuTBjUuXqGBsqSBrzCBrDELMAkGA1UEBhMCRVUxQzBBBgNVBAcTOk1hZHJp
+ZCAoc2VlIGN1cnJlbnQgYWRkcmVzcyBhdCB3d3cuY2FtZXJmaXJtYS5jb20vYWRk
+cmVzcykxEjAQBgNVBAUTCUE4Mjc0MzI4NzEbMBkGA1UEChMSQUMgQ2FtZXJmaXJt
+YSBTLkEuMScwJQYDVQQDEx5HbG9iYWwgQ2hhbWJlcnNpZ24gUm9vdCAtIDIwMDiC
+CQDJzdPp1X0jzjAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCow
+KAYIKwYBBQUHAgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZI
+hvcNAQEFBQADggIBAICIf3DekijZBZRG/5BXqfEv3xoNa/p8DhxJJHkn2EaqbylZ
+UohwEurdPfWbU1Rv4WCiqAm57OtZfMY18dwY6fFn5a+6ReAJ3spED8IXDneRRXoz
+X1+WLGiLwUePmJs9wOzL9dWCkoQ10b42OFZyMVtHLaoXpGNR6woBrX/sdZ7LoR/x
+fxKxueRkf2fWIyr0uDldmOghp+G9PUIadJpwr2hsUF1Jz//7Dl3mLEfXgTpZALVz
+a2Mg9jFFCDkO9HB+QHBaP9BrQql0PSgvAm11cpUJjUhjxsYjV5KTXjXBjfkK9yyd
+Yhz2rXzdpjEetrHHfoUm+qRqtdpjMNHvkzeyZi99Bffnt0uYlDXA2TopwZ2yUDMd
+SqlapskD7+3056huirRXhOukP9DuqqqHW2Pok+JrqNS4cnhrG+055F3Lm6qH1U9O
+AP7Zap88MQ8oAgF9mOinsKJknnn4SPIVqczmyETrP3iZ8ntxPjzxmKfFGBI/5rso
+M0LpRQp8bfKGeS/Fghl9CYl8slR2iK7ewfPM4W7bMdaTrpmg7yVqc5iJWzouE4ge
+v8CSlDQb4ye3ix5vQv/n6TebUB0tovkC7stYWDpxvGjjqsGvHCgfotwjZT+B6q6Z
+09gwzxMNTxXJhLynSC34MCN32EZLeW32jO06f2ARePTpm67VVMB0gNELQp/B
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFcjCCA1qgAwIBAgIQH51ZWtcvwgZEpYAIaeNe9jANBgkqhkiG9w0BAQUFADA/
+MQswCQYDVQQGEwJUVzEwMC4GA1UECgwnR292ZXJubWVudCBSb290IENlcnRpZmlj
+YXRpb24gQXV0aG9yaXR5MB4XDTAyMTIwNTEzMjMzM1oXDTMyMTIwNTEzMjMzM1ow
+PzELMAkGA1UEBhMCVFcxMDAuBgNVBAoMJ0dvdmVybm1lbnQgUm9vdCBDZXJ0aWZp
+Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
+AJoluOzMonWoe/fOW1mKydGGEghU7Jzy50b2iPN86aXfTEc2pBsBHH8eV4qNw8XR
+IePaJD9IK/ufLqGU5ywck9G/GwGHU5nOp/UKIXZ3/6m3xnOUT0b3EEk3+qhZSV1q
+gQdW8or5BtD3cCJNtLdBuTK4sfCxw5w/cP1T3YGq2GN49thTbqGsaoQkclSGxtKy
+yhwOeYHWtXBiCAEuTk8O1RGvqa/lmr/czIdtJuTJV6L7lvnM4T9TjGxMfptTCAts
+F/tnyMKtsc2AtJfcdgEWFelq16TheEfOhtX7MfP6Mb40qij7cEwdScevLJ1tZqa2
+jWR+tSBqnTuBto9AAGdLiYa4zGX+FVPpBMHWXx1E1wovJ5pGfaENda1UhhXcSTvx
+ls4Pm6Dso3pdvtUqdULle96ltqqvKKyskKw4t9VoNSZ63Pc78/1Fm9G7Q3hub/FC
+VGqY8A2tl+lSXunVanLeavcbYBT0peS2cWeqH+riTcFCQP5nRhc4L0c/cZyu5SHK
+YS1tB6iEfC3uUSXxY5Ce/eFXiGvviiNtsea9P63RPZYLhY3Naye7twWb7LuRqQoH
+EgKXTiCQ8P8NHuJBO9NAOueNXdpm5AKwB1KYXA6OM5zCppX7VRluTI6uSw+9wThN
+Xo+EHWbNxWCWtFJaBYmOlXqYwZE8lSOyDvR5tMl8wUohAgMBAAGjajBoMB0GA1Ud
+DgQWBBTMzO/MKWCkO7GStjz6MmKPrCUVOzAMBgNVHRMEBTADAQH/MDkGBGcqBwAE
+MTAvMC0CAQAwCQYFKw4DAhoFADAHBgVnKgMAAAQUA5vwIhP/lSg209yewDL7MTqK
+UWUwDQYJKoZIhvcNAQEFBQADggIBAECASvomyc5eMN1PhnR2WPWus4MzeKR6dBcZ
+TulStbngCnRiqmjKeKBMmo4sIy7VahIkv9Ro04rQ2JyftB8M3jh+Vzj8jeJPXgyf
+qzvS/3WXy6TjZwj/5cAWtUgBfen5Cv8b5Wppv3ghqMKnI6mGq3ZW6A4M9hPdKmaK
+ZEk9GhiHkASfQlK3T8v+R0F2Ne//AHY2RTKbxkaFXeIksB7jSJaYV0eUVXoPQbFE
+JPPB/hprv4j9wabak2BegUqZIJxIZhm1AHlUD7gsL0u8qV1bYH+Mh6XgUmMqvtg7
+hUAV/h62ZT/FS9p+tXo1KaMuephgIqP0fSdOLeq0dDzpD6QzDxARvBMB1uUO07+1
+EqLhRSPAzAhuYbeJq4PjJB7mXQfnHyA+z2fI56wwbSdLaG5LKlwCCDTb+HbkZ6Mm
+nD+iMsJKxYEYMRBWqoTvLQr/uB930r+lWKBi5NdLkXWNiYCYfm3LU05er/ayl4WX
+udpVBrkk7tfGOB5jGxI7leFYrPLfhNVfmS8NVVvmONsuP3LpSIXLuykTjx44Vbnz
+ssQwmSNOXfJIoRIM3BKQCZBUkQM8R+XVyWXgt0t97EfTsws+rZ7QdAAO671RrcDe
+LMDDav7v3Aun+kbfYNucpllQdSNpc5Oy+fwC00fmcc4QAu4njIT/rEUNE1yDMuAl
+pYYsfPQS
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDUzCCAjugAwIBAgIBATANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJOTzEd
+MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxHTAbBgNVBAMMFEJ1eXBhc3Mg
+Q2xhc3MgMiBDQSAxMB4XDTA2MTAxMzEwMjUwOVoXDTE2MTAxMzEwMjUwOVowSzEL
+MAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MR0wGwYD
+VQQDDBRCdXlwYXNzIENsYXNzIDIgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEP
+ADCCAQoCggEBAIs8B0XY9t/mx8q6jUPFR42wWsE425KEHK8T1A9vNkYgxC7McXA0
+ojTTNy7Y3Tp3L8DrKehc0rWpkTSHIln+zNvnma+WwajHQN2lFYxuyHyXA8vmIPLX
+l18xoS830r7uvqmtqEyeIWZDO6i88wmjONVZJMHCR3axiFyCO7srpgTXjAePzdVB
+HfCuuCkslFJgNJQ72uA40Z0zPhX0kzLFANq1KWYOOngPIVJfAuWSeyXTkh4vFZ2B
+5J2O6O+JzhRMVB0cgRJNcKi+EAUXfh/RuFdV7c27UsKwHnjCTTZoy1YmwVLBvXb3
+WNVyfh9EdrsAiR0WnVE1703CVu9r4Iw7DekCAwEAAaNCMEAwDwYDVR0TAQH/BAUw
+AwEB/zAdBgNVHQ4EFgQUP42aWYv8e3uco684sDntkHGA1sgwDgYDVR0PAQH/BAQD
+AgEGMA0GCSqGSIb3DQEBBQUAA4IBAQAVGn4TirnoB6NLJzKyQJHyIdFkhb5jatLP
+gcIV1Xp+DCmsNx4cfHZSldq1fyOhKXdlyTKdqC5Wq2B2zha0jX94wNWZUYN/Xtm+
+DKhQ7SLHrQVMdvvt7h5HZPb3J31cKA9FxVxiXqaakZG3Uxcu3K1gnZZkOb1naLKu
+BctN518fV4bVIJwo+28TOPX2EZL2fZleHwzoq0QkKXJAPTZSr4xYkHPB7GEseaHs
+h7U/2k3ZIQAw3pDaDtMaSKk+hQsUi4y8QZ5q9w5wwDX3OaJdZtB7WZ+oRxKaJyOk
+LY4ng5IgodcVf/EuGO70SH8vf/GhGLWhC5SgYiAynB321O+/TIho
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU
+MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs
+IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290
+MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux
+FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h
+bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v
+dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt
+H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9
+uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX
+mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX
+a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN
+E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0
+WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD
+VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0
+Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU
+cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx
+IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN
+AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH
+YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5
+6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC
+Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX
+c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a
+mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEuTCCA6GgAwIBAgIQQBrEZCGzEyEDDrvkEhrFHTANBgkqhkiG9w0BAQsFADCB
+vTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL
+ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwOCBWZXJp
+U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MTgwNgYDVQQDEy9W
+ZXJpU2lnbiBVbml2ZXJzYWwgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe
+Fw0wODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIG9MQswCQYDVQQGEwJVUzEX
+MBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0
+IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAyMDA4IFZlcmlTaWduLCBJbmMuIC0gRm9y
+IGF1dGhvcml6ZWQgdXNlIG9ubHkxODA2BgNVBAMTL1ZlcmlTaWduIFVuaXZlcnNh
+bCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEF
+AAOCAQ8AMIIBCgKCAQEAx2E3XrEBNNti1xWb/1hajCMj1mCOkdeQmIN65lgZOIzF
+9uVkhbSicfvtvbnazU0AtMgtc6XHaXGVHzk8skQHnOgO+k1KxCHfKWGPMiJhgsWH
+H26MfF8WIFFE0XBPV+rjHOPMee5Y2A7Cs0WTwCznmhcrewA3ekEzeOEz4vMQGn+H
+LL729fdC4uW/h2KJXwBL38Xd5HVEMkE6HnFuacsLdUYI0crSK5XQz/u5QGtkjFdN
+/BMReYTtXlT2NJ8IAfMQJQYXStrxHXpma5hgZqTZ79IugvHw7wnqRMkVauIDbjPT
+rJ9VAMf2CGqUuV/c4DPxhGD5WycRtPwW8rtWaoAljQIDAQABo4GyMIGvMA8GA1Ud
+EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMG0GCCsGAQUFBwEMBGEwX6FdoFsw
+WTBXMFUWCWltYWdlL2dpZjAhMB8wBwYFKw4DAhoEFI/l0xqGrI2Oa8PPgGrUSBgs
+exkuMCUWI2h0dHA6Ly9sb2dvLnZlcmlzaWduLmNvbS92c2xvZ28uZ2lmMB0GA1Ud
+DgQWBBS2d/ppSEefUxLVwuoHMnYH0ZcHGTANBgkqhkiG9w0BAQsFAAOCAQEASvj4
+sAPmLGd75JR3Y8xuTPl9Dg3cyLk1uXBPY/ok+myDjEedO2Pzmvl2MpWRsXe8rJq+
+seQxIcaBlVZaDrHC1LGmWazxY8u4TB1ZkErvkBYoH1quEPuBUDgMbMzxPcP1Y+Oz
+4yHJJDnp/RVmRvQbEdBNc6N9Rvk97ahfYtTxP/jgdFcrGJ2BtMQo2pSXpXDrrB2+
+BxHw1dvd5Yzw1TKwg+ZX4o+/vqGqvz0dtdQ46tewXDpPaj+PwGZsY6rp2aQW9IHR
+lRQOfc2VNNnSj3BzgXucfr2YYdhFh5iQxeuGMMY1v/D/w1WIg0vvBZIGcfK4mJO3
+7M2CYfE45k+XmCpajQ==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ
+RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD
+VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX
+DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y
+ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy
+VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr
+mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr
+IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK
+mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu
+XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy
+dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye
+jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1
+BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3
+DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92
+9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx
+jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0
+Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz
+ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS
+R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI
+MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x
+FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz
+MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv
+cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN
+AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz
+Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO
+0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao
+wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj
+7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS
+8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT
+BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB
+/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg
+JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC
+NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3
+6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/
+3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm
+D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS
+CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR
+3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFaDCCA1CgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJVUzEW
+MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEeMBwGA1UEAxMVR2VvVHJ1c3QgVW5pdmVy
+c2FsIENBMB4XDTA0MDMwNDA1MDAwMFoXDTI5MDMwNDA1MDAwMFowRTELMAkGA1UE
+BhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xHjAcBgNVBAMTFUdlb1RydXN0
+IFVuaXZlcnNhbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKYV
+VaCjxuAfjJ0hUNfBvitbtaSeodlyWL0AG0y/YckUHUWCq8YdgNY96xCcOq9tJPi8
+cQGeBvV8Xx7BDlXKg5pZMK4ZyzBIle0iN430SppyZj6tlcDgFgDgEB8rMQ7XlFTT
+QjOgNB0eRXbdT8oYN+yFFXoZCPzVx5zw8qkuEKmS5j1YPakWaDwvdSEYfyh3peFh
+F7em6fgemdtzbvQKoiFs7tqqhZJmr/Z6a4LauiIINQ/PQvE1+mrufislzDoR5G2v
+c7J2Ha3QsnhnGqQ5HFELZ1aD/ThdDc7d8Lsrlh/eezJS/R27tQahsiFepdaVaH/w
+mZ7cRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7VqnJNk22CDtucvc+081xd
+VHppCZbW2xHBjXWotM85yM48vCR85mLK4b19p71XZQvk/iXttmkQ3CgaRr0BHdCX
+teGYO8A3ZNY9lO4L4fUorgtWv3GLIylBjobFS1J72HGrH4oVpjuDWtdYAVHGTEHZ
+f9hBZ3KiKN9gg6meyHv8U3NyWfWTehd2Ds735VzZC1U0oqpbtWpU5xPKV+yXbfRe
+Bi9Fi1jUIxaS5BZuKGNZMN9QAZxjiRqf2xeUgnA3wySemkfWWspOqGmJch+RbNt+
+nhutxx9z3SxPGWX9f5NAEC7S8O08ni4oPmkmM8V7AgMBAAGjYzBhMA8GA1UdEwEB
+/wQFMAMBAf8wHQYDVR0OBBYEFNq7LqqwDLiIJlF0XG0D08DYj3rWMB8GA1UdIwQY
+MBaAFNq7LqqwDLiIJlF0XG0D08DYj3rWMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG
+9w0BAQUFAAOCAgEAMXjmx7XfuJRAyXHEqDXsRh3ChfMoWIawC/yOsjmPRFWrZIRc
+aanQmjg8+uUfNeVE44B5lGiku8SfPeE0zTBGi1QrlaXv9z+ZhP015s8xxtxqv6fX
+IwjhmF7DWgh2qaavdy+3YL1ERmrvl/9zlcGO6JP7/TG37FcREUWbMPEaiDnBTzyn
+ANXH/KttgCJwpQzgXQQpAvvLoJHRfNbDflDVnVi+QTjruXU8FdmbyUqDWcDaU/0z
+uzYYm4UPFd3uLax2k7nZAY1IEKj79TiG8dsKxr2EoyNB3tZ3b4XUhRxQ4K5RirqN
+Pnbiucon8l+f725ZDQbYKxek0nxru18UGkiPGkzns0ccjkxFKyDuSN/n3QmOGKja
+QI2SJhFTYXNd673nxE0pN2HrrDktZy4W1vUAg4WhzH92xH3kt0tm7wNFYGm2DFKW
+koRepqO1pD4r2czYG0eq8kTaT/kD6PAUyz/zg97QwVTjt+gKN02LIFkDMBmhLMi9
+ER/frslKxfMnZmaGrGiR/9nmUxwPi1xpZQomyB40w11Re9epnAahNt3ViZS82eQt
+DF4JbAiXfKM9fJP/P6EUp8+1Xevb2xzEdt+Iub1FBZUbrvxGakyvSOPOrg/Sfuvm
+bJxPgWp6ZKy7PtXny3YuxadIwVyQD8vIP/rmMuGNG2+k5o7Y+SlIis5z/iw=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH
+MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT
+MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
+b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI
+2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx
+1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ
+q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz
+tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ
+vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP
+BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV
+5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY
+1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4
+NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG
+Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91
+8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe
+pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl
+MrY=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFpDCCA4ygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEc
+MBoGA1UEChMTQW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBP
+bmxpbmUgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAyMB4XDTAyMDUyODA2
+MDAwMFoXDTM3MDkyOTE0MDgwMFowYzELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0Ft
+ZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2EgT25saW5lIFJvb3Qg
+Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIP
+ADCCAgoCggIBAMxBRR3pPU0Q9oyxQcngXssNt79Hc9PwVU3dxgz6sWYFas14tNwC
+206B89enfHG8dWOgXeMHDEjsJcQDIPT/DjsS/5uN4cbVG7RtIuOx238hZK+GvFci
+KtZHgVdEglZTvYYUAQv8f3SkWq7xuhG1m1hagLQ3eAkzfDJHA1zEpYNI9FdWboE2
+JxhP7JsowtS013wMPgwr38oE18aO6lhOqKSlGBxsRZijQdEt0sdtjRnxrXm3gT+9
+BoInLRBYBbV4Bbkv2wxrkJB+FFk4u5QkE+XRnRTf04JNRvCAOVIyD+OEsnpD8l7e
+Xz8d3eOyG6ChKiMDbi4BFYdcpnV1x5dhvt6G3NRI270qv0pV2uh9UPu0gBe4lL8B
+PeraunzgWGcXuVjgiIZGZ2ydEEdYMtA1fHkqkKJaEBEjNa0vzORKW6fIJ/KD3l67
+Xnfn6KVuY8INXWHQjNJsWiEOyiijzirplcdIz5ZvHZIlyMbGwcEMBawmxNJ10uEq
+Z8A9W6Wa6897GqidFEXlD6CaZd4vKL3Ob5Rmg0gp2OpljK+T2WSfVVcmv2/LNzGZ
+o2C7HK2JNDJiuEMhBnIMoVxtRsX6Kc8w3onccVvdtjc+31D1uAclJuW8tf48ArO3
++L5DwYcRlJ4jbBeKuIonDFRH8KmzwICMoCfrHRnjB453cMor9H124HhnAgMBAAGj
+YzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFE1FwWg4u3OpaaEg5+31IqEj
+FNeeMB8GA1UdIwQYMBaAFE1FwWg4u3OpaaEg5+31IqEjFNeeMA4GA1UdDwEB/wQE
+AwIBhjANBgkqhkiG9w0BAQUFAAOCAgEAZ2sGuV9FOypLM7PmG2tZTiLMubekJcmn
+xPBUlgtk87FYT15R/LKXeydlwuXK5w0MJXti4/qftIe3RUavg6WXSIylvfEWK5t2
+LHo1YGwRgJfMqZJS5ivmae2p+DYtLHe/YUjRYwu5W1LtGLBDQiKmsXeu3mnFzccc
+obGlHBD7GL4acN3Bkku+KVqdPzW+5X1R+FXgJXUjhx5c3LqdsKyzadsXg8n33gy8
+CNyRnqjQ1xU3c6U1uPx+xURABsPr+CKAXEfOAuMRn0T//ZoyzH1kUQ7rVyZ2OuMe
+IjzCpjbdGe+n/BLzJsBZMYVMnNjP36TMzCmT/5RtdlwTCJfy7aULTd3oyWgOZtMA
+DjMSW7yV5TKQqLPGbIOtd+6Lfn6xqavT4fG2wLHqiMDn05DpKJKUe2h7lyoKZy2F
+AjgQ5ANh1NolNscIWC2hp1GvMApJ9aZphwctREZ2jirlmjvXGKL8nDgQzMY70rUX
+Om/9riW99XJZZLF0KjhfGEzfz3EEWjbUvy+ZnOjZurGV5gJLIaFb1cFPj65pbVPb
+AZO1XB4Y3WRayhgoPmMEEf0cjQAPuDffZ4qdZqkCapH/E8ovXYO8h5Ns3CRRFgQl
+Zvqz2cK6Kb6aSDiCmfS/O0oxGfm/jiEzFMpPVF/7zvuPcX/9XhmgD0uRuMRUvAaw
+RY8mkaKO/qk=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDzzCCAregAwIBAgIDAWweMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYDVQQGEwJB
+VDFIMEYGA1UECgw/QS1UcnVzdCBHZXMuIGYuIFNpY2hlcmhlaXRzc3lzdGVtZSBp
+bSBlbGVrdHIuIERhdGVudmVya2VociBHbWJIMRkwFwYDVQQLDBBBLVRydXN0LW5R
+dWFsLTAzMRkwFwYDVQQDDBBBLVRydXN0LW5RdWFsLTAzMB4XDTA1MDgxNzIyMDAw
+MFoXDTE1MDgxNzIyMDAwMFowgY0xCzAJBgNVBAYTAkFUMUgwRgYDVQQKDD9BLVRy
+dXN0IEdlcy4gZi4gU2ljaGVyaGVpdHNzeXN0ZW1lIGltIGVsZWt0ci4gRGF0ZW52
+ZXJrZWhyIEdtYkgxGTAXBgNVBAsMEEEtVHJ1c3QtblF1YWwtMDMxGTAXBgNVBAMM
+EEEtVHJ1c3QtblF1YWwtMDMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
+AQCtPWFuA/OQO8BBC4SAzewqo51ru27CQoT3URThoKgtUaNR8t4j8DRE/5TrzAUj
+lUC5B3ilJfYKvUWG6Nm9wASOhURh73+nyfrBJcyFLGM/BWBzSQXgYHiVEEvc+RFZ
+znF/QJuKqiTfC0Li21a8StKlDJu3Qz7dg9MmEALP6iPESU7l0+m0iKsMrmKS1GWH
+2WrX9IWf5DMiJaXlyDO6w8dB3F/GaswADm0yqLaHNgBid5seHzTLkDx4iHQF63n1
+k3Flyp3HaxgtPVxO59X4PzF9j4fsCiIvI+n+u33J4PTs63zEsMMtYrWacdaxaujs
+2e3Vcuy+VwHOBVWf3tFgiBCzAgMBAAGjNjA0MA8GA1UdEwEB/wQFMAMBAf8wEQYD
+VR0OBAoECERqlWdVeRFPMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOC
+AQEAVdRU0VlIXLOThaq/Yy/kgM40ozRiPvbY7meIMQQDbwvUB/tOdQ/TLtPAF8fG
+KOwGDREkDg6lXb+MshOWcdzUzg4NCmgybLlBMRmrsQd7TZjTXLDR8KdCoLXEjq/+
+8T/0709GAHbrAvv5ndJAlseIOrifEXnzgGWovR/TeIGgUUw3tKZdJXDRZslo+S4R
+FGjxVJgIrCaSD96JntT6s3kr0qN51OyLrIdTaEJMUVF0HhsnLuP1Hyl0Te2v9+GS
+mYHovjrHF1D2t8b8m7CKa9aIA5GPBnc6hQLdmNVDeD/GMBWsm2vLV7eJUYs66MmE
+DNuxUCAKGkq6ahq97BvIxYSazQ==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv
+b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG
+EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl
+cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi
+MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA
+n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc
+biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp
+EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA
+bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu
+YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB
+AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW
+BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI
+QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I
+0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni
+lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9
+B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv
+ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo
+IhNzbM8m9Yop5w==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV
+UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy
+dGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1
+MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0VxdWlmYXgxLTArBgNVBAsTJEVx
+dWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCBnzANBgkqhkiG9w0B
+AQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPRfM6f
+BeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+A
+cJkVV5MW8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kC
+AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQ
+MA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlm
+aWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTgw
+ODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvSspXXR9gj
+IBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQF
+MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA
+A4GBAFjOKer89961zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y
+7qj/WsjTVbJmcVfewCHrPSqnI0kBBIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh
+1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee9570+sB3c4
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM
+MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D
+ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU
+cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3
+WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg
+Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw
+IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B
+AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH
+UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM
+TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU
+BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM
+kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x
+AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV
+HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV
+HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y
+sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL
+I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8
+J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY
+VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI
+03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE
+AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw
+CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ
+BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND
+VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb
+qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY
+HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo
+G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA
+lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr
+IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/
+0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH
+k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47
+4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO
+m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa
+cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl
+uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI
+KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls
+ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG
+AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2
+VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT
+VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG
+CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA
+cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA
+QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA
+7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA
+cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA
+QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA
+czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu
+aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt
+aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud
+DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF
+BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp
+D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU
+JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m
+AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD
+vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms
+tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH
+7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h
+I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA
+h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF
+d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H
+pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFbDCCA1SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBHMQswCQYDVQQGEwJVUzEW
+MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVy
+c2FsIENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQswCQYD
+VQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1
+c3QgVW5pdmVyc2FsIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
+AQCzVFLByT7y2dyxUxpZKeexw0Uo5dfR7cXFS6GqdHtXr0om/Nj1XqduGdt0DE81
+WzILAePb63p3NeqqWuDW6KFXlPCQo3RWlEQwAx5cTiuFJnSCegx2oG9NzkEtoBUG
+FF+3Qs17j1hhNNwqCPkuwwGmIkQcTAeC5lvO0Ep8BNMZcyfwqph/Lq9O64ceJHdq
+XbboW0W63MOhBW9Wjo8QJqVJwy7XQYci4E+GymC16qFjwAGXEHm9ADwSbSsVsaxL
+se4YuU6W3Nx2/zu+z18DwPw76L5GG//aQMJS9/7jOvdqdzXQ2o3rXhhqMcceujwb
+KNZrVMaqW9eiLBsZzKIC9ptZvTdrhrVtgrrY6slWvKk2WP0+GfPtDCapkzj4T8Fd
+IgbQl+rhrcZV4IErKIM6+vR7IVEAvlI4zs1meaj0gVbi0IMJR1FbUGrP20gaXT73
+y/Zl92zxlfgCOzJWgjl6W70viRu/obTo/3+NjN8D8WBOWBFM66M/ECuDmgFz2ZRt
+hAAnZqzwcEAJQpKtT5MNYQlRJNiS1QuUYbKHsu3/mjX/hVTK7URDrBs8FmtISgoc
+QIgfksILAAX/8sgCSqSqqcyZlpwvWOB94b67B9xfBHJcMTTD7F8t4D1kkCLm0ey4
+Lt1ZrtmhN79UNdxzMk+MBB4zsslG8dhcyFVQyWi9qLo2CQIDAQABo2MwYTAPBgNV
+HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAfBgNV
+HSMEGDAWgBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAOBgNVHQ8BAf8EBAMCAYYwDQYJ
+KoZIhvcNAQEFBQADggIBAGbBxiPz2eAubl/oz66wsCVNK/g7WJtAJDday6sWSf+z
+dXkzoS9tcBc0kf5nfo/sm+VegqlVHy/c1FEHEv6sFj4sNcZj/NwQ6w2jqtB8zNHQ
+L1EuxBRa3ugZ4T7GzKQp5y6EqgYweHZUcyiYWTjgAA1i00J9IZ+uPTqM1fp3DRgr
+Fg5fNuH8KrUwJM/gYwx7WBr+mbpCErGR9Hxo4sjoryzqyX6uuyo9DRXcNJW2GHSo
+ag/HtPQTxORb7QrSpJdMKu0vbBKJPfEncKpqA1Ihn0CoZ1Dy81of398j9tx4TuaY
+T1U6U+Pv8vSfx3zYWK8pIpe44L2RLrB27FcRz+8pRPPphXpgY+RdM4kX2TGq2tbz
+GDVyz4crL2MjhF2EjD9XoIj8mZEoJmmZ1I+XRL6O1UixpCgp8RW04eWe3fiPpm8m
+1wk8OhwRDqZsN/etRIcsKMfYdIKz0G9KV7s1KSegi+ghp4dkNl3M2Basx7InQJJV
+OCiNUW7dFGdTbHFcJoRNdVq2fmBWqU2t+5sel/MN2dKXVHfaPRK34B7vCAas+YWH
+6aLcr34YEoP9VhdBLtUpgn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwX
+QMAJKOSLakhT2+zNVVXxxvjpoixMptEmX36vWkzaH6byHCx+rgIW0lbQL1dTR+iS
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh
+MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE
+YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3
+MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo
+ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg
+MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN
+ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA
+PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w
+wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi
+EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY
+avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+
+YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE
+sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h
+/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5
+IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj
+YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD
+ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy
+OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P
+TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ
+HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER
+dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf
+ReYNnyicsbkqWletNw+vHX/bvZ8=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEqjCCA5KgAwIBAgIOSkcAAQAC5aBd1j8AUb8wDQYJKoZIhvcNAQEFBQAwdjEL
+MAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNV
+BAsTGVRDIFRydXN0Q2VudGVyIENsYXNzIDMgQ0ExJTAjBgNVBAMTHFRDIFRydXN0
+Q2VudGVyIENsYXNzIDMgQ0EgSUkwHhcNMDYwMTEyMTQ0MTU3WhcNMjUxMjMxMjI1
+OTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1c3RDZW50ZXIgR21i
+SDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQTElMCMGA1UEAxMc
+VEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBALTgu1G7OVyLBMVMeRwjhjEQY0NVJz/GRcekPewJDRoeIMJW
+Ht4bNwcwIi9v8Qbxq63WyKthoy9DxLCyLfzDlml7forkzMA5EpBCYMnMNWju2l+Q
+Vl/NHE1bWEnrDgFPZPosPIlY2C8u4rBo6SI7dYnWRBpl8huXJh0obazovVkdKyT2
+1oQDZogkAHhg8fir/gKya/si+zXmFtGt9i4S5Po1auUZuV3bOx4a+9P/FRQI2Alq
+ukWdFHlgfa9Aigdzs5OW03Q0jTo3Kd5c7PXuLjHCINy+8U9/I1LZW+Jk2ZyqBwi1
+Rb3R0DHBq1SfqdLDYmAD8bs5SpJKPQq5ncWg/jcCAwEAAaOCATQwggEwMA8GA1Ud
+EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTUovyfs8PYA9NX
+XAek0CSnwPIA1DCB7QYDVR0fBIHlMIHiMIHfoIHcoIHZhjVodHRwOi8vd3d3LnRy
+dXN0Y2VudGVyLmRlL2NybC92Mi90Y19jbGFzc18zX2NhX0lJLmNybIaBn2xkYXA6
+Ly93d3cudHJ1c3RjZW50ZXIuZGUvQ049VEMlMjBUcnVzdENlbnRlciUyMENsYXNz
+JTIwMyUyMENBJTIwSUksTz1UQyUyMFRydXN0Q2VudGVyJTIwR21iSCxPVT1yb290
+Y2VydHMsREM9dHJ1c3RjZW50ZXIsREM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9u
+TGlzdD9iYXNlPzANBgkqhkiG9w0BAQUFAAOCAQEANmDkcPcGIEPZIxpC8vijsrlN
+irTzwppVMXzEO2eatN9NDoqTSheLG43KieHPOh6sHfGcMrSOWXaiQYUlN6AT0PV8
+TtXqluJucsG7Kv5sbviRmEb8yRtXW+rIGjs/sFGYPAfaLFkB2otE6OF0/ado3VS6
+g0bsyEa1+K+XwDsJHI/OcpY9M1ZwvJbL2NV9IJqDnxrcOfHFcqMRA/07QlIp2+gB
+95tejNaNhk4Z+rwcvsUhpYeeeC422wlxo3I0+GzjBgnyXlal092Y+tTmBvTwtiBj
+S+opvaqCZh77gaqnN60TGOaSw4HBM7uIHqHn4rS9MWwOUT1v+5ZWgOI2F9Hc5A==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDEzCCAnygAwIBAgIBATANBgkqhkiG9w0BAQQFADCBxDELMAkGA1UEBhMCWkEx
+FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD
+VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv
+biBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEm
+MCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wHhcNOTYwODAx
+MDAwMDAwWhcNMjAxMjMxMjM1OTU5WjCBxDELMAkGA1UEBhMCWkExFTATBgNVBAgT
+DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3
+dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNl
+cyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3
+DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQAD
+gY0AMIGJAoGBANOkUG7I/1Zr5s9dtuoMaHVHoqrC2oQl/Kj0R1HahbUgdJSGHg91
+yekIYfUGbTBuFRkC6VLAYttNmZ7iagxEOM3+vuNkCXDF/rFrKbYvScg71CcEJRCX
+L+eQbcAoQpnXTEPew/UhbVSfXcNY4cDk2VuwuNy0e982OsK1ZiIS1ocNAgMBAAGj
+EzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAB/pMaVz7lcxG
+7oWDTSEwjsrZqG9JGubaUeNgcGyEYRGhGshIPllDfU+VPaGLtwtimHp1it2ITk6e
+QNuozDJ0uW8NxuOzRAvZim+aKZuZGCg70eNAKJpaPNW15yAbi8qkq43pUdniTCxZ
+qdq5snUb9kLy78fyGPmJvKP/iiMucEc=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDZzCCAk+gAwIBAgIQGx+ttiD5JNM2a/fH8YygWTANBgkqhkiG9w0BAQUFADBF
+MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPVHJ1c3RpcyBMaW1pdGVkMRwwGgYDVQQL
+ExNUcnVzdGlzIEZQUyBSb290IENBMB4XDTAzMTIyMzEyMTQwNloXDTI0MDEyMTEx
+MzY1NFowRTELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1RydXN0aXMgTGltaXRlZDEc
+MBoGA1UECxMTVHJ1c3RpcyBGUFMgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBAMVQe547NdDfxIzNjpvto8A2mfRC6qc+gIMPpqdZh8mQRUN+
+AOqGeSoDvT03mYlmt+WKVoaTnGhLaASMk5MCPjDSNzoiYYkchU59j9WvezX2fihH
+iTHcDnlkH5nSW7r+f2C/revnPDgpai/lkQtV/+xvWNUtyd5MZnGPDNcE2gfmHhjj
+vSkCqPoc4Vu5g6hBSLwacY3nYuUtsuvffM/bq1rKMfFMIvMFE/eC+XN5DL7XSxzA
+0RU8k0Fk0ea+IxciAIleH2ulrG6nS4zto3Lmr2NNL4XSFDWaLk6M6jKYKIahkQlB
+OrTh4/L68MkKokHdqeMDx4gVOxzUGpTXn2RZEm0CAwEAAaNTMFEwDwYDVR0TAQH/
+BAUwAwEB/zAfBgNVHSMEGDAWgBS6+nEleYtXQSUhhgtx67JkDoshZzAdBgNVHQ4E
+FgQUuvpxJXmLV0ElIYYLceuyZA6LIWcwDQYJKoZIhvcNAQEFBQADggEBAH5Y//01
+GX2cGE+esCu8jowU/yyg2kdbw++BLa8F6nRIW/M+TgfHbcWzk88iNVy2P3UnXwmW
+zaD+vkAMXBJV+JOCyinpXj9WV4s4NvdFGkwozZ5BuO1WTISkQMi4sKUraXAEasP4
+1BIy+Q7DsdwyhEQsb8tGD+pmQQ9P8Vilpg0ND2HepZ5dfWWhPBfnqFVO76DH7cZE
+f1T1o+CP8HxVIo8ptoGj4W1OLBuAZ+ytIJ8MYmHVl/9D7S3B2l0pKoU/rGXuhg8F
+jZBf3+6f9L/uHfuY5H+QK4R4EA5sSVPvFVtlRkpdr7r7OnIdzfYliB6XzCGcKQEN
+ZetX2fNXlrtIzYE=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDYTCCAkmgAwIBAgIQCgEBAQAAAnwAAAAKAAAAAjANBgkqhkiG9w0BAQUFADA6
+MRkwFwYDVQQKExBSU0EgU2VjdXJpdHkgSW5jMR0wGwYDVQQLExRSU0EgU2VjdXJp
+dHkgMjA0OCBWMzAeFw0wMTAyMjIyMDM5MjNaFw0yNjAyMjIyMDM5MjNaMDoxGTAX
+BgNVBAoTEFJTQSBTZWN1cml0eSBJbmMxHTAbBgNVBAsTFFJTQSBTZWN1cml0eSAy
+MDQ4IFYzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt49VcdKA3Xtp
+eafwGFAyPGJn9gqVB93mG/Oe2dJBVGutn3y+Gc37RqtBaB4Y6lXIL5F4iSj7Jylg
+/9+PjDvJSZu1pJTOAeo+tWN7fyb9Gd3AIb2E0S1PRsNO3Ng3OTsor8udGuorryGl
+wSMiuLgbWhOHV4PR8CDn6E8jQrAApX2J6elhc5SYcSa8LWrg903w8bYqODGBDSnh
+AMFRD0xS+ARaqn1y07iHKrtjEAMqs6FPDVpeRrc9DvV07Jmf+T0kgYim3WBU6JU2
+PcYJk5qjEoAAVZkZR73QpXzDuvsf9/UP+Ky5tfQ3mBMY3oVbtwyCO4dvlTlYMNpu
+AWgXIszACwIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
+BjAfBgNVHSMEGDAWgBQHw1EwpKrpRa41JPr/JCwz0LGdjDAdBgNVHQ4EFgQUB8NR
+MKSq6UWuNST6/yQsM9CxnYwwDQYJKoZIhvcNAQEFBQADggEBAF8+hnZuuDU8TjYc
+HnmYv/3VEhF5Ug7uMYm83X/50cYVIeiKAVQNOvtUudZj1LGqlk2iQk3UUx+LEN5/
+Zb5gEydxiKRz44Rj0aRV4VCT5hsOedBnvEbIvz8XDZXmxpBp3ue0L96VfdASPz0+
+f00/FGj1EVDVwfSQpQgdMWD/YIwjVAqv/qFuxdF6Kmh4zx6CCiC0H63lhbJqaHVO
+rSU3lIW+vaHU6rcMSzyd6BIA8F+sDeGscGNz9395nzIlQnQFgCi/vcEkllgVsRch
+6YlL2weIZ/QVrXA+L02FO8K32/6YaCOJ4XQP3vTFhGMpG8zLB8kApKnXwiJPZ9d3
+7CAFYd4=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/
+MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
+DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow
+PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
+Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O
+rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq
+OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b
+xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw
+7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD
+aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV
+HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG
+SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69
+ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr
+AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz
+R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5
+JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo
+Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw
+CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu
+ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe
+Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw
+EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x
+IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF
+K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG
+fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO
+Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd
+BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx
+AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/
+oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8
+sycX
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIExTCCA62gAwIBAgIBADANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJFVTEn
+MCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQL
+ExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEgMB4GA1UEAxMXR2xvYmFsIENo
+YW1iZXJzaWduIFJvb3QwHhcNMDMwOTMwMTYxNDE4WhcNMzcwOTMwMTYxNDE4WjB9
+MQswCQYDVQQGEwJFVTEnMCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgy
+NzQzMjg3MSMwIQYDVQQLExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEgMB4G
+A1UEAxMXR2xvYmFsIENoYW1iZXJzaWduIFJvb3QwggEgMA0GCSqGSIb3DQEBAQUA
+A4IBDQAwggEIAoIBAQCicKLQn0KuWxfH2H3PFIP8T8mhtxOviteePgQKkotgVvq0
+Mi+ITaFgCPS3CU6gSS9J1tPfnZdan5QEcOw/Wdm3zGaLmFIoCQLfxS+EjXqXd7/s
+QJ0lcqu1PzKY+7e3/HKE5TWH+VX6ox8Oby4o3Wmg2UIQxvi1RMLQQ3/bvOSiPGpV
+eAp3qdjqGTK3L/5cPxvusZjsyq16aUXjlg9V9ubtdepl6DJWk0aJqCWKZQbua795
+B9Dxt6/tLE2Su8CoX6dnfQTyFQhwrJLWfQTSM/tMtgsL+xrJxI0DqX5c8lCrEqWh
+z0hQpe/SyBoT+rB/sYIcd2oPX9wLlY/vQ37mRQklAgEDo4IBUDCCAUwwEgYDVR0T
+AQH/BAgwBgEB/wIBDDA/BgNVHR8EODA2MDSgMqAwhi5odHRwOi8vY3JsLmNoYW1i
+ZXJzaWduLm9yZy9jaGFtYmVyc2lnbnJvb3QuY3JsMB0GA1UdDgQWBBRDnDafsJ4w
+TcbOX60Qq+UDpfqpFDAOBgNVHQ8BAf8EBAMCAQYwEQYJYIZIAYb4QgEBBAQDAgAH
+MCoGA1UdEQQjMCGBH2NoYW1iZXJzaWducm9vdEBjaGFtYmVyc2lnbi5vcmcwKgYD
+VR0SBCMwIYEfY2hhbWJlcnNpZ25yb290QGNoYW1iZXJzaWduLm9yZzBbBgNVHSAE
+VDBSMFAGCysGAQQBgYcuCgEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly9jcHMuY2hh
+bWJlcnNpZ24ub3JnL2Nwcy9jaGFtYmVyc2lnbnJvb3QuaHRtbDANBgkqhkiG9w0B
+AQUFAAOCAQEAPDtwkfkEVCeR4e3t/mh/YV3lQWVPMvEYBZRqHN4fcNs+ezICNLUM
+bKGKfKX0j//U2K0X1S0E0T9YgOKBWYi+wONGkyT+kL0mojAt6JcmVzWJdJYY9hXi
+ryQZVgICsroPFOrGimbBhkVVi76SvpykBMdJPJ7oKXqJ1/6v/2j1pReQvayZzKWG
+VwlnRtvWFsJG8eSpUPWP0ZIV018+xgBJOm5YstHRJw0lyDL4IBHNfTIzSJRUTN3c
+ecQwn+uOuFW114hcxWokPbLTBQNRxgfvzBRydD1ucs4YKIxKoHflCStFREest2d/
+AYoFWpO+ocH/+OcOZ6RHSXZddZAa9SaP8A==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDIDCCAgigAwIBAgIBHTANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEP
+MA0GA1UEChMGU29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MyIENBMB4XDTAx
+MDQwNjA3Mjk0MFoXDTIxMDQwNjA3Mjk0MFowOTELMAkGA1UEBhMCRkkxDzANBgNV
+BAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJhIENsYXNzMiBDQTCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBAJAXSjWdyvANlsdE+hY3/Ei9vX+ALTU74W+o
+Z6m/AxxNjG8yR9VBaKQTBME1DJqEQ/xcHf+Js+gXGM2RX/uJ4+q/Tl18GybTdXnt
+5oTjV+WtKcT0OijnpXuENmmz/V52vaMtmdOQTiMofRhj8VQ7Jp12W5dCsv+u8E7s
+3TmVToMGf+dJQMjFAbJUWmYdPfz56TwKnoG4cPABi+QjVHzIrviQHgCWctRUz2Ej
+vOr7nQKV0ba5cTppCD8PtOFCx4j1P5iop7oc4HFx71hXgVB6XGt0Rg6DA5jDjqhu
+8nYybieDwnPz3BjotJPqdURrBGAgcVeHnfO+oJAjPYok4doh28MCAwEAAaMzMDEw
+DwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQISqCqWITTXjwwCwYDVR0PBAQDAgEG
+MA0GCSqGSIb3DQEBBQUAA4IBAQBazof5FnIVV0sd2ZvnoiYw7JNn39Yt0jSv9zil
+zqsWuasvfDXLrNAPtEwr/IDva4yRXzZ299uzGxnq9LIR/WFxRL8oszodv7ND6J+/
+3DEIcbCdjdY0RzKQxmUk96BKfARzjzlvF4xytb1LyHr4e4PDKE6cCepnP7JnBBvD
+FNr450kkkdAdavphOe9r5yF1BgfYErQhIHBCcYHaPJo2vqZbDWpsmh+Re/n570K6
+Tk6ezAyNlNzZRZxe7EJQY670XcSxEtzKO6gunRRaBXW37Ndj4ro1tgQIkejanZz2
+ZrUYrAqmVCY0M9IbwdR/GjqOC6oybtv8TyWf2TLHllpwrN9M
+-----END CERTIFICATE-----
diff --git a/run/sota_client.service b/run/sota_client.service
new file mode 100644
index 0000000..53016bd
--- /dev/null
+++ b/run/sota_client.service
@@ -0,0 +1,15 @@
+[Unit]
+Description=SOTA Client
+Wants=network-online.target
+After=network.target network-online.target
+Requires=network-online.target
+
+[Service]
+RestartSec=5
+Restart=on-failure
+Environment="RUST_LOG=info"
+DefaultTimeoutStopSec=5
+ExecStart=/usr/bin/sota_client --config /etc/sota.toml
+
+[Install]
+WantedBy=multi-user.target
diff --git a/run/system_info.sh b/run/system_info.sh
new file mode 100755
index 0000000..ccbca86
--- /dev/null
+++ b/run/system_info.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+INFO=$(lshw -json -sanitize)
+
+MANIFEST_PATH=/etc/manifest.xml
+if [ -f $MANIFEST_PATH ]; then
+ MANIFEST={\"manifest_file\":\"$( cat $MANIFEST_PATH | sed 's/"/\\"/'g | sed -e ':a' -e 'N' -e '$!ba' -e 's/\n/ /g' )\"}
+ INFO=$(echo $MANIFEST $INFO | jq -s add | jq -r .)
+fi
+
+SERVICE_HOSTNAME_PATH=/var/lib/tor/hidden_service/hostname
+if [ -f $SERVICE_HOSTNAME_PATH ]; then
+ SERVICE_HOSTNAME={\"service_hostname\":\"$( cat $SERVICE_HOSTNAME_PATH )\"}
+ INFO=$(echo $SERVICE_HOSTNAME $INFO | jq -s add | jq -r .)
+fi
+
+echo $INFO | jq -r .
diff --git a/src/broadcast.rs b/src/broadcast.rs
new file mode 100644
index 0000000..5389b0c
--- /dev/null
+++ b/src/broadcast.rs
@@ -0,0 +1,60 @@
+use chan;
+use chan::{Sender, Receiver};
+
+
+/// Retain a list of all peers that should receive the incoming message.
+pub struct Broadcast<A: Clone> {
+ peers: Vec<Sender<A>>,
+ rx: Receiver<A>
+}
+
+impl<A: Clone> Broadcast<A> {
+ /// Instantiate a new broadcaster for the given `Receiver`.
+ pub fn new(rx: Receiver<A>) -> Broadcast<A> {
+ Broadcast { peers: vec![], rx: rx }
+ }
+
+ /// Start receiving broadcasting messages and forwarding each to the list
+ /// of peers.
+ pub fn start(&self) {
+ loop {
+ self.rx.recv().map(|a| {
+ for subscriber in &self.peers {
+ subscriber.send(a.clone());
+ }
+ });
+ }
+ }
+
+ /// Add a new subscriber to the list of peers that will receive the broadcast
+ /// messages.
+ pub fn subscribe(&mut self) -> Receiver<A> {
+ let (tx, rx) = chan::sync::<A>(0);
+ self.peers.push(tx);
+ rx
+ }
+}
+
+
+#[cfg(test)]
+mod tests {
+ use chan;
+ use std::thread;
+
+ use super::*;
+
+
+ #[test]
+ fn test_broadcasts_events() {
+ let (tx, rx) = chan::sync(0);
+ let mut broadcast = Broadcast::new(rx);
+
+ let a = broadcast.subscribe();
+ let b = broadcast.subscribe();
+ thread::spawn(move || broadcast.start());
+
+ tx.send(123);
+ assert_eq!(123, a.recv().unwrap());
+ assert_eq!(123, b.recv().unwrap());
+ }
+}
diff --git a/src/configuration/client.rs b/src/configuration/client.rs
deleted file mode 100644
index 625f4e9..0000000
--- a/src/configuration/client.rs
+++ /dev/null
@@ -1,177 +0,0 @@
-//! Handles the `client` section of the configuration file.
-
-use toml;
-
-use super::common::{get_required_key, get_optional_key, ConfTreeParser, Result};
-
-/// Type to encode allowed keys for the `client` section of the configuration.
-#[derive(Clone)]
-pub struct ClientConfiguration {
- /// Directory where chunks and packages will be stored.
- pub storage_dir: String,
- /// The full URL where RVI can be reached.
- pub rvi_url: Option<String>,
- /// The `host:port` combination where the client should bind and listen for incoming RVI calls.
- pub edge_url: Option<String>,
- /// How long to wait for further server messages before the `Transfer` will be dropped.
- pub timeout: Option<i64>,
- /// Index of the RVI service URL, that holds the VIN for this device.
- pub vin_match: i32
-}
-
-impl ConfTreeParser<ClientConfiguration> for ClientConfiguration {
- fn parse(tree: &toml::Table) -> Result<ClientConfiguration> {
- let client_tree = try!(tree.get("client")
- .ok_or("Missing required subgroup \"client\""));
-
- let storage_dir = try!(get_required_key(client_tree, "storage_dir", "client"));
- let rvi_url = try!(get_optional_key(client_tree, "rvi_url", "client"));
- let edge_url = try!(get_optional_key(client_tree, "edge_url", "client"));
- let timeout = try!(get_optional_key(client_tree, "timeout", "client"));
- let vin_match = try!(get_optional_key(client_tree, "vin_match", "client"));
-
- Ok(ClientConfiguration {
- storage_dir: storage_dir,
- rvi_url: rvi_url,
- edge_url: edge_url,
- timeout: timeout,
- vin_match: vin_match.unwrap_or(2)
- })
- }
-}
-
-#[cfg(test)] static STORAGE: &'static str = "/var/sota";
-#[cfg(test)] static RVI: &'static str = "/http://localhost:8901";
-#[cfg(test)] static EDGE: &'static str = "localhost:9080";
-#[cfg(test)] static TIMEOUT: i64 = 10;
-#[cfg(test)] static VIN: i32 = 3;
-
-#[cfg(test)]
-pub fn gen_valid_conf() -> String {
- format!(r#"
- [client]
- storage_dir = "{}"
- rvi_url = "{}"
- edge_url = "{}"
- timeout = {}
- vin_match = {}
- "#, STORAGE, RVI, EDGE, TIMEOUT, VIN)
-}
-
-#[cfg(test)]
-pub fn assert_conf(configuration: &ClientConfiguration) -> bool {
- assert_eq!(&configuration.storage_dir, STORAGE);
- assert_eq!(&configuration.rvi_url.clone().unwrap(), RVI);
- assert_eq!(&configuration.edge_url.clone().unwrap(), EDGE);
- assert_eq!(configuration.timeout.unwrap(), TIMEOUT);
- assert_eq!(configuration.vin_match, VIN);
- true
-}
-
-#[cfg(test)]
-pub mod test {
- use super::*;
- use super::{STORAGE, RVI, EDGE, TIMEOUT, VIN};
- use configuration::common::{ConfTreeParser, read_tree};
-
- #[test]
- fn it_requires_the_storage_dir_key() {
- test_init!();
- let data = format!(r#"
- [client]
- rvi_url = "{}"
- edge_url = "{}"
- timeout = {}
- vin_match = {}
- "#, RVI, EDGE, TIMEOUT, VIN);
-
- let tree = read_tree(&data).unwrap();
- match ClientConfiguration::parse(&tree) {
- Ok(..) => panic!("Accepted invalid configuration!"),
- Err(e) => {
- assert_eq!(e,
- "Missing required key \"storage_dir\" in \"client\""
- .to_string());
- }
- };
- }
-
- #[test]
- fn it_doesnt_require_the_rvi_url_key() {
- test_init!();
- let data = format!(r#"
- [client]
- storage_dir = "{}"
- edge_url = "{}"
- timeout = {}
- vin_match = {}
- "#, STORAGE, EDGE, TIMEOUT, VIN);
-
- let tree = read_tree(&data).unwrap();
- let configuration = ClientConfiguration::parse(&tree).unwrap();
- assert_eq!(&configuration.storage_dir, STORAGE);
- assert_eq!(configuration.rvi_url, None);
- assert_eq!(&configuration.edge_url.unwrap(), EDGE);
- assert_eq!(configuration.timeout.unwrap(), TIMEOUT);
- assert_eq!(configuration.vin_match, VIN);
- }
-
- #[test]
- fn it_doesnt_require_the_edge_url_key() {
- test_init!();
- let data = format!(r#"
- [client]
- storage_dir = "{}"
- rvi_url = "{}"
- timeout = {}
- vin_match = {}
- "#, STORAGE, RVI, TIMEOUT, VIN);
-
- let tree = read_tree(&data).unwrap();
- let configuration = ClientConfiguration::parse(&tree).unwrap();
- assert_eq!(&configuration.storage_dir, STORAGE);
- assert_eq!(&configuration.rvi_url.unwrap(), RVI);
- assert_eq!(configuration.edge_url, None);
- assert_eq!(configuration.vin_match, VIN);
- }
-
- #[test]
- fn it_doesnt_require_the_timeout_key() {
- test_init!();
- let data = format!(r#"
- [client]
- storage_dir = "{}"
- rvi_url = "{}"
- edge_url = "{}"
- vin_match = {}
- "#, STORAGE, RVI, EDGE, VIN);
-
- let tree = read_tree(&data).unwrap();
- let configuration = ClientConfiguration::parse(&tree).unwrap();
- assert_eq!(&configuration.storage_dir, STORAGE);
- assert_eq!(&configuration.rvi_url.unwrap(), RVI);
- assert_eq!(&configuration.edge_url.unwrap(), EDGE);
- assert_eq!(configuration.timeout, None);
- assert_eq!(configuration.vin_match, VIN);
- }
-
- #[test]
- fn it_doesnt_require_the_vin_match_key_and_uses_a_default() {
- test_init!();
- let data = format!(r#"
- [client]
- storage_dir = "{}"
- rvi_url = "{}"
- edge_url = "{}"
- timeout = {}
- "#, STORAGE, RVI, EDGE, TIMEOUT);
-
- let tree = read_tree(&data).unwrap();
- let configuration = ClientConfiguration::parse(&tree).unwrap();
- assert_eq!(&configuration.storage_dir, STORAGE);
- assert_eq!(&configuration.rvi_url.unwrap(), RVI);
- assert_eq!(&configuration.edge_url.unwrap(), EDGE);
- assert_eq!(configuration.timeout.unwrap(), TIMEOUT);
- assert_eq!(configuration.vin_match, 2);
- }
-}
diff --git a/src/configuration/common.rs b/src/configuration/common.rs
deleted file mode 100644
index e6b0fcd..0000000
--- a/src/configuration/common.rs
+++ /dev/null
@@ -1,128 +0,0 @@
-//! Helper functions and traits for all configuration sections.
-
-use toml;
-use std::result;
-use std::fmt;
-
-/// `Result` type used throughout the configuration parser.
-pub type Result<T> = result::Result<T, String>;
-
-/// Trait that provides a interface for parsing a (sub-) tree of the configuration.
-pub trait ConfTreeParser<C> {
- /// Try to parse the given `tree` into the type this trait is implemented for.
- /// Returns the parsed object or a error message, with the first error encountered while
- /// parsing the `tree`.
- ///
- /// # Arguments
- /// * `tree`: The `toml` tree to parse
- fn parse(tree: &toml::Table) -> Result<C>;
-}
-
-/// Parse a required key, returning a appropriate error message, if the key can't be found in the
-/// configuration.
-///
-/// # Arguments
-/// * `subtree`: The `toml` tree to parse.
-/// * `key`: The key to look for.
-/// * `group`: The group, this (sub-) tree is associated with.
-pub fn get_required_key<D>(subtree: &toml::Value, key: &str, group: &str)
- -> Result<D> where D: ParseTomlValue {
- let value = try!(subtree.lookup(key)
- .ok_or(format!("Missing required key \"{}\" in \"{}\"",
- key, group)));
- ParseTomlValue::parse(value, key, group)
-}
-
-/// Parse a optional key, returning None if it can't be found.
-///
-/// This basically does a `Option<Result>` to `Result<Option>` translation
-///
-/// # Arguments
-/// * `subtree`: The `toml` tree to parse.
-/// * `key`: The key to look for.
-/// * `group`: The group, this (sub-) tree is associated with.
-pub fn get_optional_key<D>(subtree: &toml::Value, key: &str, group: &str)
- -> Result<Option<D>> where D: ParseTomlValue {
- match subtree.lookup(key) {
- Some(val) => {
- Ok(Some(try!(ParseTomlValue::parse(val, key, group))))
- },
- None => Ok(None)
- }
-}
-
-/// Trait that provides a interface for parsing a single `toml` value.
-pub trait ParseTomlValue {
- /// Parse a `String` from a `toml` value. Returns the parsed value on success or a error
- /// message on failure.
- ///
- /// # Arguments
- /// * `val`: The `toml` value to parse.
- /// * `key`: The key this value is associated with.
- /// * `group`: The group, this (sub-) tree is associated with.
- fn parse(val: &toml::Value, key: &str, group: &str) -> Result<Self> where Self: Sized;
-}
-
-impl ParseTomlValue for String {
- fn parse(val: &toml::Value, key: &str, group: &str)
- -> Result<String> {
- val.as_str().map(|s| s.to_string())
- .ok_or(format!("Key \"{}\" in \"{}\" is not a string", key, group))
- }
-}
-
-impl ParseTomlValue for i32 {
- fn parse(val: &toml::Value, key: &str, group: &str)
- -> Result<i32> {
- val.as_integer().map(|i| i as i32)
- .ok_or(format!("Key \"{}\" in \"{}\" is not a integer", key, group))
- }
-}
-
-impl ParseTomlValue for i64 {
- fn parse(val: &toml::Value, key: &str, group: &str)
- -> Result<i64> {
- val.as_integer()
- .ok_or(format!("Key \"{}\" in \"{}\" is not a integer", key, group))
- }
-}
-
-/// Helper function to format a `toml::Parser` error message to the format used in this
-/// implementation. This is only safe to call if the `parser` is associated with a *real* file on
-/// disk.
-///
-/// # Arguments
-/// * `parser`: Pointer to the `toml::Parser`, that produced a error.
-#[cfg(not(test))]
-pub fn format_parser_error(parser: &toml::Parser) -> String {
- let linecol = parser.to_linecol(0);
- format!("parse error: {}:{}: {:?}", linecol.0, linecol.1, parser.errors)
-}
-
-/// Helper function to format a `toml::Parser` error message to the format used in this
-/// implementation. This version is always safe to call, but doesn't print the line and column
-/// where the error was encountered.
-///
-/// # Arguments
-/// * `parser`: Pointer to the `toml::Parser`, that produced a error.
-#[cfg(test)]
-pub fn format_parser_error(parser: &toml::Parser) -> String {
- format!("parse error: {:?}", parser.errors)
-}
-
-/// Helper function to copy anything that implements `Display` to a `String`.
-pub fn stringify<T>(e: T) -> String
- where T: fmt::Display {
- format!("{}", e)
-}
-
-/// Reads the provided `tree` as a `toml::Table`. Returns the `toml::Table` on success or a error
-/// message on failure.
-///
-/// # Arguments
-/// * `tree`: Pointer to a `str`, that holds a toml configuration.
-#[cfg(test)]
-pub fn read_tree(tree: &str) -> Result<toml::Table> {
- let mut parser = toml::Parser::new(tree);
- parser.parse().ok_or(format_parser_error(&parser))
-}
diff --git a/src/configuration/configuration.rs b/src/configuration/configuration.rs
deleted file mode 100644
index c2c0416..0000000
--- a/src/configuration/configuration.rs
+++ /dev/null
@@ -1,176 +0,0 @@
-//! Main logic for parsing the configuration file
-
-use toml;
-use std::io::prelude::*;
-use std::path::PathBuf;
-use std::fs::OpenOptions;
-use std::env;
-
-use super::common::{ConfTreeParser, format_parser_error, stringify, Result};
-use super::client::ClientConfiguration;
-use super::dbus::DBusConfiguration;
-
-/// Type to encode the full configuration.
-#[derive(Clone)]
-pub struct Configuration {
- /// The `client` section of the configuration
- pub client: ClientConfiguration,
- /// The `dbus` section of the configuration
- pub dbus: DBusConfiguration
-}
-
-impl Configuration {
- /// Try to read the configuration from the provided path and parse it into a `Configuration`
- /// object. Returns the parsed `Configuration` on success or the first error message
- /// encountered while reading or parsing the configuration file.
- ///
- /// # Arguments
- /// * `path`: Path to the location of the configuration file.
- pub fn read(path: &str) -> Result<Configuration> {
- let path = PathBuf::from(path);
- let mut f = try!(OpenOptions::new().open(path).map_err(stringify));
- let mut buf = Vec::new();
- try!(f.read_to_end(&mut buf).map_err(stringify));
- let data = try!(String::from_utf8(buf).map_err(stringify));
- Configuration::parse(&data)
- }
-
- /// Try to parse the given string to a `Configuration`.
- ///
- /// # Arguments
- /// * `conf`: The configuration to parse.
- pub fn parse(conf: &str) -> Result<Configuration> {
- let mut parser = toml::Parser::new(conf);
- let tree = try!(parser.parse().ok_or(format_parser_error(&parser)));
-
- let client = try!(ClientConfiguration::parse(&tree));
- let dbus = try!(DBusConfiguration::parse(&tree));
-
- Ok(Configuration {
- client: client,
- dbus: dbus
- })
- }
-
- /// Try to find the configuration file in different paths. First
- /// `$XDG_CONFIG_HOME/sota/client.toml` is tried, then `$HOME/.sota/client.toml` returns
- /// `$PWD/.sota/client.toml` if none of the above can be found. The case where this file also
- /// doesn't exist should be handled by [`read`](#method.read).
- pub fn default_path() -> String {
- match env::var_os("XDG_CONFIG_HOME")
- .and_then(|s| s.into_string().ok()) {
- Some(val) => { return val + "/sota/client.toml"; },
- None => { error!("$XDG_CONFIG_HOME is not set"); }
- }
-
- match env::var_os("HOME").and_then(|s| s.into_string().ok()) {
- Some(val) => {
- warn!("Falling back to $HOME/.config");
- return val + "/.sota/client.toml";
- },
- None => { error!("$HOME is not set"); }
- }
-
- warn!("Falling back to $PWD");
- ".sota/client.toml".to_string()
- }
-}
-
-#[cfg(test)]
-mod test {
- use super::*;
- use std::env;
- use configuration::client;
- use configuration::dbus;
-
- #[test]
- fn it_uses_fallbacks_for_its_configuration() {
- test_init!();
- env::remove_var("XDG_CONFIG_HOME");
- env::set_var("XDG_CONFIG_HOME", "/some/thing");
- assert_eq!(Configuration::default_path(),
- "/some/thing/sota/client.toml".to_string());
- env::remove_var("XDG_CONFIG_HOME");
- env::remove_var("HOME");
- env::set_var("HOME", "/some/thing");
- assert_eq!(Configuration::default_path(),
- "/some/thing/.sota/client.toml".to_string());
- env::remove_var("XDG_CONFIG_HOME");
- env::remove_var("HOME");
- assert_eq!(Configuration::default_path(),
- ".sota/client.toml".to_string());
- }
-
- #[test]
- fn it_correctly_parses_a_valid_configuration() {
- test_init!();
- let data = format!("{}\n{}",
- client::gen_valid_conf(),
- dbus::gen_valid_conf());
-
- let configuration = Configuration::parse(&data).unwrap();
- assert!(client::assert_conf(&configuration.client));
- assert!(dbus::assert_conf(&configuration.dbus));
- }
-
- #[test]
- fn it_ignores_extra_keys() {
- test_init!();
- let data = format!(r#"
- {}
- test_key = "hello world"
-
- {}
- test_key = "see ya world"
- "#, client::gen_valid_conf(),
- dbus::gen_valid_conf());
-
- let configuration = Configuration::parse(&data).unwrap();
- assert!(client::assert_conf(&configuration.client));
- assert!(dbus::assert_conf(&configuration.dbus));
- }
-
- #[test]
- fn it_ignores_extra_groups() {
- test_init!();
- let data = format!(r#"
- {}
-
- {}
-
- [test]
- test_key = "hello world"
- "#, client::gen_valid_conf(),
- dbus::gen_valid_conf());
-
- let configuration = Configuration::parse(&data).unwrap();
- assert!(client::assert_conf(&configuration.client));
- assert!(dbus::assert_conf(&configuration.dbus));
- }
-
- #[test]
- fn it_requires_the_client_group() {
- test_init!();
- let data = format!("{}", dbus::gen_valid_conf());
- match Configuration::parse(&data) {
- Ok(..) => panic!("Accepted invalid configuration!"),
- Err(e) => {
- assert_eq!(e, "Missing required subgroup \"client\""
- .to_string());
- }
- };
- }
-
- #[test]
- fn it_requires_the_dbus_group() {
- test_init!();
- let data = format!("{}", client::gen_valid_conf());
- match Configuration::parse(&data) {
- Ok(..) => panic!("Accepted invalid configuration!"),
- Err(e) => {
- assert_eq!(e, "Missing required subgroup \"dbus\""
- .to_string());
- }
- };
- }
-}
diff --git a/src/configuration/dbus.rs b/src/configuration/dbus.rs
deleted file mode 100644
index 83a06dc..0000000
--- a/src/configuration/dbus.rs
+++ /dev/null
@@ -1,155 +0,0 @@
-//! Handles the `dbus` section of the configuration file.
-
-use toml;
-
-use super::common::{get_required_key, get_optional_key, ConfTreeParser, Result};
-
-/// Type to encode allowed keys for the `dbus` section of the configuration.
-#[derive(Clone)]
-pub struct DBusConfiguration {
- /// The DBus name that sota_client registers.
- pub name: String,
- /// The DBus path that sota_client registers.
- pub path: String,
- /// The interface name that sota_client provides.
- pub interface: String,
- /// The name and interface, where the software loading manager can be reached.
- pub software_manager: String,
- /// The name and interface, where the software loading manager can be reached.
- pub software_manager_path: String,
- /// Time to wait for installation of a package before it is considered a failure. In seconds.
- pub timeout: i32 // dbus-rs expects a signed int
-}
-
-#[cfg(test)]
-impl DBusConfiguration {
- /// Generate a test configuration.
- pub fn gen_test() -> DBusConfiguration {
- DBusConfiguration {
- name: "org.test.test".to_string(),
- path: "org.test.test".to_string(),
- interface: "org.test.test".to_string(),
- software_manager: "org.test.software_manager".to_string(),
- software_manager_path: "org.test.software_manager".to_string(),
- timeout: 20
- }
- }
-}
-
-impl ConfTreeParser<DBusConfiguration> for DBusConfiguration {
- fn parse(tree: &toml::Table) -> Result<DBusConfiguration> {
- let dbus_tree = try!(tree.get("dbus")
- .ok_or("Missing required subgroup \"dbus\""));
- let name = try!(get_required_key(dbus_tree, "name", "dbus"));
- let path = try!(get_required_key(dbus_tree, "path", "dbus"));
- let interface = try!(get_required_key(dbus_tree, "interface", "dbus"));
- let software_manager = try!(get_required_key(dbus_tree, "software_manager", "dbus"));
- let software_manager_path = try!(get_required_key(dbus_tree, "software_manager_path", "dbus"));
- let timeout = try!(get_optional_key(dbus_tree, "timeout", "dbus"));
-
- Ok(DBusConfiguration {
- name: name,
- path: path,
- interface: interface,
- software_manager: software_manager,
- software_manager_path: software_manager_path,
- timeout: timeout.unwrap_or(60) * 1000
- })
- }
-}
-
-#[cfg(test)] static NAME: &'static str = "org.genivi.sota_client";
-#[cfg(test)] static PATH: &'static str = "/org/genivi/sota_client";
-#[cfg(test)] static INTERFACE: &'static str = "org.genivi.software_manager";
-#[cfg(test)] static SOFTWARE_MANAGER: &'static str = "org.genivi.software_manager";
-#[cfg(test)] static SOFTWARE_MANAGER_PATH: &'static str = "/org/genivi/software_manager";
-
-#[cfg(test)]
-pub fn gen_valid_conf() -> String {
- format!(r#"
- [dbus]
- name = "{}"
- path = "{}"
- interface = "{}"
- software_manager = "{}"
- software_manager_path = "{}"
- "#, NAME, PATH, INTERFACE, SOFTWARE_MANAGER, SOFTWARE_MANAGER_PATH)
-}
-
-#[cfg(test)]
-pub fn assert_conf(conf: &DBusConfiguration) -> bool {
- assert_eq!(&conf.name, NAME);
- assert_eq!(&conf.path, PATH);
- assert_eq!(&conf.interface, INTERFACE);
- assert_eq!(&conf.software_manager, SOFTWARE_MANAGER);
- assert_eq!(&conf.software_manager_path, SOFTWARE_MANAGER_PATH);
- true
-}
-
-#[cfg(test)]
-mod test {
- use super::*;
- use super::{NAME, PATH, INTERFACE, SOFTWARE_MANAGER};
- use configuration::common::{ConfTreeParser, read_tree};
-
- #[test]
- fn it_requires_the_dbus_name_key() {
- test_init!();
- let data = format!(r#"
- [dbus]
- interface = "{}"
- software_manager = "{}"
- "#, INTERFACE, SOFTWARE_MANAGER);
-
- let tree = read_tree(&data).unwrap();
- match DBusConfiguration::parse(&tree) {
- Ok(..) => panic!("Accepted invalid configuration!"),
- Err(e) => {
- assert_eq!(e,
- "Missing required key \"name\" in \"dbus\""
- .to_string());
- }
- };
- }
-
- #[test]
- fn it_requires_the_dbus_interface_key() {
- test_init!();
- let data = format!(r#"
- [dbus]
- name = "{}"
- path = "{}"
- software_manager = "{}"
- "#, NAME, PATH, SOFTWARE_MANAGER);
-
- let tree = read_tree(&data).unwrap();
- match DBusConfiguration::parse(&tree) {
- Ok(..) => panic!("Accepted invalid configuration!"),
- Err(e) => {
- assert_eq!(e,
- "Missing required key \"interface\" in \"dbus\""
- .to_string());
- }
- };
- }
-
- #[test]
- fn it_requires_the_dbus_software_manager_key() {
- test_init!();
- let data = format!(r#"
- [dbus]
- name = "{}"
- path = "{}"
- interface = "{}"
- "#, NAME, PATH, INTERFACE);
-
- let tree = read_tree(&data).unwrap();
- match DBusConfiguration::parse(&tree) {
- Ok(..) => panic!("Accepted invalid configuration!"),
- Err(e) => {
- assert_eq!(e, "Missing required key \"software_manager\" \
- in \"dbus\"".to_string());
- }
- };
- }
-}
diff --git a/src/configuration/mod.rs b/src/configuration/mod.rs
deleted file mode 100644
index e2a986f..0000000
--- a/src/configuration/mod.rs
+++ /dev/null
@@ -1,12 +0,0 @@
-//! Parsing of the configuration file of `sota_client`.
-//!
-//! Also see the documentation for [`toml`](../../toml/index.html).
-
-mod configuration;
-mod common;
-mod client;
-mod dbus;
-
-pub use self::configuration::Configuration;
-pub use self::client::ClientConfiguration;
-pub use self::dbus::DBusConfiguration;
diff --git a/src/datatype/auth.rs b/src/datatype/auth.rs
new file mode 100644
index 0000000..cbfd097
--- /dev/null
+++ b/src/datatype/auth.rs
@@ -0,0 +1,49 @@
+use std::borrow::Cow;
+
+
+/// The available authentication types for communicating with the Auth server.
+#[derive(Clone, Debug)]
+pub enum Auth {
+ None,
+ Credentials(ClientId, ClientSecret),
+ Token(AccessToken),
+}
+
+impl<'a> Into<Cow<'a, Auth>> for Auth {
+ fn into(self) -> Cow<'a, Auth> {
+ Cow::Owned(self)
+ }
+}
+
+
+/// For storage of the returned access token data following a successful
+/// authentication.
+#[derive(RustcDecodable, Debug, PartialEq, Clone, Default)]
+pub struct AccessToken {
+ pub access_token: String,
+ pub token_type: String,
+ pub expires_in: i32,
+ pub scope: String
+}
+
+impl<'a> Into<Cow<'a, AccessToken>> for AccessToken {
+ fn into(self) -> Cow<'a, AccessToken> {
+ Cow::Owned(self)
+ }
+}
+
+
+/// Encapsulates a `String` type for use in `Auth::Credentials`
+#[derive(Clone, PartialEq, Eq, Debug, RustcEncodable, RustcDecodable)]
+pub struct ClientId(pub String);
+
+/// Encapsulates a `String` type for use in `Auth::Credentials`
+#[derive(Clone, PartialEq, Eq, Debug, RustcEncodable, RustcDecodable)]
+pub struct ClientSecret(pub String);
+
+/// Encapsulates the client id and secret used during authentication.
+#[derive(Clone, PartialEq, Eq, Debug, RustcEncodable, RustcDecodable)]
+pub struct ClientCredentials {
+ pub client_id: ClientId,
+ pub client_secret: ClientSecret,
+}
diff --git a/src/datatype/command.rs b/src/datatype/command.rs
new file mode 100644
index 0000000..d449bb6
--- /dev/null
+++ b/src/datatype/command.rs
@@ -0,0 +1,306 @@
+use std::fmt::{Display, Formatter, Result as FmtResult};
+use std::str;
+use std::str::FromStr;
+
+use nom::{IResult, space, eof};
+use datatype::{ClientCredentials, ClientId, ClientSecret, DownloadComplete, Error,
+ InstalledSoftware, Package, UpdateReport, UpdateRequestId,
+ UpdateResultCode};
+
+
+/// System-wide commands that are sent to the interpreter.
+#[derive(RustcDecodable, RustcEncodable, PartialEq, Eq, Debug, Clone)]
+pub enum Command {
+ /// Authenticate with the auth server.
+ Authenticate(Option<ClientCredentials>),
+ /// Shutdown the client immediately.
+ Shutdown,
+
+ /// Check for any new updates.
+ GetNewUpdates,
+ /// List the installed packages on the system.
+ ListInstalledPackages,
+ /// Get the latest system information, and optionally publish it to Core.
+ RefreshSystemInfo(bool),
+
+ /// Start downloading one or more updates.
+ StartDownload(Vec<UpdateRequestId>),
+ /// Start installing an update
+ StartInstall(DownloadComplete),
+
+ /// Send a list of packages to the Core server.
+ SendInstalledPackages(Vec<Package>),
+ /// Send a list of all packages and firmware to the Core server.
+ SendInstalledSoftware(InstalledSoftware),
+ /// Send a package update report to the Core server.
+ SendUpdateReport(UpdateReport),
+}
+
+impl Display for Command {
+ fn fmt(&self, f: &mut Formatter) -> FmtResult {
+ write!(f, "{:?}", self)
+ }
+}
+
+impl FromStr for Command {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Command, Error> {
+ match command(s.as_bytes()) {
+ IResult::Done(_, cmd) => parse_arguments(cmd.0, cmd.1.clone()),
+ _ => Err(Error::Command(format!("bad command: {}", s)))
+ }
+ }
+}
+
+
+named!(command <(Command, Vec<&str>)>, chain!(
+ space?
+ ~ cmd: alt!(
+ alt_complete!(tag!("Authenticate") | tag!("auth"))
+ => { |_| Command::Authenticate(None) }
+ | alt_complete!(tag!("GetNewUpdates") | tag!("new"))
+ => { |_| Command::GetNewUpdates }
+ | alt_complete!(tag!("ListInstalledPackages") | tag!("ls"))
+ => { |_| Command::ListInstalledPackages }
+ | alt_complete!(tag!("RefreshSystemInfo") | tag!("info"))
+ => { |_| Command::RefreshSystemInfo(false) }
+ | alt_complete!(tag!("Shutdown") | tag!("shutdown"))
+ => { |_| Command::Shutdown }
+ | alt_complete!(tag!("SendInstalledPackages") | tag!("sendpack"))
+ => { |_| Command::SendInstalledPackages(Vec::new()) }
+ | alt_complete!(tag!("SendInstalledSoftware") | tag!("sendinst"))
+ => { |_| Command::SendInstalledSoftware(InstalledSoftware::default()) }
+ | alt_complete!(tag!("SendUpdateReport") | tag!("sendup"))
+ => { |_| Command::SendUpdateReport(UpdateReport::default()) }
+ | alt_complete!(tag!("StartDownload") | tag!("dl"))
+ => { |_| Command::StartDownload(Vec::new()) }
+ | alt_complete!(tag!("StartInstall") | tag!("inst"))
+ => { |_| Command::StartInstall(DownloadComplete::default()) }
+ )
+ ~ args: arguments
+ ~ alt!(eof | tag!("\r") | tag!("\n") | tag!(";")),
+ move || { (cmd, args) }
+));
+
+named!(arguments <&[u8], Vec<&str> >, chain!(
+ args: many0!(chain!(
+ space?
+ ~ text: map_res!(is_not!(" \t\r\n;"), str::from_utf8)
+ ~ space?,
+ || { text }
+ )),
+ move || {
+ args.into_iter()
+ .filter(|arg| arg.len() > 0)
+ .collect()
+ }
+));
+
+fn parse_arguments(cmd: Command, args: Vec<&str>) -> Result<Command, Error> {
+ match cmd {
+ Command::Authenticate(_) => match args.len() {
+ 0 => Ok(Command::Authenticate(None)),
+ 1 => Err(Error::Command("usage: auth <client-id> <client-secret>".to_string())),
+ 2 => Ok(Command::Authenticate(Some(ClientCredentials {
+ client_id: ClientId(args[0].to_string()),
+ client_secret: ClientSecret(args[1].to_string())}))),
+ _ => Err(Error::Command(format!("unexpected Authenticate args: {:?}", args))),
+ },
+
+ Command::GetNewUpdates => match args.len() {
+ 0 => Ok(Command::GetNewUpdates),
+ _ => Err(Error::Command(format!("unexpected GetNewUpdates args: {:?}", args))),
+ },
+
+ Command::ListInstalledPackages => match args.len() {
+ 0 => Ok(Command::ListInstalledPackages),
+ _ => Err(Error::Command(format!("unexpected ListInstalledPackages args: {:?}", args))),
+ },
+
+ Command::RefreshSystemInfo(_) => match args.len() {
+ 0 => Ok(Command::RefreshSystemInfo(false)),
+ 1 => {
+ if let Ok(b) = args[0].parse::<bool>() {
+ Ok(Command::RefreshSystemInfo(b))
+ } else {
+ Err(Error::Command("couldn't parse 1st argument as boolean".to_string()))
+ }
+ }
+ _ => Err(Error::Command(format!("unexpected RefreshSystemInfo args: {:?}", args))),
+ },
+
+ Command::SendInstalledPackages(_) => match args.len() {
+ 0 | 1 => Err(Error::Command("usage: sendpack (<name> <version> )+".to_string())),
+ n if n % 2 == 0 => {
+ let (names, versions): (Vec<(_, &str)>, Vec<(_, &str)>) =
+ args.into_iter().enumerate().partition(|&(n, _)| n % 2 == 0);
+ let packages = names.into_iter().zip(versions.into_iter())
+ .map(|((_, name), (_, version))| Package {
+ name: name.to_string(),
+ version: version.to_string()
+ }).collect::<Vec<Package>>();
+ Ok(Command::SendInstalledPackages(packages))
+ }
+ _ => Err(Error::Command(format!("SendInstalledPackages expects an even number of 'name version' pairs"))),
+ },
+
+ Command::SendInstalledSoftware(_) => match args.len() {
+ // FIXME(PRO-1160): args
+ _ => Err(Error::Command(format!("unexpected SendInstalledSoftware args: {:?}", args))),
+ },
+
+ Command::SendUpdateReport(_) => match args.len() {
+ 0 | 1 => Err(Error::Command("usage: sendup <update-id> <result-code>".to_string())),
+ 2 => {
+ if let Ok(code) = args[1].parse::<UpdateResultCode>() {
+ Ok(Command::SendUpdateReport(UpdateReport::single(args[0].to_string(), code, "".to_string())))
+ } else {
+ Err(Error::Command("couldn't parse 2nd argument as an UpdateResultCode".to_string()))
+ }
+ }
+ _ => Err(Error::Command(format!("unexpected SendUpdateReport args: {:?}", args))),
+ },
+
+ Command::Shutdown => match args.len() {
+ 0 => Ok(Command::Shutdown),
+ _ => Err(Error::Command(format!("unexpected Shutdown args: {:?}", args))),
+ },
+
+ Command::StartDownload(_) => match args.len() {
+ 0 => Err(Error::Command("usage: dl [<id>]".to_string())),
+ _ => Ok(Command::StartDownload(args.iter().map(|arg| String::from(*arg)).collect())),
+ },
+
+ Command::StartInstall(_) => match args.len() {
+ // FIXME(PRO-1160): args
+ _ => Err(Error::Command(format!("unexpected StartInstall args: {:?}", args))),
+ },
+
+ }
+}
+
+
+#[cfg(test)]
+mod tests {
+ use super::{command, arguments};
+ use datatype::{Command, ClientCredentials, ClientId, ClientSecret, Package,
+ UpdateReport, UpdateResultCode};
+ use nom::IResult;
+
+
+ #[test]
+ fn parse_command_test() {
+ assert_eq!(command(&b"auth foo bar"[..]),
+ IResult::Done(&b""[..], (Command::Authenticate(None), vec!["foo", "bar"])));
+ assert_eq!(command(&b"dl 1"[..]),
+ IResult::Done(&b""[..], (Command::StartDownload(Vec::new()), vec!["1"])));
+ assert_eq!(command(&b"ls;\n"[..]),
+ IResult::Done(&b"\n"[..], (Command::ListInstalledPackages, Vec::new())));
+ }
+
+ #[test]
+ fn parse_arguments_test() {
+ assert_eq!(arguments(&b"one"[..]), IResult::Done(&b""[..], vec!["one"]));
+ assert_eq!(arguments(&b"foo bar"[..]), IResult::Done(&b""[..], vec!["foo", "bar"]));
+ assert_eq!(arguments(&b"n=5"[..]), IResult::Done(&b""[..], vec!["n=5"]));
+ assert_eq!(arguments(&b""[..]), IResult::Done(&b""[..], Vec::new()));
+ assert_eq!(arguments(&b" \t some"[..]), IResult::Done(&b""[..], vec!["some"]));
+ assert_eq!(arguments(&b";"[..]), IResult::Done(&b";"[..], Vec::new()));
+ }
+
+
+ #[test]
+ fn authenticate_test() {
+ assert_eq!("Authenticate".parse::<Command>().unwrap(), Command::Authenticate(None));
+ assert_eq!("auth".parse::<Command>().unwrap(), Command::Authenticate(None));
+ assert_eq!("auth user pass".parse::<Command>().unwrap(),
+ Command::Authenticate(Some(ClientCredentials {
+ client_id: ClientId("user".to_string()),
+ client_secret: ClientSecret("pass".to_string()),
+ })));
+ assert!("auth one".parse::<Command>().is_err());
+ assert!("auth one two three".parse::<Command>().is_err());
+ }
+
+ #[test]
+ fn get_new_updates_test() {
+ assert_eq!("GetNewUpdates".parse::<Command>().unwrap(), Command::GetNewUpdates);
+ assert_eq!("new".parse::<Command>().unwrap(), Command::GetNewUpdates);
+ assert!("new old".parse::<Command>().is_err());
+ }
+
+ #[test]
+ fn list_installed_test() {
+ assert_eq!("ListInstalledPackages".parse::<Command>().unwrap(), Command::ListInstalledPackages);
+ assert_eq!("ls".parse::<Command>().unwrap(), Command::ListInstalledPackages);
+ assert!("ls some".parse::<Command>().is_err());
+ }
+
+ #[test]
+ fn refresh_system_info_test() {
+ assert_eq!("RefreshSystemInfo true".parse::<Command>().unwrap(), Command::RefreshSystemInfo(true));
+ assert_eq!("info".parse::<Command>().unwrap(), Command::RefreshSystemInfo(false));
+ assert!("RefreshSystemInfo 1 2".parse::<Command>().is_err());
+ assert!("info please".parse::<Command>().is_err());
+ }
+
+ #[test]
+ fn send_installed_packages_test() {
+ assert_eq!("SendInstalledPackages myname myversion".parse::<Command>().unwrap(),
+ Command::SendInstalledPackages(vec![Package {
+ name: "myname".to_string(),
+ version: "myversion".to_string()
+ }]));
+ assert_eq!("sendpack n1 v1 n2 v2".parse::<Command>().unwrap(),
+ Command::SendInstalledPackages(vec![Package {
+ name: "n1".to_string(),
+ version: "v1".to_string()
+ }, Package {
+ name: "n2".to_string(),
+ version: "v2".to_string()
+ }]));
+ assert!("SendInstalledPackages some".parse::<Command>().is_err());
+ assert!("sendpack 1 2 3".parse::<Command>().is_err());
+ }
+
+ #[test]
+ fn send_installed_software_test() {
+ assert!("SendInstalledSoftware".parse::<Command>().is_err());
+ assert!("sendsoft some".parse::<Command>().is_err());
+ }
+
+ #[test]
+ fn send_update_report_test() {
+ assert_eq!("SendUpdateReport myid OK".parse::<Command>().unwrap(), Command::SendUpdateReport(
+ UpdateReport::single("myid".to_string(), UpdateResultCode::OK, "".to_string())));
+ assert_eq!("sendup myid 19".parse::<Command>().unwrap(), Command::SendUpdateReport(
+ UpdateReport::single("myid".to_string(), UpdateResultCode::GENERAL_ERROR, "".to_string())));
+ assert!("sendup myid 20".parse::<Command>().is_err());
+ assert!("SendInstalledPackages".parse::<Command>().is_err());
+ assert!("sendup 1 2 3".parse::<Command>().is_err());
+ }
+
+ #[test]
+ fn shutdown_test() {
+ assert_eq!("Shutdown".parse::<Command>().unwrap(), Command::Shutdown);
+ assert_eq!("shutdown".parse::<Command>().unwrap(), Command::Shutdown);
+ assert!("Shutdown 1 2".parse::<Command>().is_err());
+ assert!("shutdown now".parse::<Command>().is_err());
+ }
+
+ #[test]
+ fn start_download_test() {
+ assert_eq!("StartDownload this".parse::<Command>().unwrap(),
+ Command::StartDownload(vec!["this".to_string()]));
+ assert_eq!("dl some more".parse::<Command>().unwrap(),
+ Command::StartDownload(vec!["some".to_string(), "more".to_string()]));
+ assert!("dl".parse::<Command>().is_err());
+ }
+
+ #[test]
+ fn start_install_test() {
+ assert!("StartInstall".parse::<Command>().is_err());
+ assert!("inst more than one".parse::<Command>().is_err());
+ }
+}
diff --git a/src/datatype/config.rs b/src/datatype/config.rs
new file mode 100644
index 0000000..5db0f12
--- /dev/null
+++ b/src/datatype/config.rs
@@ -0,0 +1,389 @@
+use rustc_serialize::Decodable;
+use std::fs;
+use std::fs::File;
+use std::io::ErrorKind;
+use std::os::unix::fs::PermissionsExt;
+use std::io::prelude::*;
+use std::path::Path;
+use toml;
+use toml::{Decoder, Parser, Table, Value};
+
+use datatype::{Error, SystemInfo, Url};
+use package_manager::PackageManager;
+
+
+/// An aggregation of all the configuration options parsed at startup.
+#[derive(Default, PartialEq, Eq, Debug, Clone)]
+pub struct Config {
+ pub auth: Option<AuthConfig>,
+ pub core: CoreConfig,
+ pub dbus: Option<DBusConfig>,
+ pub device: DeviceConfig,
+ pub gateway: GatewayConfig,
+ pub network: NetworkConfig,
+ pub rvi: Option<RviConfig>,
+}
+
+impl Config {
+ pub fn load(path: &str) -> Result<Config, Error> {
+ info!("Loading config file: {}", path);
+ let mut file = try!(File::open(path).map_err(Error::Io));
+ let mut toml = String::new();
+ try!(file.read_to_string(&mut toml));
+ Config::parse(&toml)
+ }
+
+ pub fn parse(toml: &str) -> Result<Config, Error> {
+ let table = try!(parse_table(&toml));
+
+ let auth_cfg = if let Some(auth) = table.get("auth") {
+ let parsed = try!(decode_section(auth.clone()));
+ Some(try!(bootstrap_credentials(parsed)))
+ } else {
+ None
+ };
+
+ let dbus_cfg = if let Some(dbus) = table.get("dbus") {
+ Some(try!(decode_section(dbus.clone())))
+ } else {
+ None
+ };
+
+ let rvi_cfg = if let Some(rvi) = table.get("rvi") {
+ Some(try!(decode_section(rvi.clone())))
+ } else {
+ None
+ };
+
+ Ok(Config {
+ auth: auth_cfg,
+ core: try!(read_section(&table, "core")),
+ dbus: dbus_cfg,
+ device: try!(read_section(&table, "device")),
+ gateway: try!(read_section(&table, "gateway")),
+ network: try!(read_section(&table, "network")),
+ rvi: rvi_cfg,
+ })
+ }
+}
+
+fn parse_table(toml: &str) -> Result<Table, Error> {
+ let mut parser = Parser::new(toml);
+ Ok(try!(parser.parse().ok_or_else(move || parser.errors)))
+}
+
+fn read_section<T: Decodable>(table: &Table, section: &str) -> Result<T, Error> {
+ let part = try!(table.get(section)
+ .ok_or_else(|| Error::Parse(format!("invalid section: {}", section))));
+ decode_section(part.clone())
+}
+
+fn decode_section<T: Decodable>(section: Value) -> Result<T, Error> {
+ let mut decoder = Decoder::new(section);
+ Ok(try!(T::decode(&mut decoder)))
+}
+
+
+#[derive(RustcEncodable, RustcDecodable)]
+struct CredentialsFile {
+ pub client_id: String,
+ pub client_secret: String,
+}
+
+// Read AuthConfig values from the credentials file if it exists, or write the
+// current AuthConfig values to a new credentials file otherwise.
+fn bootstrap_credentials(auth_cfg: AuthConfig) -> Result<AuthConfig, Error> {
+ let creds = auth_cfg.credentials_file.clone();
+ let path = Path::new(&creds);
+ debug!("bootstrap_credentials: {:?}", path);
+
+ let credentials = match File::open(path) {
+ Ok(mut file) => {
+ let mut text = String::new();
+ try!(file.read_to_string(&mut text));
+ let table = try!(parse_table(&text));
+ try!(read_section::<CredentialsFile>(&table, "auth"))
+ }
+
+ Err(ref err) if err.kind() == ErrorKind::NotFound => {
+ let mut table = Table::new();
+ let credentials = CredentialsFile {
+ client_id: auth_cfg.client_id,
+ client_secret: auth_cfg.client_secret
+ };
+ table.insert("auth".to_string(), toml::encode(&credentials));
+
+ let dir = try!(path.parent().ok_or(Error::Parse("Invalid credentials file path".to_string())));
+ try!(fs::create_dir_all(&dir));
+ let mut file = try!(File::create(path));
+ let mut perms = try!(file.metadata()).permissions();
+ perms.set_mode(0o600);
+ try!(fs::set_permissions(path, perms));
+ try!(file.write_all(&toml::encode_str(&table).into_bytes()));
+
+ credentials
+ }
+
+ Err(err) => return Err(Error::Io(err))
+ };
+
+ Ok(AuthConfig {
+ server: auth_cfg.server,
+ client_id: credentials.client_id,
+ client_secret: credentials.client_secret,
+ credentials_file: auth_cfg.credentials_file,
+ })
+}
+
+
+/// A parsed representation of the [auth] configuration section.
+#[derive(RustcDecodable, PartialEq, Eq, Debug, Clone)]
+pub struct AuthConfig {
+ pub server: Url,
+ pub client_id: String,
+ pub client_secret: String,
+ pub credentials_file: String,
+}
+
+impl Default for AuthConfig {
+ fn default() -> AuthConfig {
+ AuthConfig {
+ server: "http://127.0.0.1:9001".parse().unwrap(),
+ client_id: "client-id".to_string(),
+ client_secret: "client-secret".to_string(),
+ credentials_file: "/tmp/sota_credentials.toml".to_string(),
+ }
+ }
+}
+
+
+/// A parsed representation of the [core] configuration section.
+#[derive(RustcDecodable, PartialEq, Eq, Debug, Clone)]
+pub struct CoreConfig {
+ pub server: Url
+}
+
+impl Default for CoreConfig {
+ fn default() -> CoreConfig {
+ CoreConfig {
+ server: "http://127.0.0.1:8080".parse().unwrap()
+ }
+ }
+}
+
+
+/// A parsed representation of the [dbus] configuration section.
+#[derive(RustcDecodable, PartialEq, Eq, Debug, Clone)]
+pub struct DBusConfig {
+ pub name: String,
+ pub path: String,
+ pub interface: String,
+ pub software_manager: String,
+ pub software_manager_path: String,
+ pub timeout: i32, // dbus-rs expects a signed int
+}
+
+impl Default for DBusConfig {
+ fn default() -> DBusConfig {
+ DBusConfig {
+ name: "org.genivi.SotaClient".to_string(),
+ path: "/org/genivi/SotaClient".to_string(),
+ interface: "org.genivi.SotaClient".to_string(),
+ software_manager: "org.genivi.SoftwareLoadingManager".to_string(),
+ software_manager_path: "/org/genivi/SoftwareLoadingManager".to_string(),
+ timeout: 60
+ }
+ }
+}
+
+
+/// A parsed representation of the [device] configuration section.
+#[derive(RustcDecodable, PartialEq, Eq, Debug, Clone)]
+pub struct DeviceConfig {
+ pub uuid: String,
+ pub vin: String,
+ pub packages_dir: String,
+ pub package_manager: PackageManager,
+ pub system_info: SystemInfo,
+ pub polling_interval: u64,
+ pub certificates_path: String,
+}
+
+impl Default for DeviceConfig {
+ fn default() -> DeviceConfig {
+ DeviceConfig {
+ uuid: "123e4567-e89b-12d3-a456-426655440000".to_string(),
+ vin: "V1234567890123456".to_string(),
+ packages_dir: "/tmp/".to_string(),
+ package_manager: PackageManager::Deb,
+ system_info: SystemInfo::default(),
+ polling_interval: 10,
+ certificates_path: "/tmp/sota_certificates".to_string()
+ }
+ }
+}
+
+
+/// A parsed representation of the [gateway] configuration section.
+#[derive(RustcDecodable, PartialEq, Eq, Debug, Clone)]
+pub struct GatewayConfig {
+ pub console: bool,
+ pub dbus: bool,
+ pub http: bool,
+ pub rvi: bool,
+ pub socket: bool,
+ pub websocket: bool,
+}
+
+impl Default for GatewayConfig {
+ fn default() -> GatewayConfig {
+ GatewayConfig {
+ console: false,
+ dbus: false,
+ http: false,
+ rvi: false,
+ socket: false,
+ websocket: true,
+ }
+ }
+}
+
+
+/// A parsed representation of the [network] configuration section.
+#[derive(RustcDecodable, PartialEq, Eq, Debug, Clone)]
+pub struct NetworkConfig {
+ pub http_server: String,
+ pub rvi_edge_server: String,
+ pub socket_commands_path: String,
+ pub socket_events_path: String,
+ pub websocket_server: String
+}
+
+impl Default for NetworkConfig {
+ fn default() -> NetworkConfig {
+ NetworkConfig {
+ http_server: "http://127.0.0.1:8888".to_string(),
+ rvi_edge_server: "http://127.0.0.1:9080".to_string(),
+ socket_commands_path: "/tmp/sota-commands.socket".to_string(),
+ socket_events_path: "/tmp/sota-events.socket".to_string(),
+ websocket_server: "ws://127.0.0.1:3012".to_string()
+ }
+ }
+}
+
+
+/// A parsed representation of the [rvi] configuration section.
+#[derive(RustcDecodable, PartialEq, Eq, Debug, Clone)]
+pub struct RviConfig {
+ pub client: Url,
+ pub storage_dir: String,
+ pub timeout: Option<i64>,
+}
+
+impl Default for RviConfig {
+ fn default() -> RviConfig {
+ RviConfig {
+ client: "http://127.0.0.1:8901".parse().unwrap(),
+ storage_dir: "/var/sota".to_string(),
+ timeout: Some(20),
+ }
+ }
+}
+
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+
+ const AUTH_CONFIG: &'static str =
+ r#"
+ [auth]
+ server = "http://127.0.0.1:9001"
+ client_id = "client-id"
+ client_secret = "client-secret"
+ credentials_file = "/tmp/sota_credentials.toml"
+ "#;
+
+ const CORE_CONFIG: &'static str =
+ r#"
+ [core]
+ server = "http://127.0.0.1:8080"
+ "#;
+
+ const DBUS_CONFIG: &'static str =
+ r#"
+ [dbus]
+ name = "org.genivi.SotaClient"
+ path = "/org/genivi/SotaClient"
+ interface = "org.genivi.SotaClient"
+ software_manager = "org.genivi.SoftwareLoadingManager"
+ software_manager_path = "/org/genivi/SoftwareLoadingManager"
+ timeout = 60
+ "#;
+
+ const DEVICE_CONFIG: &'static str =
+ r#"
+ [device]
+ uuid = "123e4567-e89b-12d3-a456-426655440000"
+ vin = "V1234567890123456"
+ system_info = "system_info.sh"
+ polling_interval = 10
+ packages_dir = "/tmp/"
+ package_manager = "deb"
+ certificates_path = "/tmp/sota_certificates"
+ "#;
+
+ const GATEWAY_CONFIG: &'static str =
+ r#"
+ [gateway]
+ console = false
+ dbus = false
+ http = false
+ rvi = false
+ socket = false
+ websocket = true
+ "#;
+
+ const NETWORK_CONFIG: &'static str =
+ r#"
+ [network]
+ http_server = "http://127.0.0.1:8888"
+ rvi_edge_server = "http://127.0.0.1:9080"
+ socket_commands_path = "/tmp/sota-commands.socket"
+ socket_events_path = "/tmp/sota-events.socket"
+ websocket_server = "ws://127.0.0.1:3012"
+ "#;
+
+ const RVI_CONFIG: &'static str =
+ r#"
+ [rvi]
+ client = "http://127.0.0.1:8901"
+ storage_dir = "/var/sota"
+ timeout = 20
+ "#;
+
+
+ #[test]
+ fn parse_default_config() {
+ let config = String::new()
+ + CORE_CONFIG
+ + DEVICE_CONFIG
+ + GATEWAY_CONFIG
+ + NETWORK_CONFIG;
+ assert_eq!(Config::parse(&config).unwrap(), Config::default());
+ }
+
+ #[test]
+ fn parse_example_config() {
+ let config = String::new()
+ + AUTH_CONFIG
+ + CORE_CONFIG
+ + DBUS_CONFIG
+ + DEVICE_CONFIG
+ + GATEWAY_CONFIG
+ + NETWORK_CONFIG
+ + RVI_CONFIG;
+ assert_eq!(Config::load("tests/sota.toml").unwrap(), Config::parse(&config).unwrap());
+ }
+}
diff --git a/src/datatype/dbus.rs b/src/datatype/dbus.rs
new file mode 100644
index 0000000..a038568
--- /dev/null
+++ b/src/datatype/dbus.rs
@@ -0,0 +1,81 @@
+use dbus::{FromMessageItem, MessageItem};
+use toml::{decode, Table, Value};
+
+use datatype::update_report::{InstalledFirmware, InstalledPackage, OperationResult};
+
+
+static MISSING_ARG: &'static str = "Error.MissingArgument";
+static MALFORMED_ARG: &'static str = "Error.MalformedArgument";
+
+/// Format a `DBus` error message indicating a missing argument.
+pub fn missing_arg() -> (&'static str, String) {
+ (MISSING_ARG, "Missing argument".to_string())
+}
+
+/// Format a `DBus` error message indicating a malformed argument.
+pub fn malformed_arg() -> (&'static str, String) {
+ (MALFORMED_ARG, "Malformed argument".to_string())
+}
+
+
+struct DecodedValue(pub Value);
+
+impl<'m> FromMessageItem<'m> for DecodedValue {
+ fn from(m: &'m MessageItem) -> Result<Self, ()> {
+ match *m {
+ MessageItem::Str(ref b) => Ok(DecodedValue(Value::String(b.clone()))),
+ MessageItem::Bool(ref b) => Ok(DecodedValue(Value::Boolean(*b))),
+ MessageItem::Byte(ref b) => Ok(DecodedValue(Value::Integer(*b as i64))),
+ MessageItem::Int16(ref b) => Ok(DecodedValue(Value::Integer(*b as i64))),
+ MessageItem::Int32(ref b) => Ok(DecodedValue(Value::Integer(*b as i64))),
+ MessageItem::Int64(ref b) => Ok(DecodedValue(Value::Integer(*b as i64))),
+ MessageItem::UInt16(ref b) => Ok(DecodedValue(Value::Integer(*b as i64))),
+ MessageItem::UInt32(ref b) => Ok(DecodedValue(Value::Integer(*b as i64))),
+ MessageItem::UInt64(ref b) => Ok(DecodedValue(Value::Integer(*b as i64))),
+ MessageItem::Variant(ref b) => FromMessageItem::from(&**b),
+ _ => Err(())
+ }
+ }
+}
+
+
+struct DecodedStruct(pub Value);
+
+impl<'m> FromMessageItem<'m> for DecodedStruct {
+ fn from(item: &'m MessageItem) -> Result<Self, ()> {
+ let items: &Vec<MessageItem> = try!(FromMessageItem::from(item));
+ items.iter().map(|entry| {
+ let entry: Result<(&MessageItem, &MessageItem), ()> = FromMessageItem::from(entry);
+ entry.and_then(|(key, val)| {
+ let key: Result<&String,()> = FromMessageItem::from(key);
+ key.and_then(|key| {
+ let val: Result<DecodedValue,()> = FromMessageItem::from(val);
+ val.map(|val| (key.clone(), val.0))
+ })
+ })
+ }).collect::<Result<Vec<(_, _)>, ()>>()
+ .map(|arr| DecodedStruct(Value::Table(arr.into_iter().collect::<Table>())))
+ }
+}
+
+
+impl<'m> FromMessageItem<'m> for OperationResult {
+ fn from(item: &'m MessageItem) -> Result<Self, ()> {
+ let item: DecodedStruct = try!(FromMessageItem::from(item));
+ decode::<OperationResult>(item.0).ok_or(())
+ }
+}
+
+impl<'m> FromMessageItem<'m> for InstalledPackage {
+ fn from(item: &'m MessageItem) -> Result<Self, ()> {
+ let item: DecodedStruct = try!(FromMessageItem::from(item));
+ decode::<InstalledPackage>(item.0).ok_or(())
+ }
+}
+
+impl<'m> FromMessageItem<'m> for InstalledFirmware {
+ fn from(item: &'m MessageItem) -> Result<Self, ()> {
+ let item: DecodedStruct = try!(FromMessageItem::from(item));
+ decode::<InstalledFirmware>(item.0).ok_or(())
+ }
+}
diff --git a/src/datatype/error.rs b/src/datatype/error.rs
new file mode 100644
index 0000000..8267234
--- /dev/null
+++ b/src/datatype/error.rs
@@ -0,0 +1,119 @@
+use hyper::error::Error as HyperError;
+use hyper::client::ClientError as HyperClientError;
+use rustc_serialize::json::{EncoderError as JsonEncoderError,
+ DecoderError as JsonDecoderError,
+ ParserError as JsonParserError};
+use std::convert::From;
+use std::fmt::{Display, Formatter, Result as FmtResult};
+use std::io::Error as IoError;
+use std::string::FromUtf8Error;
+use std::sync::PoisonError;
+use std::sync::mpsc::{SendError, RecvError};
+use toml::{ParserError as TomlParserError, DecodeError as TomlDecodeError};
+use url::ParseError as UrlParseError;
+
+use datatype::Event;
+use http::auth_client::AuthHandler;
+use gateway::Interpret;
+use ws::Error as WebsocketError;
+
+
+/// System-wide errors that are returned from `Result` type failures.
+#[derive(Debug)]
+pub enum Error {
+ Authorization(String),
+ Client(String),
+ Command(String),
+ FromUtf8(FromUtf8Error),
+ Hyper(HyperError),
+ HyperClient(HyperClientError<AuthHandler>),
+ Io(IoError),
+ JsonDecoder(JsonDecoderError),
+ JsonEncoder(JsonEncoderError),
+ JsonParser(JsonParserError),
+ Poison(String),
+ Package(String),
+ Parse(String),
+ Recv(RecvError),
+ SendEvent(SendError<Event>),
+ SendInterpret(SendError<Interpret>),
+ Socket(String),
+ SystemInfo(String),
+ TomlParser(Vec<TomlParserError>),
+ TomlDecode(TomlDecodeError),
+ UrlParse(UrlParseError),
+ Websocket(WebsocketError),
+}
+
+impl<E> From<PoisonError<E>> for Error {
+ fn from(e: PoisonError<E>) -> Error {
+ Error::Poison(format!("{}", e))
+ }
+}
+
+macro_rules! derive_from {
+ ([ $( $from: ident => $to: ident ),* ]) => {
+ $(impl From<$from> for Error {
+ fn from(e: $from) -> Error {
+ Error::$to(e)
+ }
+ })*
+ };
+
+ ([ $( $error: ident < $ty: ty > => $to: ident),* ]) => {
+ $(impl From<$error<$ty>> for Error {
+ fn from(e: $error<$ty>) -> Error {
+ Error::$to(e)
+ }
+ })*
+ }
+}
+
+derive_from!([
+ FromUtf8Error => FromUtf8,
+ HyperError => Hyper,
+ IoError => Io,
+ JsonEncoderError => JsonEncoder,
+ JsonDecoderError => JsonDecoder,
+ RecvError => Recv,
+ TomlDecodeError => TomlDecode,
+ UrlParseError => UrlParse,
+ WebsocketError => Websocket
+]);
+
+derive_from!([
+ HyperClientError<AuthHandler> => HyperClient,
+ SendError<Event> => SendEvent,
+ SendError<Interpret> => SendInterpret,
+ Vec<TomlParserError> => TomlParser
+]);
+
+impl Display for Error {
+ fn fmt(&self, f: &mut Formatter) -> FmtResult {
+ let inner: String = match *self {
+ Error::Client(ref s) => format!("Http client error: {}", s.clone()),
+ Error::Authorization(ref s) => format!("Http client authorization error: {}", s.clone()),
+ Error::Command(ref e) => format!("Unknown Command: {}", e.clone()),
+ Error::FromUtf8(ref e) => format!("From utf8 error: {}", e.clone()),
+ Error::Hyper(ref e) => format!("Hyper error: {}", e.clone()),
+ Error::HyperClient(ref e) => format!("Hyper client error: {}", e.clone()),
+ Error::Io(ref e) => format!("IO error: {}", e.clone()),
+ Error::JsonDecoder(ref e) => format!("Failed to decode JSON: {}", e.clone()),
+ Error::JsonEncoder(ref e) => format!("Failed to encode JSON: {}", e.clone()),
+ Error::JsonParser(ref e) => format!("Failed to parse JSON: {}", e.clone()),
+ Error::Poison(ref e) => format!("Poison error: {}", e.clone()),
+ Error::Package(ref s) => format!("Package error: {}", s.clone()),
+ Error::Parse(ref s) => format!("Parse error: {}", s.clone()),
+ Error::Recv(ref s) => format!("Recv error: {}", s.clone()),
+ Error::SendEvent(ref s) => format!("Send error for Event: {}", s.clone()),
+ Error::SendInterpret(ref s) => format!("Send error for Interpret: {}", s.clone()),
+ Error::Socket(ref s) => format!("Unix Domain Socket error: {}", s.clone()),
+ Error::SystemInfo(ref s) => format!("System info error: {}", s.clone()),
+ Error::TomlDecode(ref e) => format!("Toml decode error: {}", e.clone()),
+ Error::TomlParser(ref e) => format!("Toml parser errors: {:?}", e.clone()),
+ Error::UrlParse(ref s) => format!("Url parse error: {}", s.clone()),
+ Error::Websocket(ref e) => format!("Websocket Error: {:?}", e.clone()),
+ };
+ write!(f, "{}", inner)
+ }
+}
diff --git a/src/datatype/event.rs b/src/datatype/event.rs
new file mode 100644
index 0000000..e3f84ca
--- /dev/null
+++ b/src/datatype/event.rs
@@ -0,0 +1,56 @@
+use std::fmt::{Display, Formatter, Result as FmtResult};
+
+use datatype::{DownloadComplete, Package, UpdateAvailable, UpdateReport,
+ UpdateRequestId};
+
+
+/// System-wide events that are broadcast to all interested parties.
+#[derive(RustcEncodable, RustcDecodable, Debug, Clone, PartialEq, Eq)]
+pub enum Event {
+ /// General error event with a printable representation for debugging.
+ Error(String),
+
+ /// Authentication was successful.
+ Authenticated,
+ /// An operation failed because we are not currently authenticated.
+ NotAuthenticated,
+
+ /// There are new updates available.
+ NewUpdatesReceived(Vec<UpdateRequestId>),
+ /// A notification from RVI of a new update.
+ NewUpdateAvailable(UpdateAvailable),
+ /// There are no new updates available.
+ NoNewUpdates,
+
+ /// The following packages are installed on the device.
+ FoundInstalledPackages(Vec<Package>),
+ /// An update on the system information was received.
+ FoundSystemInfo(String),
+ /// A list of installed packages was sent to the Core server.
+ InstalledPackagesSent,
+ /// An update report was sent to the Core server.
+ UpdateReportSent,
+
+ /// Downloading an update.
+ DownloadingUpdate(UpdateRequestId),
+ /// An update was downloaded.
+ DownloadComplete(DownloadComplete),
+ /// Downloading an update failed.
+ DownloadFailed(UpdateRequestId, String),
+
+ /// Installing an update.
+ InstallingUpdate(UpdateRequestId),
+ /// An update was installed.
+ InstallComplete(UpdateReport),
+ /// The installation of an update failed.
+ InstallFailed(UpdateReport),
+
+ /// A broadcast event requesting an update on externally installed software.
+ InstalledSoftwareNeeded,
+}
+
+impl Display for Event {
+ fn fmt(&self, f: &mut Formatter) -> FmtResult {
+ write!(f, "{:?}", self)
+ }
+}
diff --git a/src/datatype/json_rpc.rs b/src/datatype/json_rpc.rs
new file mode 100644
index 0000000..3eed9a2
--- /dev/null
+++ b/src/datatype/json_rpc.rs
@@ -0,0 +1,107 @@
+use rustc_serialize::{json, Decodable, Encodable};
+use time;
+
+use http::{AuthClient, Client};
+use super::Url;
+
+
+/// Encode the body of a JSON-RPC call.
+#[derive(RustcDecodable, RustcEncodable)]
+pub struct RpcRequest<E: Encodable> {
+ pub jsonrpc: String,
+ pub id: u64,
+ pub method: String,
+ pub params: E
+}
+
+impl<E: Encodable> RpcRequest<E> {
+ /// Instantiate a new `RpcRequest` with the default version (2.0) and an id
+ /// generated from the current time.
+ pub fn new(method: &str, params: E) -> RpcRequest<E> {
+ RpcRequest {
+ jsonrpc: "2.0".to_string(),
+ id: time::precise_time_ns(),
+ method: method.to_string(),
+ params: params
+ }
+ }
+
+ /// Send a JSON-RPC POST request to the specified URL.
+ pub fn send(&self, url: Url) -> Result<String, String> {
+ let client = AuthClient::default();
+ let body = json::encode(self).expect("couldn't encode RpcRequest");
+ let resp_rx = client.post(url, Some(body.into_bytes()));
+ let resp = resp_rx.recv().expect("no RpcRequest response received");
+ let data = try!(resp.map_err(|err| format!("{}", err)));
+ String::from_utf8(data).map_err(|err| format!("{}", err))
+ }
+}
+
+
+/// Encapsulates a successful JSON-RPC response.
+#[derive(RustcDecodable, RustcEncodable)]
+pub struct RpcOk<D: Decodable> {
+ pub jsonrpc: String,
+ pub id: u64,
+ pub result: Option<D>
+}
+
+impl<D: Decodable> RpcOk<D> {
+ /// Instantiate a new successful JSON-RPC response type.
+ pub fn new(id: u64, result: Option<D>) -> RpcOk<D> {
+ RpcOk {
+ jsonrpc: "2.0".to_string(),
+ id: id,
+ result: result
+ }
+ }
+}
+
+
+/// The error code as [specified by jsonrpc](http://www.jsonrpc.org/specification#error_object).
+#[derive(RustcDecodable, RustcEncodable)]
+pub struct ErrorCode {
+ pub code: i32,
+ pub message: String,
+ pub data: String
+}
+
+/// Encapsulates a failed JSON-RPC response.
+#[derive(RustcDecodable, RustcEncodable)]
+pub struct RpcErr {
+ pub jsonrpc: String,
+ pub id: u64,
+ pub error: ErrorCode
+}
+
+impl RpcErr {
+ /// Instantiate a new `RpcErr` type with the default JSON-RPC version (2.0).
+ pub fn new(id: u64, error: ErrorCode) -> Self {
+ RpcErr { jsonrpc: "2.0".to_string(), id: id, error: error }
+ }
+
+ /// Create a new `RpcErr` with a reason of "Invalid Request".
+ pub fn invalid_request(id: u64, data: String) -> Self {
+ Self::new(id, ErrorCode { code: -32600, message: "Invalid Request".to_string(), data: data })
+ }
+
+ /// Create a new `RpcErr` with a reason of "Method not found".
+ pub fn method_not_found(id: u64, data: String) -> Self {
+ Self::new(id, ErrorCode { code: -32601, message: "Method not found".to_string(), data: data })
+ }
+
+ /// Create a new `RpcErr` with a reason of "Parse error".
+ pub fn parse_error(data: String) -> Self {
+ Self::new(0, ErrorCode { code: -32700, message: "Parse error".to_string(), data: data })
+ }
+
+ /// Create a new `RpcErr` with a reason of "Invalid params".
+ pub fn invalid_params(id: u64, data: String) -> Self {
+ Self::new(id, ErrorCode { code: -32602, message: "Invalid params".to_string(), data: data })
+ }
+
+ /// Create a new `RpcErr` with a reason of "Couldn't handle request".
+ pub fn unspecified(id: u64, data: String) -> Self {
+ Self::new(id, ErrorCode { code: -32100, message: "Couldn't handle request".to_string(), data: data })
+ }
+}
diff --git a/src/datatype/mod.rs b/src/datatype/mod.rs
new file mode 100644
index 0000000..8a9ca4e
--- /dev/null
+++ b/src/datatype/mod.rs
@@ -0,0 +1,26 @@
+pub mod auth;
+pub mod command;
+pub mod config;
+pub mod dbus;
+pub mod error;
+pub mod event;
+pub mod json_rpc;
+pub mod package;
+pub mod system_info;
+pub mod update_report;
+pub mod url;
+
+pub use self::auth::{AccessToken, Auth, ClientId, ClientSecret, ClientCredentials};
+pub use self::command::Command;
+pub use self::config::{AuthConfig, CoreConfig, Config, DBusConfig, DeviceConfig,
+ GatewayConfig, RviConfig};
+pub use self::error::Error;
+pub use self::event::Event;
+pub use self::json_rpc::{RpcRequest, RpcOk, RpcErr};
+pub use self::package::{ChunkReceived, DownloadStarted, DownloadComplete, Package,
+ PendingUpdateRequest, UpdateAvailable, UpdateRequestId};
+pub use self::system_info::SystemInfo;
+pub use self::update_report::{DeviceReport, InstalledFirmware, InstalledPackage,
+ InstalledSoftware, OperationResult, UpdateResultCode,
+ UpdateReport};
+pub use self::url::{Method, Url};
diff --git a/src/datatype/package.rs b/src/datatype/package.rs
new file mode 100644
index 0000000..146ff06
--- /dev/null
+++ b/src/datatype/package.rs
@@ -0,0 +1,79 @@
+use std::fmt::{Display, Formatter, Result as FmtResult};
+
+use rvi::services::LocalServices;
+
+
+/// Encapsulate a `String` type used to represent the `Package` version.
+pub type Version = String;
+
+/// Encodes the name and version of a specific package.
+#[derive(Debug, PartialEq, Eq, RustcEncodable, RustcDecodable, Clone)]
+pub struct Package {
+ pub name: String,
+ pub version: Version
+}
+
+impl Display for Package {
+ fn fmt(&self, f: &mut Formatter) -> FmtResult {
+ write!(f, "{} {}", self.name, self.version)
+ }
+}
+
+
+/// Encapsulate a `String` type as the id of a specific update request.
+pub type UpdateRequestId = String;
+
+/// A single pending update request to be installed by the client.
+#[allow(non_snake_case)]
+#[derive(Clone, PartialEq, Eq, Debug, RustcEncodable, RustcDecodable)]
+pub struct PendingUpdateRequest {
+ pub requestId: UpdateRequestId,
+ pub installPos: i32,
+ pub packageId: Package,
+ pub createdAt: String
+}
+
+/// A notification from RVI that a new update is available.
+#[derive(RustcDecodable, RustcEncodable, PartialEq, Eq, Debug, Clone)]
+pub struct UpdateAvailable {
+ pub update_id: String,
+ pub signature: String,
+ pub description: String,
+ pub request_confirmation: bool,
+ pub size: u64
+}
+
+/// A JSON-RPC request type to notify RVI that a new package download has started.
+#[derive(RustcDecodable, RustcEncodable)]
+pub struct DownloadStarted {
+ pub device: String,
+ pub update_id: UpdateRequestId,
+ pub services: LocalServices,
+}
+
+/// A JSON-RPC request type to notify RVI that a new package chunk was received.
+#[derive(RustcDecodable, RustcEncodable)]
+pub struct ChunkReceived {
+ pub device: String,
+ pub update_id: UpdateRequestId,
+ pub chunks: Vec<u64>,
+}
+
+/// A notification to indicate to any external package manager that the package
+/// download has successfully completed.
+#[derive(RustcDecodable, RustcEncodable, PartialEq, Eq, Debug, Clone)]
+pub struct DownloadComplete {
+ pub update_id: String,
+ pub update_image: String,
+ pub signature: String
+}
+
+impl Default for DownloadComplete {
+ fn default() -> Self {
+ DownloadComplete {
+ update_id: "".to_string(),
+ update_image: "".to_string(),
+ signature: "".to_string()
+ }
+ }
+}
diff --git a/src/datatype/system_info.rs b/src/datatype/system_info.rs
new file mode 100644
index 0000000..2d8fff2
--- /dev/null
+++ b/src/datatype/system_info.rs
@@ -0,0 +1,46 @@
+use rustc_serialize::{Decoder, Decodable};
+use std::process::Command;
+use std::str::FromStr;
+
+use datatype::Error;
+
+
+/// A reference to the command that will report on the system information.
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub struct SystemInfo {
+ command: String
+}
+
+impl SystemInfo {
+ /// Instantiate a new type to report on the system information.
+ pub fn new(command: String) -> SystemInfo {
+ SystemInfo { command: command }
+ }
+
+ /// Generate a new report of the system information.
+ pub fn report(&self) -> Result<String, Error> {
+ Command::new(&self.command)
+ .output().map_err(|err| Error::SystemInfo(err.to_string()))
+ .and_then(|info| String::from_utf8(info.stdout).map_err(Error::FromUtf8))
+ }
+}
+
+impl Default for SystemInfo {
+ fn default() -> SystemInfo {
+ SystemInfo::new("system_info.sh".to_string())
+ }
+}
+
+impl FromStr for SystemInfo {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<SystemInfo, Error> {
+ Ok(SystemInfo::new(s.to_string()))
+ }
+}
+
+impl Decodable for SystemInfo {
+ fn decode<D: Decoder>(d: &mut D) -> Result<SystemInfo, D::Error> {
+ d.read_str().and_then(|s| Ok(s.parse::<SystemInfo>().unwrap()))
+ }
+}
diff --git a/src/datatype/update_report.rs b/src/datatype/update_report.rs
new file mode 100644
index 0000000..f81fbc4
--- /dev/null
+++ b/src/datatype/update_report.rs
@@ -0,0 +1,182 @@
+use rustc_serialize::{Encodable, Encoder};
+use std::str::FromStr;
+
+use datatype::{Error, UpdateRequestId};
+
+
+/// An encodable report of the installation outcome.
+#[derive(RustcDecodable, RustcEncodable, Clone, Debug, PartialEq, Eq)]
+pub struct UpdateReport {
+ pub update_id: UpdateRequestId,
+ pub operation_results: Vec<OperationResult>
+}
+
+impl UpdateReport {
+ /// Instantiate a new report with a vector of installation outcomes.
+ pub fn new(update_id: String, results: Vec<OperationResult>) -> UpdateReport {
+ UpdateReport { update_id: update_id, operation_results: results }
+ }
+
+ /// Instantiate a new report with a single installation outcome.
+ pub fn single(update_id: UpdateRequestId, result_code: UpdateResultCode, result_text: String) -> UpdateReport {
+ let result = OperationResult {
+ id: update_id.clone(),
+ result_code: result_code,
+ result_text: result_text
+ };
+ UpdateReport { update_id: update_id, operation_results: vec![result] }
+ }
+}
+
+impl Default for UpdateReport {
+ fn default() -> Self {
+ UpdateReport { update_id: "".to_string(), operation_results: Vec::new() }
+ }
+}
+
+
+/// Bind the installation outcome report to a specific device.
+#[derive(RustcEncodable, Clone, Debug)]
+pub struct DeviceReport<'d, 'r> {
+ pub device: &'d str,
+ pub update_report: &'r UpdateReport
+}
+
+impl<'d, 'r> DeviceReport<'d, 'r> {
+ /// Instantiate a new installation outcome report for a specific device.
+ pub fn new(device: &'d str, update_report: &'r UpdateReport) -> DeviceReport<'d, 'r> {
+ DeviceReport { device: device, update_report: update_report }
+ }
+}
+
+
+/// Enumerate the possible outcomes when trying to install a package.
+#[allow(non_camel_case_types)]
+#[derive(RustcDecodable, Clone, Debug, PartialEq, Eq)]
+pub enum UpdateResultCode {
+ /// Operation executed successfully
+ OK = 0,
+ /// Operation has already been processed
+ ALREADY_PROCESSED,
+ /// Dependency failure during package install, upgrade, or removal
+ DEPENDENCY_FAILURE,
+ /// Update image integrity has been compromised
+ VALIDATION_FAILED,
+ /// Package installation failed
+ INSTALL_FAILED,
+ /// Package upgrade failed
+ UPGRADE_FAILED,
+ /// Package removal failed
+ REMOVAL_FAILED,
+ /// The module loader could not flash its managed module
+ FLASH_FAILED,
+ /// Partition creation failed
+ CREATE_PARTITION_FAILED,
+ /// Partition deletion failed
+ DELETE_PARTITION_FAILED,
+ /// Partition resize failed
+ RESIZE_PARTITION_FAILED,
+ /// Partition write failed
+ WRITE_PARTITION_FAILED,
+ /// Partition patching failed
+ PATCH_PARTITION_FAILED,
+ /// User declined the update
+ USER_DECLINED,
+ /// Software was blacklisted
+ SOFTWARE_BLACKLISTED,
+ /// Ran out of disk space
+ DISK_FULL,
+ /// Software package not found
+ NOT_FOUND,
+ /// Tried to downgrade to older version
+ OLD_VERSION,
+ /// SWM Internal integrity error
+ INTERNAL_ERROR,
+ /// Other error
+ GENERAL_ERROR,
+}
+
+impl FromStr for UpdateResultCode {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<UpdateResultCode, Error> {
+ match &*s.to_uppercase() {
+ "0" | "OK" => Ok(UpdateResultCode::OK),
+ "1" | "ALREADY_PROCESSED" => Ok(UpdateResultCode::ALREADY_PROCESSED),
+ "2" | "DEPENDENCY_FAILURE" => Ok(UpdateResultCode::DEPENDENCY_FAILURE),
+ "3" | "VALIDATION_FAILED" => Ok(UpdateResultCode::VALIDATION_FAILED),
+ "4" | "INSTALL_FAILED" => Ok(UpdateResultCode::INSTALL_FAILED),
+ "5" | "UPGRADE_FAILED" => Ok(UpdateResultCode::UPGRADE_FAILED),
+ "6" | "REMOVAL_FAILED" => Ok(UpdateResultCode::REMOVAL_FAILED),
+ "7" | "FLASH_FAILED" => Ok(UpdateResultCode::FLASH_FAILED),
+ "8" | "CREATE_PARTITION_FAILED" => Ok(UpdateResultCode::CREATE_PARTITION_FAILED),
+ "9" | "DELETE_PARTITION_FAILED" => Ok(UpdateResultCode::DELETE_PARTITION_FAILED),
+ "10" | "RESIZE_PARTITION_FAILED" => Ok(UpdateResultCode::RESIZE_PARTITION_FAILED),
+ "11" | "WRITE_PARTITION_FAILED" => Ok(UpdateResultCode::WRITE_PARTITION_FAILED),
+ "12" | "PATCH_PARTITION_FAILED" => Ok(UpdateResultCode::PATCH_PARTITION_FAILED),
+ "13" | "USER_DECLINED" => Ok(UpdateResultCode::USER_DECLINED),
+ "14" | "SOFTWARE_BLACKLISTED" => Ok(UpdateResultCode::SOFTWARE_BLACKLISTED),
+ "15" | "DISK_FULL" => Ok(UpdateResultCode::DISK_FULL),
+ "16" | "NOT_FOUND" => Ok(UpdateResultCode::NOT_FOUND),
+ "17" | "OLD_VERSION" => Ok(UpdateResultCode::OLD_VERSION),
+ "18" | "INTERNAL_ERROR" => Ok(UpdateResultCode::INTERNAL_ERROR),
+ "19" | "GENERAL_ERROR" => Ok(UpdateResultCode::GENERAL_ERROR),
+ _ => Err(Error::Parse(format!("unknown UpdateResultCode: {}", s)))
+ }
+ }
+}
+
+impl Encodable for UpdateResultCode {
+ fn encode<S: Encoder>(&self, s: &mut S) -> Result<(), S::Error> {
+ s.emit_u64(self.clone() as u64)
+ }
+}
+
+
+/// An encodable response of the installation outcome for a particular update ID.
+#[derive(RustcDecodable, RustcEncodable, Clone, Debug, PartialEq, Eq)]
+pub struct OperationResult {
+ pub id: String,
+ pub result_code: UpdateResultCode,
+ pub result_text: String,
+}
+
+
+/// Encapsulates a single firmware installed on the device.
+#[derive(RustcDecodable, RustcEncodable, Clone, Debug, PartialEq, Eq)]
+pub struct InstalledFirmware {
+ pub module: String,
+ pub firmware_id: String,
+ pub last_modified: u64
+}
+
+
+/// Encapsulates a single package installed on the device.
+#[derive(RustcDecodable, RustcEncodable, Clone, Debug, PartialEq, Eq)]
+pub struct InstalledPackage {
+ pub package_id: String,
+ pub name: String,
+ pub description: String,
+ pub last_modified: u64
+}
+
+
+/// An encodable list of packages and firmwares to send to RVI.
+#[derive(RustcDecodable, RustcEncodable, Clone, Debug, PartialEq, Eq)]
+pub struct InstalledSoftware {
+ pub packages: Vec<InstalledPackage>,
+ pub firmwares: Vec<InstalledFirmware>
+}
+
+impl InstalledSoftware {
+ /// Instantiate a new list of the software installed on the device.
+ pub fn new(packages: Vec<InstalledPackage>, firmwares: Vec<InstalledFirmware>) -> InstalledSoftware {
+ InstalledSoftware { packages: packages, firmwares: firmwares }
+ }
+}
+
+impl Default for InstalledSoftware {
+ fn default() -> Self {
+ InstalledSoftware { packages: Vec::new(), firmwares: Vec::new() }
+ }
+}
diff --git a/src/datatype/url.rs b/src/datatype/url.rs
new file mode 100644
index 0000000..5a9c97a
--- /dev/null
+++ b/src/datatype/url.rs
@@ -0,0 +1,106 @@
+use hyper::method;
+use rustc_serialize::{Decoder, Decodable};
+use std::borrow::Cow;
+use std::fmt::{Display, Formatter, Result as FmtResult};
+use std::io;
+use std::net::ToSocketAddrs;
+use std::str::FromStr;
+use url;
+use url::SocketAddrs;
+
+use datatype::Error;
+
+
+/// Encapsulate a single crate URL with additional methods and traits.
+#[derive(PartialEq, Eq, Clone, Debug)]
+pub struct Url(pub url::Url);
+
+impl Url {
+ /// Append the string suffix to this URL.
+ pub fn join(&self, suffix: &str) -> Result<Url, Error> {
+ let url = try!(self.0.join(suffix));
+ Ok(Url(url))
+ }
+
+ /// Return the encapsulated crate URL.
+ pub fn inner(&self) -> url::Url {
+ self.0.clone()
+ }
+}
+
+impl<'a> Into<Cow<'a, Url>> for Url {
+ fn into(self) -> Cow<'a, Url> {
+ Cow::Owned(self)
+ }
+}
+
+impl FromStr for Url {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let url = try!(url::Url::parse(s));
+ Ok(Url(url))
+ }
+}
+
+impl Decodable for Url {
+ fn decode<D: Decoder>(d: &mut D) -> Result<Url, D::Error> {
+ let s = try!(d.read_str());
+ s.parse().map_err(|e: Error| d.error(&e.to_string()))
+ }
+}
+
+impl ToSocketAddrs for Url {
+ type Iter = SocketAddrs;
+
+ fn to_socket_addrs(&self) -> io::Result<Self::Iter> {
+ self.0.to_socket_addrs()
+ }
+}
+
+impl Display for Url {
+ fn fmt(&self, f: &mut Formatter) -> FmtResult {
+ let host = self.0.host_str().unwrap_or("localhost");
+ if let Some(port) = self.0.port() {
+ write!(f, "{}://{}:{}{}", self.0.scheme(), host, port, self.0.path())
+ } else {
+ write!(f, "{}://{}{}", self.0.scheme(), host, self.0.path())
+ }
+ }
+}
+
+
+/// Enumerate the supported HTTP methods.
+#[derive(Clone, Debug)]
+pub enum Method {
+ Get,
+ Post,
+ Put,
+}
+
+impl Into<method::Method> for Method {
+ fn into(self) -> method::Method {
+ match self {
+ Method::Get => method::Method::Get,
+ Method::Post => method::Method::Post,
+ Method::Put => method::Method::Put,
+ }
+ }
+}
+
+impl<'a> Into<Cow<'a, Method>> for Method {
+ fn into(self) -> Cow<'a, Method> {
+ Cow::Owned(self)
+ }
+}
+
+impl Display for Method {
+ fn fmt(&self, f: &mut Formatter) -> FmtResult {
+ let method = match *self {
+ Method::Get => "GET".to_string(),
+ Method::Post => "POST".to_string(),
+ Method::Put => "PUT".to_string(),
+ };
+ write!(f, "{}", method)
+ }
+}
diff --git a/src/event/inbound.rs b/src/event/inbound.rs
deleted file mode 100644
index 41a757c..0000000
--- a/src/event/inbound.rs
+++ /dev/null
@@ -1,28 +0,0 @@
-
-#[derive(RustcDecodable, Clone)]
-pub struct UpdateAvailable {
- pub update_id: String,
- pub signature: String,
- pub description: String,
- pub request_confirmation: bool,
- pub size: u64
-}
-
-#[derive(RustcDecodable, Clone)]
-pub struct DownloadComplete {
- pub update_id: String,
- pub update_image: String,
- pub signature: String
-}
-
-#[derive(RustcDecodable, Clone)]
-pub struct GetInstalledSoftware {
- pub include_packages: bool,
- pub include_module_firmware: bool
-}
-
-pub enum InboundEvent {
- UpdateAvailable(UpdateAvailable),
- DownloadComplete(DownloadComplete),
- GetInstalledSoftware(GetInstalledSoftware)
-}
diff --git a/src/event/mod.rs b/src/event/mod.rs
deleted file mode 100644
index b69d876..0000000
--- a/src/event/mod.rs
+++ /dev/null
@@ -1,9 +0,0 @@
-pub mod inbound;
-pub mod outbound;
-
-pub type UpdateId = String;
-
-pub enum Event {
- Inbound(inbound::InboundEvent),
- OutBound(outbound::OutBoundEvent)
-}
diff --git a/src/event/outbound.rs b/src/event/outbound.rs
deleted file mode 100644
index 4fb6fce..0000000
--- a/src/event/outbound.rs
+++ /dev/null
@@ -1,68 +0,0 @@
-use super::UpdateId;
-
-#[derive(RustcDecodable, RustcEncodable, Clone)]
-pub struct InstalledFirmware {
- pub module: String,
- pub firmware_id: String,
- pub last_modified: u64
-}
-
-#[derive(RustcDecodable, RustcEncodable, Clone)]
-pub struct InstalledFirmwares(pub Vec<InstalledFirmware>);
-
-#[derive(RustcDecodable, RustcEncodable, Clone)]
-pub struct InstalledPackage {
- pub package_id: String,
- pub name: String,
- pub description: String,
- pub last_modified: u64
-}
-
-#[derive(RustcDecodable, RustcEncodable, Clone)]
-pub struct InstalledPackages(pub Vec<InstalledPackage>);
-
-#[derive(RustcDecodable, RustcEncodable, Clone)]
-pub struct InstalledSoftware {
- pub packages: Vec<InstalledPackage>,
- pub firmware: Vec<InstalledFirmware>
-}
-
-impl InstalledSoftware {
- pub fn new(p: InstalledPackages, f: InstalledFirmwares) -> InstalledSoftware {
- InstalledSoftware {
- packages: p.0,
- firmware: f.0
- }
- }
-}
-
-#[derive(RustcDecodable, RustcEncodable, Clone)]
-pub struct OperationResult {
- pub id: String,
- pub result_code: u32,
- pub result_text: String
-}
-
-#[derive(RustcDecodable, RustcEncodable, Clone)]
-pub struct OperationResults(pub Vec<OperationResult>);
-
-#[derive(RustcDecodable, RustcEncodable, Clone)]
-pub struct UpdateReport {
- pub update_id: String,
- pub operation_results: Vec<OperationResult>
-}
-
-impl UpdateReport {
- pub fn new(id: String, res: OperationResults) -> UpdateReport {
- UpdateReport {
- update_id: id,
- operation_results: res.0
- }
- }
-}
-
-pub enum OutBoundEvent {
- InitiateDownload(UpdateId),
- AbortDownload(UpdateId),
- UpdateReport(UpdateReport)
-}
diff --git a/src/gateway/console.rs b/src/gateway/console.rs
new file mode 100644
index 0000000..da8ba80
--- /dev/null
+++ b/src/gateway/console.rs
@@ -0,0 +1,46 @@
+use chan;
+use chan::Sender;
+use std::{io, thread};
+use std::io::Write;
+use std::string::ToString;
+use std::sync::{Arc, Mutex};
+
+use datatype::{Command, Error, Event};
+use super::gateway::{Gateway, Interpret};
+
+
+/// The console gateway is used for REPL-style interaction with the client.
+pub struct Console;
+
+impl Gateway for Console {
+ fn initialize(&mut self, itx: Sender<Interpret>) -> Result<(), String> {
+ let (etx, erx) = chan::sync::<Event>(0);
+ let etx = Arc::new(Mutex::new(etx));
+
+ thread::spawn(move || {
+ loop {
+ match get_input() {
+ Ok(cmd) => itx.send(Interpret { command: cmd, response_tx: Some(etx.clone()) }),
+ Err(err) => error!("Console Error: {:?}", err)
+ }
+ }
+ });
+
+ thread::spawn(move || {
+ loop {
+ let e = erx.recv().expect("all console event transmitters are closed");
+ info!("Console Response: {}", e.to_string());
+ }
+ });
+
+ Ok(info!("Console gateway started."))
+ }
+}
+
+fn get_input() -> Result<Command, Error> {
+ let mut input = String::new();
+ let _ = io::stdout().write(b"> ");
+ io::stdout().flush().expect("couldn't flush console stdout buffer");
+ let _ = io::stdin().read_line(&mut input);
+ input.parse()
+}
diff --git a/src/gateway/dbus.rs b/src/gateway/dbus.rs
new file mode 100644
index 0000000..3bd41ea
--- /dev/null
+++ b/src/gateway/dbus.rs
@@ -0,0 +1,170 @@
+use chan::Sender;
+use dbus::{Connection, BusType, ConnectionItem, FromMessageItem,
+ Message, MessageItem, NameFlag};
+use dbus::obj::{Argument, Interface, Method, MethodResult, ObjectPath};
+use std::thread;
+use std::convert::From;
+
+use datatype::{Command, DBusConfig, Event, InstalledFirmware, InstalledPackage,
+ InstalledSoftware, OperationResult, UpdateReport};
+use datatype::dbus;
+use super::{Gateway, Interpret};
+
+
+/// The `DBus` gateway is used with the RVI module for communicating with the
+/// system session bus.
+pub struct DBus {
+ pub dbus_cfg: DBusConfig,
+ pub itx: Sender<Interpret>,
+}
+
+impl Gateway for DBus {
+ fn initialize(&mut self, itx: Sender<Interpret>) -> Result<(), String> {
+ let dbus_cfg = self.dbus_cfg.clone();
+
+ thread::spawn(move || {
+ let conn = Connection::get_private(BusType::Session).expect("couldn't get dbus session");
+ conn.register_name(&dbus_cfg.name, NameFlag::ReplaceExisting as u32).unwrap();
+
+ let mut obj_path = ObjectPath::new(&conn, &dbus_cfg.path, true);
+ obj_path.insert_interface(&dbus_cfg.interface, default_interface(itx));
+ obj_path.set_registered(true).expect("couldn't set registration status");
+
+ loop {
+ for item in conn.iter(1000) {
+ if let ConnectionItem::MethodCall(mut msg) = item {
+ info!("DBus method call: {:?}", msg);
+ obj_path.handle_message(&mut msg).map(|result| {
+ let _ = result.map_err(|_| error!("dbus method call failed: {:?}", msg));
+ });
+ }
+ }
+ }
+ });
+
+ Ok(info!("DBus gateway started."))
+ }
+
+ fn pulse(&self, event: Event) {
+ match event {
+ Event::NewUpdateAvailable(avail) => {
+ let msg = self.new_message("updateAvailable", &[
+ MessageItem::from(avail.update_id),
+ MessageItem::from(avail.signature),
+ MessageItem::from(avail.description),
+ MessageItem::from(avail.request_confirmation)
+ ]);
+ let conn = Connection::get_private(BusType::Session).expect("couldn't get dbus session");
+ let _ = conn.send(msg).map_err(|_| error!("couldn't send updateAvailable message"));
+ }
+
+ Event::DownloadComplete(comp) => {
+ let msg = self.new_message("downloadComplete", &[
+ MessageItem::from(comp.update_image),
+ MessageItem::from(comp.signature)
+ ]);
+ let conn = Connection::get_private(BusType::Session).expect("couldn't get dbus session");
+ let _ = conn.send(msg).map_err(|_| error!("couldn't send downloadComplete message"));
+ }
+
+ Event::InstalledSoftwareNeeded => {
+ let msg = self.new_message("getInstalledPackages", &[
+ MessageItem::from(true), // include packages?
+ MessageItem::from(false) // include firmware?
+ ]);
+ let conn = Connection::get_private(BusType::Session).expect("couldn't get dbus session");
+ let reply = conn.send_with_reply_and_block(msg, self.dbus_cfg.timeout).unwrap();
+
+ let _ = || -> Result<InstalledSoftware, ()> {
+ let mut args = reply.get_items().into_iter();
+
+ let pkg_arg = try!(args.next().ok_or(()));
+ let msgs: &Vec<MessageItem> = try!(FromMessageItem::from(&pkg_arg));
+ let packages = try!(msgs.into_iter()
+ .map(|item| -> Result<InstalledPackage, ()> {
+ FromMessageItem::from(item)
+ }).collect::<Result<Vec<InstalledPackage>, ()>>());
+
+ let firm_arg = try!(args.next().ok_or(()));
+ let msgs: &Vec<MessageItem> = try!(FromMessageItem::from(&firm_arg));
+ let firmwares = try!(msgs.into_iter()
+ .map(|item| -> Result<InstalledFirmware, ()> {
+ FromMessageItem::from(item)
+ }).collect::<Result<Vec<InstalledFirmware>, ()>>());
+
+ Ok(InstalledSoftware::new(packages, firmwares))
+ }().map(|inst| send(&self.itx, Command::SendInstalledSoftware(inst)))
+ .map_err(|_| error!("unable to ReportInstalledSoftware"));
+ }
+
+ _ => ()
+ }
+ }
+}
+
+impl DBus {
+ fn new_message(&self, method: &str, args: &[MessageItem]) -> Message {
+ let mgr = self.dbus_cfg.software_manager.clone();
+ let path = self.dbus_cfg.software_manager_path.clone();
+ let result = Message::new_method_call(&mgr, &path, &mgr, method);
+ let mut msg = result.expect("couldn't create dbus message");
+ msg.append_items(args);
+ msg
+ }
+}
+
+fn default_interface<'i>(itx: Sender<Interpret>) -> Interface<'i> {
+ let initiate_itx = itx.clone();
+ let initiate_download = Method::new(
+ "initiateDownload",
+ vec![Argument::new("update_id", "s")],
+ vec![],
+ Box::new(move |msg| handle_initiate_download(&initiate_itx, msg))
+ );
+
+ let update_itx = itx.clone();
+ let update_report = Method::new(
+ "updateReport",
+ vec![Argument::new("update_id", "s"), Argument::new("operations_results", "aa{sv}")],
+ vec![],
+ Box::new(move |msg| handle_update_report(&update_itx, msg))
+ );
+
+ Interface::new(vec![initiate_download, update_report], vec![], vec![])
+}
+
+fn send(itx: &Sender<Interpret>, cmd: Command) {
+ itx.send(Interpret { command: cmd, response_tx: None });
+}
+
+fn handle_initiate_download(itx: &Sender<Interpret>, msg: &mut Message) -> MethodResult {
+ let sender = try!(msg.sender().map(|s| s.to_string()).ok_or(dbus::missing_arg()));
+ debug!("handle_initiate_download: sender={:?}, msg={:?}", sender, msg);
+
+ let mut args = msg.get_items().into_iter();
+ let arg_id = try!(args.next().ok_or(dbus::missing_arg()));
+ let update_id: &String = try!(FromMessageItem::from(&arg_id).or(Err(dbus::malformed_arg())));
+ send(itx, Command::StartDownload(vec![update_id.clone()]));
+
+ Ok(vec![])
+}
+
+fn handle_update_report(itx: &Sender<Interpret>, msg: &mut Message) -> MethodResult {
+ let sender = try!(msg.sender().map(|s| s.to_string()).ok_or(dbus::missing_arg()));
+ debug!("handle_update_report: sender ={:?}, msg ={:?}", sender, msg);
+ let mut args = msg.get_items().into_iter();
+
+ let id_arg = try!(args.next().ok_or(dbus::missing_arg()));
+ let update_id: &String = try!(FromMessageItem::from(&id_arg).or(Err(dbus::malformed_arg())));
+
+ let results_arg = try!(args.next().ok_or(dbus::missing_arg()));
+ let msgs: &Vec<MessageItem> = try!(FromMessageItem::from(&results_arg).or(Err(dbus::malformed_arg())));
+ let results = try!(msgs.into_iter()
+ .map(|item| -> Result<OperationResult, ()> { FromMessageItem::from(item) })
+ .collect::<Result<Vec<OperationResult>, ()>>()
+ .or(Err(dbus::malformed_arg()))
+ );
+ send(itx, Command::SendUpdateReport(UpdateReport::new(update_id.clone(), results)));
+
+ Ok(vec![])
+}
diff --git a/src/gateway/gateway.rs b/src/gateway/gateway.rs
new file mode 100644
index 0000000..96fef8d
--- /dev/null
+++ b/src/gateway/gateway.rs
@@ -0,0 +1,32 @@
+use chan::{Sender, Receiver};
+use std::process;
+use std::sync::{Arc, Mutex};
+
+use datatype::{Command, Event};
+
+
+/// Encapsulates a `Command` to be sent to the `GlobalInterpreter` for processing,
+/// with an optional channel to receive the outcome `Event`.
+pub struct Interpret {
+ pub command: Command,
+ pub response_tx: Option<Arc<Mutex<Sender<Event>>>>,
+}
+
+/// A `Gateway` may send `Command`s to the `GlobalInterpreter`, as well as listen
+/// to the system-wide `Event` messages.
+pub trait Gateway {
+ fn initialize(&mut self, itx: Sender<Interpret>) -> Result<(), String>;
+
+ fn start(&mut self, itx: Sender<Interpret>, erx: Receiver<Event>) {
+ self.initialize(itx).unwrap_or_else(|err| {
+ error!("couldn't start gateway: {}", err);
+ process::exit(1);
+ });
+
+ loop {
+ self.pulse(erx.recv().expect("all gateway event transmitters are closed"));
+ }
+ }
+
+ fn pulse(&self, _: Event) {} // ignore global events by default
+}
diff --git a/src/gateway/http.rs b/src/gateway/http.rs
new file mode 100644
index 0000000..990a1fc
--- /dev/null
+++ b/src/gateway/http.rs
@@ -0,0 +1,135 @@
+use chan;
+use chan::{Sender, Receiver};
+use hyper::StatusCode;
+use hyper::net::{HttpStream, Transport};
+use hyper::server::{Server as HyperServer, Request as HyperRequest};
+use rustc_serialize::json;
+use std::thread;
+use std::sync::{Arc, Mutex};
+
+use datatype::{Command, Event};
+use gateway::{Gateway, Interpret};
+use http::{Server, ServerHandler};
+
+
+/// The `Http` gateway parses `Command`s from the body of incoming requests.
+pub struct Http {
+ pub server: String,
+}
+
+impl Gateway for Http {
+ fn initialize(&mut self, itx: Sender<Interpret>) -> Result<(), String> {
+ let itx = Arc::new(Mutex::new(itx));
+ let server = match HyperServer::http(&self.server.parse().expect("couldn't parse http address")) {
+ Ok(server) => server,
+ Err(err) => return Err(format!("couldn't start http gateway: {}", err))
+ };
+ thread::spawn(move || {
+ let (_, server) = server.handle(move |_| HttpHandler::new(itx.clone())).unwrap();
+ server.run();
+ });
+
+ Ok(info!("HTTP gateway listening at http://{}", self.server))
+ }
+}
+
+
+struct HttpHandler {
+ itx: Arc<Mutex<Sender<Interpret>>>,
+ response_rx: Option<Receiver<Event>>
+}
+
+impl HttpHandler {
+ fn new(itx: Arc<Mutex<Sender<Interpret>>>) -> ServerHandler<HttpStream> {
+ ServerHandler::new(Box::new(HttpHandler { itx: itx, response_rx: None }))
+ }
+}
+
+impl<T: Transport> Server<T> for HttpHandler {
+ fn headers(&mut self, _: HyperRequest<T>) {}
+
+ fn request(&mut self, body: Vec<u8>) {
+ String::from_utf8(body).map(|body| {
+ json::decode::<Command>(&body).map(|cmd| {
+ info!("Incoming HTTP request command: {}", cmd);
+ let (etx, erx) = chan::async::<Event>();
+ self.response_rx = Some(erx);
+ self.itx.lock().unwrap().send(Interpret {
+ command: cmd,
+ response_tx: Some(Arc::new(Mutex::new(etx))),
+ });
+ }).unwrap_or_else(|err| error!("http request parse json: {}", err))
+ }).unwrap_or_else(|err| error!("http request parse string: {}", err))
+ }
+
+ fn response(&mut self) -> (StatusCode, Option<Vec<u8>>) {
+ self.response_rx.as_ref().map_or((StatusCode::BadRequest, None), |rx| {
+ rx.recv().map_or_else(|| {
+ error!("on_response receiver error");
+ (StatusCode::InternalServerError, None)
+ }, |event| {
+ json::encode(&event).map(|body| {
+ (StatusCode::Ok, Some(body.into_bytes()))
+ }).unwrap_or_else(|err| {
+ error!("on_response encoding json: {:?}", err);
+ (StatusCode::InternalServerError, None)
+ })
+ })
+ })
+ }
+}
+
+
+#[cfg(test)]
+mod tests {
+ use chan;
+ use crossbeam;
+ use rustc_serialize::json;
+ use std::path::Path;
+ use std::thread;
+
+ use super::*;
+ use gateway::{Gateway, Interpret};
+ use datatype::{Command, Event};
+ use http::{AuthClient, Client, set_ca_certificates};
+
+
+ #[test]
+ fn http_connections() {
+ set_ca_certificates(&Path::new("run/sota_certificates"));
+
+ let (etx, erx) = chan::sync::<Event>(0);
+ let (itx, irx) = chan::sync::<Interpret>(0);
+
+ thread::spawn(move || Http { server: "127.0.0.1:8888".to_string() }.start(itx, erx));
+ thread::spawn(move || {
+ let _ = etx; // move into this scope
+ loop {
+ let interpret = irx.recv().expect("itx is closed");
+ match interpret.command {
+ Command::StartDownload(ids) => {
+ let tx = interpret.response_tx.unwrap();
+ tx.lock().unwrap().send(Event::FoundSystemInfo(ids.first().unwrap().to_owned()));
+ }
+ _ => panic!("expected AcceptUpdates"),
+ }
+ }
+ });
+
+ crossbeam::scope(|scope| {
+ for id in 0..10 {
+ scope.spawn(move || {
+ let cmd = Command::StartDownload(vec!(format!("{}", id)));
+ let client = AuthClient::default();
+ let url = "http://127.0.0.1:8888".parse().unwrap();
+ let body = json::encode(&cmd).unwrap();
+ let resp_rx = client.post(url, Some(body.into_bytes()));
+ let resp = resp_rx.recv().unwrap().unwrap();
+ let text = String::from_utf8(resp).unwrap();
+ assert_eq!(json::decode::<Event>(&text).unwrap(),
+ Event::FoundSystemInfo(format!("{}", id)));
+ });
+ }
+ });
+ }
+}
diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs
new file mode 100644
index 0000000..027ba51
--- /dev/null
+++ b/src/gateway/mod.rs
@@ -0,0 +1,13 @@
+pub mod console;
+pub mod dbus;
+pub mod gateway;
+pub mod http;
+pub mod socket;
+pub mod websocket;
+
+pub use self::console::Console;
+pub use self::dbus::DBus;
+pub use self::gateway::{Gateway, Interpret};
+pub use self::http::Http;
+pub use self::socket::Socket;
+pub use self::websocket::Websocket;
diff --git a/src/gateway/socket.rs b/src/gateway/socket.rs
new file mode 100644
index 0000000..f252e7c
--- /dev/null
+++ b/src/gateway/socket.rs
@@ -0,0 +1,161 @@
+use chan;
+use chan::Sender;
+use rustc_serialize::json;
+use std::io::{BufReader, Read, Write};
+use std::net::Shutdown;
+use std::sync::{Arc, Mutex};
+use std::{fs, thread};
+
+use datatype::{Command, Error, Event};
+use super::{Gateway, Interpret};
+use unix_socket::{UnixListener, UnixStream};
+
+
+/// The `Socket` gateway is used for communication via Unix Domain Sockets.
+pub struct Socket {
+ pub commands_path: String,
+ pub events_path: String,
+}
+
+impl Gateway for Socket {
+ fn initialize(&mut self, itx: Sender<Interpret>) -> Result<(), String> {
+ let _ = fs::remove_file(&self.commands_path);
+ let commands = match UnixListener::bind(&self.commands_path) {
+ Ok(sock) => sock,
+ Err(err) => return Err(format!("couldn't open commands socket: {}", err))
+ };
+
+ let itx = Arc::new(Mutex::new(itx));
+ thread::spawn(move || {
+ for conn in commands.incoming() {
+ if let Err(err) = conn {
+ error!("couldn't get commands socket connection: {}", err);
+ continue
+ }
+ let mut stream = conn.unwrap();
+ let itx = itx.clone();
+
+ thread::spawn(move || {
+ let resp = handle_client(&mut stream, itx)
+ .map(|ev| json::encode(&ev).expect("couldn't encode Event").into_bytes())
+ .unwrap_or_else(|err| format!("{}", err).into_bytes());
+
+ stream.write_all(&resp)
+ .unwrap_or_else(|err| error!("couldn't write to commands socket: {}", err));
+ stream.shutdown(Shutdown::Write)
+ .unwrap_or_else(|err| error!("couldn't close commands socket: {}", err));
+ });
+ }
+ });
+
+ Ok(info!("Socket listening for commands at {} and sending events to {}.",
+ self.commands_path, self.events_path))
+ }
+
+ fn pulse(&self, event: Event) {
+ match event {
+ Event::DownloadComplete(dl) => {
+ let _ = UnixStream::connect(&self.events_path).map(|mut stream| {
+ stream.write_all(&json::encode(&dl).expect("couldn't encode Event").into_bytes())
+ .unwrap_or_else(|err| error!("couldn't write to events socket: {}", err));
+ stream.shutdown(Shutdown::Write)
+ .unwrap_or_else(|err| error!("couldn't close events socket: {}", err));
+ }).map_err(|err| error!("couldn't open events socket: {}", err));
+ }
+
+ _ => ()
+ }
+ }
+}
+
+fn handle_client(stream: &mut UnixStream, itx: Arc<Mutex<Sender<Interpret>>>) -> Result<Event, Error> {
+ info!("New domain socket connection");
+ let mut reader = BufReader::new(stream);
+ let mut input = String::new();
+ try!(reader.read_to_string(&mut input));
+ debug!("socket input: {}", input);
+
+ let cmd = try!(input.parse::<Command>());
+ let (etx, erx) = chan::async::<Event>();
+ itx.lock().unwrap().send(Interpret {
+ command: cmd,
+ response_tx: Some(Arc::new(Mutex::new(etx))),
+ });
+ erx.recv().ok_or(Error::Socket("internal receiver error".to_string()))
+}
+
+
+#[cfg(test)]
+mod tests {
+ use chan;
+ use crossbeam;
+ use rustc_serialize::json;
+ use std::{fs, thread};
+ use std::io::{Read, Write};
+ use std::net::Shutdown;
+ use std::time::Duration;
+
+ use datatype::{Command, DownloadComplete, Event};
+ use gateway::{Gateway, Interpret};
+ use super::*;
+ use unix_socket::{UnixListener, UnixStream};
+
+
+ #[test]
+ fn socket_commands_and_events() {
+ let (etx, erx) = chan::sync::<Event>(0);
+ let (itx, irx) = chan::sync::<Interpret>(0);
+
+ thread::spawn(move || Socket {
+ commands_path: "/tmp/sota-commands.socket".to_string(),
+ events_path: "/tmp/sota-events.socket".to_string(),
+ }.start(itx, erx));
+ thread::sleep(Duration::from_millis(100)); // wait until socket gateway is created
+
+ let path = "/tmp/sota-events.socket";
+ let _ = fs::remove_file(&path);
+ let server = UnixListener::bind(&path).expect("couldn't create events socket for testing");
+
+ let send = DownloadComplete {
+ update_id: "1".to_string(),
+ update_image: "/foo/bar".to_string(),
+ signature: "abc".to_string()
+ };
+ etx.send(Event::DownloadComplete(send.clone()));
+
+ let (mut stream, _) = server.accept().expect("couldn't read from events socket");
+ let mut text = String::new();
+ stream.read_to_string(&mut text).unwrap();
+ let receive: DownloadComplete = json::decode(&text).expect("couldn't decode DownloadComplete message");
+ assert_eq!(send, receive);
+
+ thread::spawn(move || {
+ let _ = etx; // move into this scope
+ loop {
+ let interpret = irx.recv().expect("gtx is closed");
+ match interpret.command {
+ Command::StartDownload(ids) => {
+ let tx = interpret.response_tx.unwrap();
+ tx.lock().unwrap().send(Event::FoundSystemInfo(ids.first().unwrap().to_owned()));
+ }
+ _ => panic!("expected AcceptUpdates"),
+ }
+ }
+ });
+
+ crossbeam::scope(|scope| {
+ for id in 0..10 {
+ scope.spawn(move || {
+ let mut stream = UnixStream::connect("/tmp/sota-commands.socket").expect("couldn't connect to socket");
+ let _ = stream.write_all(&format!("dl {}", id).into_bytes()).expect("couldn't write to stream");
+ stream.shutdown(Shutdown::Write).expect("couldn't shut down writing");
+
+ let mut resp = String::new();
+ stream.read_to_string(&mut resp).expect("couldn't read from stream");
+ let ev: Event = json::decode(&resp).expect("couldn't decode json event");
+ assert_eq!(ev, Event::FoundSystemInfo(format!("{}", id)));
+ });
+ }
+ });
+ }
+}
diff --git a/src/gateway/websocket.rs b/src/gateway/websocket.rs
new file mode 100644
index 0000000..eb5e040
--- /dev/null
+++ b/src/gateway/websocket.rs
@@ -0,0 +1,169 @@
+use chan;
+use chan::Sender;
+use rustc_serialize::json;
+use std::thread;
+use std::collections::HashMap;
+use std::sync::{Arc, Mutex};
+use std::time::Duration;
+use ws;
+use ws::{listen, CloseCode, Handler, Handshake, Message, Sender as WsSender};
+use ws::util::Token;
+
+use datatype::{Command, Error, Event};
+use super::gateway::{Gateway, Interpret};
+
+
+/// The `Websocket` gateway allows connected clients to listen to `Event`s that
+/// happen in the SOTA client.
+pub struct Websocket {
+ pub server: String,
+ pub clients: Arc<Mutex<HashMap<Token, WsSender>>>
+}
+
+impl Gateway for Websocket {
+ fn initialize(&mut self, itx: Sender<Interpret>) -> Result<(), String> {
+ let clients = self.clients.clone();
+ let addr = self.server.clone();
+ info!("Opening websocket listener at {}", addr);
+
+ thread::spawn(move || {
+ listen(&addr as &str, |out| {
+ WebsocketHandler {
+ out: out,
+ itx: itx.clone(),
+ clients: clients.clone()
+ }
+ }).expect("couldn't start websocket listener");
+ });
+
+ thread::sleep(Duration::from_secs(1)); // FIXME: ugly hack for blocking listen call
+ Ok(info!("Websocket gateway started."))
+ }
+
+ fn pulse(&self, event: Event) {
+ let json = encode(event);
+ for (_, out) in self.clients.lock().unwrap().iter() {
+ let _ = out.send(Message::Text(json.clone()));
+ }
+ }
+}
+
+
+pub struct WebsocketHandler {
+ out: WsSender,
+ itx: Sender<Interpret>,
+ clients: Arc<Mutex<HashMap<Token, WsSender>>>
+}
+
+impl Handler for WebsocketHandler {
+ fn on_message(&mut self, msg: Message) -> ws::Result<()> {
+ debug!("received websocket message: {:?}", msg);
+ msg.as_text().or_else(|err| {
+ error!("websocket on_message text error: {}", err);
+ Err(err)
+ }).and_then(|msg| match decode(msg) {
+ Ok(cmd) => Ok(self.forward_command(cmd)),
+
+ Err(Error::Websocket(err)) => {
+ error!("websocket on_message error: {}", err);
+ Err(err)
+ }
+
+ Err(_) => unreachable!()
+ })
+ }
+
+ fn on_open(&mut self, _: Handshake) -> ws::Result<()> {
+ let _ = self.clients.lock().unwrap().insert(self.out.token(), self.out.clone());
+ Ok(debug!("new websocket client: {:?}", self.out.token()))
+ }
+
+ fn on_close(&mut self, code: CloseCode, _: &str) {
+ let _ = self.clients.lock().unwrap().remove(&self.out.token());
+ debug!("closing websocket client {:?}: {:?}", self.out.token(), code);
+ }
+
+ fn on_error(&mut self, err: ws::Error) {
+ error!("websocket error: {:?}", err);
+ }
+}
+
+impl WebsocketHandler {
+ fn forward_command(&self, cmd: Command) {
+ let (etx, erx) = chan::sync::<Event>(0);
+ let etx = Arc::new(Mutex::new(etx.clone()));
+ self.itx.send(Interpret { command: cmd, response_tx: Some(etx) });
+
+ let e = erx.recv().expect("websocket response_tx is closed");
+ let _ = self.out.send(Message::Text(encode(e)));
+ }
+}
+
+fn encode(event: Event) -> String {
+ json::encode(&event).expect("Error encoding event into JSON")
+}
+
+fn decode(s: &str) -> Result<Command, Error> {
+ Ok(try!(json::decode::<Command>(s)))
+}
+
+
+#[cfg(test)]
+mod tests {
+ use chan;
+ use crossbeam;
+ use rustc_serialize::json;
+ use std::thread;
+ use std::collections::HashMap;
+ use std::sync::{Arc, Mutex};
+ use ws;
+ use ws::{connect, CloseCode};
+
+ use datatype::{Command, Event};
+ use gateway::{Gateway, Interpret};
+ use super::*;
+
+
+ #[test]
+ fn websocket_connections() {
+ let (etx, erx) = chan::sync::<Event>(0);
+ let (itx, irx) = chan::sync::<Interpret>(0);
+
+ thread::spawn(move || {
+ Websocket {
+ server: "localhost:3012".to_string(),
+ clients: Arc::new(Mutex::new(HashMap::new()))
+ }.start(itx, erx);
+ });
+ thread::spawn(move || {
+ let _ = etx; // move into this scope
+ loop {
+ let interpret = irx.recv().expect("gtx is closed");
+ match interpret.command {
+ Command::StartDownload(ids) => {
+ let tx = interpret.response_tx.unwrap();
+ tx.lock().unwrap().send(Event::FoundSystemInfo(ids.first().unwrap().to_owned()));
+ }
+ _ => panic!("expected AcceptUpdates"),
+ }
+ }
+ });
+
+ crossbeam::scope(|scope| {
+ for id in 0..10 {
+ scope.spawn(move || {
+ connect("ws://localhost:3012", |out| {
+ out.send(format!(r#"{{ "variant": "StartDownload", "fields": [["{}"]] }}"#, id))
+ .expect("couldn't write to websocket");
+
+ move |msg: ws::Message| {
+ let ev: Event = json::decode(&format!("{}", msg)).unwrap();
+ assert_eq!(ev, Event::FoundSystemInfo(format!("{}", id)));
+ out.close(CloseCode::Normal)
+ }
+ }).expect("couldn't connect to websocket");
+ });
+ }
+ });
+ }
+}
diff --git a/src/genivi/dbus.rs b/src/genivi/dbus.rs
deleted file mode 100644
index 7b89590..0000000
--- a/src/genivi/dbus.rs
+++ /dev/null
@@ -1,121 +0,0 @@
-use dbus::{FromMessageItem, MessageItem};
-use toml::{decode, Table, Value};
-
-/// DBus error string to indicate a missing argument.
-static MISSING_ARG: &'static str = "Error.MissingArgument";
-/// DBus error string to indicate a malformed argument.
-static MALFORMED_ARG: &'static str = "Error.MalformedArgument";
-
-/// Format a DBus error message indicating a missing argument.
-pub fn missing_arg() -> (&'static str, String) {
- (MISSING_ARG, "Missing argument".to_string())
-}
-
-/// Format a DBus error message indicating a malformed argument.
-pub fn malformed_arg() -> (&'static str, String) {
- (MALFORMED_ARG, "Malformed argument".to_string())
-}
-
-
-struct DecodableValue(Value);
-
-impl<'a> FromMessageItem<'a> for DecodableValue {
- fn from(m: &'a MessageItem) -> Result<Self, ()> {
- match m {
- &MessageItem::Str(ref b) => Ok(DecodableValue(Value::String(b.clone()))),
- &MessageItem::Bool(ref b) => Ok(DecodableValue(Value::Boolean(*b))),
- &MessageItem::Byte(ref b) => Ok(DecodableValue(Value::Integer(*b as i64))),
- &MessageItem::Int16(ref b) => Ok(DecodableValue(Value::Integer(*b as i64))),
- &MessageItem::Int32(ref b) => Ok(DecodableValue(Value::Integer(*b as i64))),
- &MessageItem::Int64(ref b) => Ok(DecodableValue(Value::Integer(*b as i64))),
- &MessageItem::UInt16(ref b) => Ok(DecodableValue(Value::Integer(*b as i64))),
- &MessageItem::UInt32(ref b) => Ok(DecodableValue(Value::Integer(*b as i64))),
- &MessageItem::UInt64(ref b) => Ok(DecodableValue(Value::Integer(*b as i64))),
- &MessageItem::Variant(ref b) => FromMessageItem::from(&**b),
- _ => Err(())
- }
- }
-}
-
-pub struct DecodableStruct(pub Value);
-
-impl<'a> FromMessageItem<'a> for DecodableStruct {
- fn from(m: &'a MessageItem) -> Result<Self, ()> {
- let arr: &Vec<MessageItem> = try!(FromMessageItem::from(m));
- arr.iter()
- .map(|entry| {
- let v: Result<(&MessageItem, &MessageItem), ()> = FromMessageItem::from(entry);
- v.and_then(|(k, v)| {
- let k: Result<&String,()> = FromMessageItem::from(k);
- k.and_then(|k| {
- let v: Result<DecodableValue,()> = FromMessageItem::from(v);
- v.map(|v| (k.clone(), v.0)) }) }) })
- .collect::<Result<Vec<(_, _)>, ()>>()
- .map(|arr| DecodableStruct(Value::Table(arr.into_iter().collect::<Table>())))
- }
-}
-
-
-use event::outbound::{OperationResult, OperationResults};
-
-impl<'a> FromMessageItem<'a> for OperationResult {
- fn from(m: &'a MessageItem) -> Result<Self, ()> {
- let m: DecodableStruct = try!(FromMessageItem::from(m));
- decode::<OperationResult>(m.0).ok_or(())
- }
-}
-
-impl<'a> FromMessageItem<'a> for OperationResults {
- fn from(m: &'a MessageItem) -> Result<Self, ()> {
- let arr: &Vec<MessageItem> = try!(FromMessageItem::from(m));
- arr.into_iter()
- .map(|i| {
- let i: Result<OperationResult, ()> = FromMessageItem::from(i);
- i })
- .collect::<Result<Vec<_>, ()>>()
- .map(|a| OperationResults(a))
- }
-}
-
-use event::outbound::{InstalledPackage, InstalledPackages};
-
-impl<'a> FromMessageItem<'a> for InstalledPackage {
- fn from(m: &'a MessageItem) -> Result<Self, ()> {
- let m: DecodableStruct = try!(FromMessageItem::from(m));
- decode::<InstalledPackage>(m.0).ok_or(())
- }
-}
-
-impl<'a> FromMessageItem<'a> for InstalledPackages {
- fn from(m: &'a MessageItem) -> Result<Self, ()> {
- let arr: &Vec<MessageItem> = try!(FromMessageItem::from(m));
- arr.into_iter()
- .map(|i| {
- let i: Result<InstalledPackage, ()> = FromMessageItem::from(i);
- i })
- .collect::<Result<Vec<_>, ()>>()
- .map(|a| InstalledPackages(a))
- }
-}
-
-use event::outbound::{InstalledFirmware, InstalledFirmwares};
-
-impl<'a> FromMessageItem<'a> for InstalledFirmware {
- fn from(m: &'a MessageItem) -> Result<Self, ()> {
- let m: DecodableStruct = try!(FromMessageItem::from(m));
- decode::<InstalledFirmware>(m.0).ok_or(())
- }
-}
-
-impl<'a> FromMessageItem<'a> for InstalledFirmwares {
- fn from(m: &'a MessageItem) -> Result<Self, ()> {
- let arr: &Vec<MessageItem> = try!(FromMessageItem::from(m));
- arr.into_iter()
- .map(|i| {
- let i: Result<InstalledFirmware, ()> = FromMessageItem::from(i);
- i })
- .collect::<Result<Vec<_>, ()>>()
- .map(|a| InstalledFirmwares(a))
- }
-}
-
diff --git a/src/genivi/mod.rs b/src/genivi/mod.rs
deleted file mode 100644
index b7ae755..0000000
--- a/src/genivi/mod.rs
+++ /dev/null
@@ -1,4 +0,0 @@
-pub mod dbus;
-pub mod swm;
-pub mod sc;
-pub mod start;
diff --git a/src/genivi/sc.rs b/src/genivi/sc.rs
deleted file mode 100644
index 7c86edc..0000000
--- a/src/genivi/sc.rs
+++ /dev/null
@@ -1,126 +0,0 @@
-//! Receiving side of the DBus interface.
-
-use std::sync::mpsc::Sender;
-
-use dbus::{Connection, NameFlag, BusType, ConnectionItem, Message, FromMessageItem};
-use dbus::obj::*;
-
-use configuration::DBusConfiguration;
-use event::Event;
-use event::outbound::{OutBoundEvent, OperationResults, UpdateReport};
-use genivi::dbus::*;
-
-
-/// Encodes the state that is needed to accept incoming DBus messages.
-pub struct Receiver {
- /// The configuration for the DBus interface.
- config: DBusConfiguration,
- /// A sender to forward incoming messages.
- sender: Sender<Event>
-}
-
-impl Receiver {
- /// Create a new `Receiver`.
- ///
- /// # Arguments
- /// * `c`: The configuration for the DBus interface.
- /// * `s`: A sender to forward incoming messages.
- pub fn new(c: DBusConfiguration, s: Sender<Event>) -> Receiver {
- Receiver {
- config: c,
- sender: s
- }
- }
-
- /// Start the listener. It will register in DBus according to the configuration, wait for
- /// incoming messages and forward them via the internal `Sender`.
- pub fn start(&self) {
- let conn = Connection::get_private(BusType::Session).unwrap();
- conn.register_name(&self.config.name, NameFlag::ReplaceExisting as u32).unwrap();
-
- let initiate_download = Method::new(
- "initiateDownload",
- vec!(Argument::new("update_id", "s")),
- vec!(),
- Box::new(|msg| self.handle_initiate_download(msg)));
- let abort_download = Method::new(
- "abortDownload",
- vec!(Argument::new("update_id", "s")),
- vec!(),
- Box::new(|msg| self.handle_abort_download(msg)));
- let update_report = Method::new(
- "updateReport",
- vec!(Argument::new("update_id", "s"), Argument::new("operations_results", "aa{sv}")),
- vec!(),
- Box::new(|msg| self.handle_update_report(msg)));
- let interface = Interface::new(vec!(initiate_download, abort_download, update_report), vec!(), vec!());
-
- let mut object_path = ObjectPath::new(&conn, &self.config.path, true);
- object_path.insert_interface(&self.config.interface, interface);
- object_path.set_registered(true).unwrap();
-
- for n in conn.iter(1000) {
- match n {
- ConnectionItem::MethodCall(mut m) => {
- object_path.handle_message(&mut m);
- },
- _ => {}
- }
- }
- }
-
- /// Handles incoming "Initiate Download" messages.
- ///
- /// Parses the message and forwards it to the internal `Sender`.
- ///
- /// # Arguments
- /// * `msg`: The message to handle.
- fn handle_initiate_download(&self, msg: &mut Message) -> MethodResult {
- let sender = try!(get_sender(msg).ok_or(missing_arg()));
- trace!("sender: {:?}", sender);
- trace!("msg: {:?}", msg);
-
- let mut args = msg.get_items().into_iter();
- let arg = try!(args.next().ok_or(missing_arg()));
- let update_id: &String = try!(FromMessageItem::from(&arg).or(Err(malformed_arg())));
- let _ = self.sender.send(
- Event::OutBound(OutBoundEvent::InitiateDownload(update_id.clone())));
-
- Ok(vec!())
- }
-
- fn handle_abort_download(&self, msg: &mut Message) -> MethodResult {
- let sender = try!(get_sender(msg).ok_or(missing_arg()));
- trace!("sender: {:?}", sender);
- trace!("msg: {:?}", msg);
-
- let mut args = msg.get_items().into_iter();
- let arg = try!(args.next().ok_or(missing_arg()));
- let update_id: &String = try!(FromMessageItem::from(&arg).or(Err(malformed_arg())));
- let _ = self.sender.send(
- Event::OutBound(OutBoundEvent::AbortDownload(update_id.clone())));
-
- Ok(vec!())
- }
-
- fn handle_update_report(&self, msg: &mut Message) -> MethodResult {
- let sender = try!(get_sender(msg).ok_or(missing_arg()));
- trace!("sender: {:?}", sender);
- trace!("msg: {:?}", msg);
-
- let mut args = msg.get_items().into_iter();
- let arg = try!(args.next().ok_or(missing_arg()));
- let update_id: &String = try!(FromMessageItem::from(&arg).or(Err(malformed_arg())));
-
- let arg = try!(args.next().ok_or(missing_arg()));
- let operation_results: OperationResults = try!(FromMessageItem::from(&arg).or(Err(malformed_arg())));
-
- let report = UpdateReport::new(update_id.clone(), operation_results);
- let _ = self.sender.send(
- Event::OutBound(OutBoundEvent::UpdateReport(report)));
-
- Ok(vec!())
- }
-}
-
-fn get_sender(msg: &Message) -> Option<String> { msg.sender() }
diff --git a/src/genivi/start.rs b/src/genivi/start.rs
deleted file mode 100644
index 55bef27..0000000
--- a/src/genivi/start.rs
+++ /dev/null
@@ -1,72 +0,0 @@
-//! Main loop, starting the worker threads and wiring up communication channels between them.
-
-use std::sync::{Arc, Mutex};
-use std::sync::mpsc::{channel, Receiver};
-use std::thread;
-
-use configuration::Configuration;
-use configuration::DBusConfiguration;
-use event::Event;
-use event::inbound::InboundEvent;
-use event::outbound::OutBoundEvent;
-use remote::svc::{RemoteServices, ServiceHandler};
-use remote::rvi;
-
-pub fn handle(cfg: &DBusConfiguration, rx: Receiver<Event>, remote_svcs: Arc<Mutex<RemoteServices>>) {
- loop {
- match rx.recv().unwrap() {
- Event::Inbound(i) => match i {
- InboundEvent::UpdateAvailable(e) => {
- info!("UpdateAvailable");
- super::swm::send_update_available(&cfg, e);
- },
- InboundEvent::DownloadComplete(e) => {
- info!("DownloadComplete");
- super::swm::send_download_complete(&cfg, e);
- },
- InboundEvent::GetInstalledSoftware(e) => {
- info!("GetInstalledSoftware");
- let _ = super::swm::send_get_installed_software(&cfg, e)
- .and_then(|e| {
- remote_svcs.lock().unwrap().send_installed_software(e)
- .map_err(|e| error!("{}", e)) });
- }
- },
- Event::OutBound(o) => match o {
- OutBoundEvent::InitiateDownload(e) => {
- info!("InitiateDownload");
- let _ = remote_svcs.lock().unwrap().send_start_download(e);
- },
- OutBoundEvent::AbortDownload(_) => info!("AbortDownload"),
- OutBoundEvent::UpdateReport(e) => {
- info!("UpdateReport");
- let _ = remote_svcs.lock().unwrap().send_update_report(e);
- }
- }
- }
- }
-}
-
-/// Main loop, starting the worker threads and wiring up communication channels between them.
-///
-/// # Arguments
-/// * `conf`: A pointer to a `Configuration` object see the [documentation of the configuration
-/// crate](../configuration/index.html).
-/// * `rvi_url`: The URL, where RVI can be found, with the protocol.
-/// * `edge_url`: The `host:port` combination where the client should bind and listen for incoming
-/// RVI calls.
-pub fn start(conf: &Configuration, rvi_url: String, edge_url: String) {
- // Main message channel from RVI and DBUS
- let (tx, rx) = channel();
-
- // RVI edge handler
- let remote_svcs = Arc::new(Mutex::new(RemoteServices::new(rvi_url.clone())));
- let handler = ServiceHandler::new(tx.clone(), remote_svcs.clone(), conf.client.clone());
- let rvi_edge = rvi::ServiceEdge::new(rvi_url.clone(), edge_url, handler);
- rvi_edge.start();
-
- // DBUS handler
- let dbus_receiver = super::sc::Receiver::new(conf.dbus.clone(), tx);
- thread::spawn(move || dbus_receiver.start());
- handle(&conf.dbus, rx, remote_svcs);
-}
diff --git a/src/genivi/swm.rs b/src/genivi/swm.rs
deleted file mode 100644
index e913e1e..0000000
--- a/src/genivi/swm.rs
+++ /dev/null
@@ -1,63 +0,0 @@
-//! Sending side of the DBus interface.
-
-use std::convert::From;
-
-use dbus::{Connection, BusType, MessageItem, Message, FromMessageItem};
-
-use configuration::DBusConfiguration;
-use event::inbound::{UpdateAvailable, DownloadComplete, GetInstalledSoftware};
-use event::outbound::{InstalledFirmwares, InstalledPackages, InstalledSoftware};
-
-pub fn send_update_available(config: &DBusConfiguration, e: UpdateAvailable) {
- let args = [
- MessageItem::from(e.update_id),
- MessageItem::from(e.signature),
- MessageItem::from(e.description),
- MessageItem::from(e.request_confirmation)];
- let mut message = Message::new_method_call(
- &config.software_manager, &config.software_manager_path,
- &config.software_manager, "updateAvailable").unwrap();
- message.append_items(&args);
-
- let conn = Connection::get_private(BusType::Session).unwrap();
- let _ = conn.send(message)
- .map_err(|_| error!("Couldn't forward message to D-Bus"));
-}
-
-pub fn send_download_complete(config: &DBusConfiguration, e: DownloadComplete) {
- let args = [
- MessageItem::from(e.update_image),
- MessageItem::from(e.signature)];
- let mut message = Message::new_method_call(
- &config.software_manager, &config.software_manager_path,
- &config.software_manager, "downloadComplete").unwrap();
- message.append_items(&args);
-
- let conn = Connection::get_private(BusType::Session).unwrap();
- let _ = conn.send(message)
- .map_err(|_| error!("Couldn't forward message to D-Bus"));
-}
-
-pub fn send_get_installed_software(config: &DBusConfiguration, e: GetInstalledSoftware)
- -> Result<InstalledSoftware, ()> {
- let args = [
- MessageItem::from(e.include_packages),
- MessageItem::from(e.include_module_firmware)];
- let mut message = Message::new_method_call(
- &config.software_manager, &config.software_manager_path,
- &config.software_manager, "getInstalledPackages").unwrap();
- message.append_items(&args);
-
- let conn = Connection::get_private(BusType::Session).unwrap();
- let msg = conn.send_with_reply_and_block(message, config.timeout).unwrap();
-
- let mut args = msg.get_items().into_iter();
- let arg = try!(args.next().ok_or(()));
- let installed_packages: InstalledPackages = try!(FromMessageItem::from(&arg));
-
- let arg = try!(args.next().ok_or(()));
- let installed_firmware: InstalledFirmwares = try!(FromMessageItem::from(&arg));
-
- Ok(InstalledSoftware::new(installed_packages, installed_firmware))
-}
-
diff --git a/src/http/auth_client.rs b/src/http/auth_client.rs
new file mode 100644
index 0000000..f4ad38b
--- /dev/null
+++ b/src/http/auth_client.rs
@@ -0,0 +1,278 @@
+use chan::Sender;
+use hyper;
+use hyper::{Encoder, Decoder, Next};
+use hyper::client::{Client as HyperClient, Handler, HttpsConnector,
+ Request as HyperRequest, Response as HyperResponse};
+use hyper::header::{Authorization, Basic, Bearer, ContentLength, ContentType, Location};
+use hyper::mime::{Attr, Mime, TopLevel, SubLevel, Value};
+use hyper::net::{HttpStream, HttpsStream, OpensslStream};
+use hyper::status::StatusCode;
+use std::{io, mem};
+use std::io::{ErrorKind, Write};
+use std::str;
+use std::time::Duration;
+use time;
+
+use datatype::{Auth, Error};
+use http::{Client, get_openssl, Request, Response};
+
+
+/// The `AuthClient` will attach an `Authentication` header to each outgoing
+/// HTTP request.
+#[derive(Clone)]
+pub struct AuthClient {
+ auth: Auth,
+ client: HyperClient<AuthHandler>,
+}
+
+impl Default for AuthClient {
+ fn default() -> Self {
+ Self::from(Auth::None)
+ }
+}
+
+impl AuthClient {
+ /// Instantiates a new client ready to make requests for the given `Auth` type.
+ pub fn from(auth: Auth) -> Self {
+ let client = HyperClient::<AuthHandler>::configure()
+ .keep_alive(true)
+ .max_sockets(1024)
+ .connector(HttpsConnector::new(get_openssl()))
+ .build()
+ .expect("unable to create a new hyper Client");
+
+ AuthClient {
+ auth: auth,
+ client: client,
+ }
+ }
+}
+
+impl Client for AuthClient {
+ fn chan_request(&self, req: Request, resp_tx: Sender<Response>) {
+ info!("{} {}", req.method, req.url);
+ let _ = self.client.request(req.url.inner(), AuthHandler {
+ auth: self.auth.clone(),
+ req: req,
+ timeout: Duration::from_secs(20),
+ started: None,
+ written: 0,
+ response: Vec::new(),
+ resp_tx: resp_tx.clone(),
+ }).map_err(|err| resp_tx.send(Err(Error::from(err))));
+ }
+}
+
+
+/// The async handler for outgoing HTTP requests.
+// FIXME: uncomment when yocto is at 1.8.0: #[derive(Debug)]
+pub struct AuthHandler {
+ auth: Auth,
+ req: Request,
+ timeout: Duration,
+ started: Option<u64>,
+ written: usize,
+ response: Vec<u8>,
+ resp_tx: Sender<Response>,
+}
+
+// FIXME: required for building on 1.7.0 only
+impl ::std::fmt::Debug for AuthHandler {
+ fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
+ write!(f, "unimplemented")
+ }
+}
+
+impl AuthHandler {
+ fn redirect_request(&mut self, resp: HyperResponse) {
+ match resp.headers().get::<Location>() {
+ Some(&Location(ref loc)) => self.req.url.join(loc).map(|url| {
+ debug!("redirecting to {}", url);
+ // drop Authentication Header on redirect
+ let client = AuthClient::default();
+ let resp_rx = client.send_request(Request {
+ url: url,
+ method: self.req.method.clone(),
+ body: mem::replace(&mut self.req.body, None),
+ });
+ match resp_rx.recv().expect("no redirect_request response") {
+ Ok(data) => self.resp_tx.send(Ok(data)),
+ Err(err) => self.resp_tx.send(Err(Error::from(err)))
+ }
+ }).unwrap_or_else(|err| self.resp_tx.send(Err(Error::from(err)))),
+
+ None => self.resp_tx.send(Err(Error::Client("redirect missing Location header".to_string())))
+ }
+ }
+}
+
+/// The `AuthClient` may be used for both HTTP and HTTPS connections.
+pub type Stream = HttpsStream<OpensslStream<HttpStream>>;
+
+impl Handler<Stream> for AuthHandler {
+ fn on_request(&mut self, req: &mut HyperRequest) -> Next {
+ req.set_method(self.req.method.clone().into());
+ self.started = Some(time::precise_time_ns());
+ let mut headers = req.headers_mut();
+
+ // empty Charset to keep RVI happy
+ let mime_json = Mime(TopLevel::Application, SubLevel::Json, vec![]);
+ let mime_form = Mime(TopLevel::Application, SubLevel::WwwFormUrlEncoded,
+ vec![(Attr::Charset, Value::Utf8)]);
+
+ match self.auth {
+ Auth::None => {
+ headers.set(ContentType(mime_json));
+ }
+
+ Auth::Credentials(_, _) if self.req.body.is_some() => {
+ panic!("no request body expected for Auth::Credentials");
+ }
+
+ Auth::Credentials(ref id, ref secret) => {
+ headers.set(Authorization(Basic { username: id.0.clone(),
+ password: Some(secret.0.clone()) }));
+ headers.set(ContentType(mime_form));
+ self.req.body = Some(br#"grant_type=client_credentials"#.to_vec());
+ }
+
+ Auth::Token(ref token) => {
+ headers.set(Authorization(Bearer { token: token.access_token.clone() }));
+ headers.set(ContentType(mime_json));
+ }
+ };
+
+ self.req.body.as_ref().map_or(Next::read().timeout(self.timeout), |body| {
+ headers.set(ContentLength(body.len() as u64));
+ Next::write()
+ })
+ }
+
+ fn on_request_writable(&mut self, encoder: &mut Encoder<Stream>) -> Next {
+ let body = self.req.body.as_ref().expect("on_request_writable expects a body");
+
+ match encoder.write(&body[self.written..]) {
+ Ok(0) => {
+ info!("Request length: {} bytes", body.len());
+ if let Ok(body) = str::from_utf8(body) {
+ debug!("body:\n{}", body);
+ }
+ Next::read().timeout(self.timeout)
+ },
+
+ Ok(n) => {
+ self.written += n;
+ trace!("{} bytes written to request body", n);
+ Next::write()
+ }
+
+ Err(ref err) if err.kind() == ErrorKind::WouldBlock => {
+ trace!("retry on_request_writable");
+ Next::write()
+ }
+
+ Err(err) => {
+ error!("unable to write request body: {}", err);
+ self.resp_tx.send(Err(Error::from(err)));
+ Next::remove()
+ }
+ }
+ }
+
+ fn on_response(&mut self, resp: HyperResponse) -> Next {
+ info!("Response status: {}", resp.status());
+ debug!("on_response headers:\n{}", resp.headers());
+ let started = self.started.expect("expected start time");
+ let latency = time::precise_time_ns() as f64 - started as f64;
+ debug!("on_response latency: {}ms", (latency / 1e6) as u32);
+
+ if resp.status().is_success() {
+ if let Some(len) = resp.headers().get::<ContentLength>() {
+ if **len > 0 {
+ return Next::read();
+ }
+ }
+ self.resp_tx.send(Ok(Vec::new()));
+ Next::end()
+ } else if resp.status().is_redirection() {
+ self.redirect_request(resp);
+ Next::end()
+ } else if resp.status() == &StatusCode::Forbidden {
+ self.resp_tx.send(Err(Error::Authorization(format!("{}", resp.status()))));
+ Next::end()
+ } else {
+ self.resp_tx.send(Err(Error::Client(format!("{}", resp.status()))));
+ Next::end()
+ }
+ }
+
+ fn on_response_readable(&mut self, decoder: &mut Decoder<Stream>) -> Next {
+ match io::copy(decoder, &mut self.response) {
+ Ok(0) => {
+ debug!("on_response_readable bytes read: {}", self.response.len());
+ self.resp_tx.send(Ok(mem::replace(&mut self.response, Vec::new())));
+ Next::end()
+ }
+
+ Ok(n) => {
+ trace!("{} more response bytes read", n);
+ Next::read()
+ }
+
+ Err(ref err) if err.kind() == ErrorKind::WouldBlock => {
+ trace!("retry on_response_readable");
+ Next::read()
+ }
+
+ Err(err) => {
+ error!("unable to read response body: {}", err);
+ self.resp_tx.send(Err(Error::from(err)));
+ Next::end()
+ }
+ }
+ }
+
+ fn on_error(&mut self, err: hyper::Error) -> Next {
+ error!("on_error: {}", err);
+ self.resp_tx.send(Err(Error::from(err)));
+ Next::remove()
+ }
+}
+
+
+#[cfg(test)]
+mod tests {
+ use rustc_serialize::json::Json;
+ use std::path::Path;
+
+ use super::*;
+ use http::{Client, set_ca_certificates};
+
+
+ fn get_client() -> AuthClient {
+ set_ca_certificates(&Path::new("run/sota_certificates"));
+ AuthClient::default()
+ }
+
+ #[test]
+ fn test_send_get_request() {
+ let client = get_client();
+ let url = "http://eu.httpbin.org/bytes/16?seed=123".parse().unwrap();
+ let resp_rx = client.get(url, None);
+ let data = resp_rx.recv().unwrap().unwrap();
+ assert_eq!(data, vec![13, 22, 104, 27, 230, 9, 137, 85, 218, 40, 86, 85, 62, 0, 111, 22]);
+ }
+
+ #[test]
+ fn test_send_post_request() {
+ let client = get_client();
+ let url = "https://eu.httpbin.org/post".parse().unwrap();
+ let resp_rx = client.post(url, Some(br#"foo"#.to_vec()));
+ let body = resp_rx.recv().unwrap().unwrap();
+ let resp = String::from_utf8(body).unwrap();
+ let json = Json::from_str(&resp).unwrap();
+ let obj = json.as_object().unwrap();
+ let data = obj.get("data").unwrap().as_string().unwrap();
+ assert_eq!(data, "foo");
+ }
+}
diff --git a/src/http/http_client.rs b/src/http/http_client.rs
new file mode 100644
index 0000000..492166c
--- /dev/null
+++ b/src/http/http_client.rs
@@ -0,0 +1,43 @@
+use chan;
+use chan::{Sender, Receiver};
+
+use datatype::{Error, Method, Url};
+
+
+/// Abstracts a particular HTTP Client implementation with the basic methods
+/// for sending `Request`s and receiving asynchronous `Response`s via a channel.
+pub trait Client {
+ fn chan_request(&self, req: Request, resp_tx: Sender<Response>);
+
+ fn send_request(&self, req: Request) -> Receiver<Response> {
+ let (resp_tx, resp_rx) = chan::async::<Response>();
+ self.chan_request(req, resp_tx);
+ resp_rx
+ }
+
+ fn get(&self, url: Url, body: Option<Vec<u8>>) -> Receiver<Response> {
+ self.send_request(Request { method: Method::Get, url: url, body: body })
+ }
+
+ fn post(&self, url: Url, body: Option<Vec<u8>>) -> Receiver<Response> {
+ self.send_request(Request { method: Method::Post, url: url, body: body })
+ }
+
+ fn put(&self, url: Url, body: Option<Vec<u8>>) -> Receiver<Response> {
+ self.send_request(Request { method: Method::Put, url: url, body: body })
+ }
+
+ fn is_testing(&self) -> bool { false }
+}
+
+
+/// A simplified representation of an HTTP request for use in the client.
+#[derive(Debug)]
+pub struct Request {
+ pub method: Method,
+ pub url: Url,
+ pub body: Option<Vec<u8>>
+}
+
+/// Return the body of an HTTP response on success, or an `Error` otherwise.
+pub type Response = Result<Vec<u8>, Error>;
diff --git a/src/http/http_server.rs b/src/http/http_server.rs
new file mode 100644
index 0000000..2ecd7a2
--- /dev/null
+++ b/src/http/http_server.rs
@@ -0,0 +1,113 @@
+use hyper::{Decoder, Encoder, Next, StatusCode};
+use hyper::header::{ContentLength, ContentType};
+use hyper::mime::{Attr, Mime, TopLevel, SubLevel, Value};
+use hyper::net::Transport;
+use hyper::server::{Handler, Request as HyperRequest, Response as HyperResponse};
+use std::{mem, io};
+use std::io::{ErrorKind, Write};
+use std::time::Duration;
+
+
+/// An HTTP server handles the incoming headers and request body as well as the
+/// setting the response status and body. Other concerns regarding the asynchronous
+/// event loop handlers for writing to buffers are abstracted away.
+pub trait Server<T: Transport>: Send {
+ fn headers(&mut self, req: HyperRequest<T>);
+ fn request(&mut self, body: Vec<u8>);
+ fn response(&mut self) -> (StatusCode, Option<Vec<u8>>);
+}
+
+
+/// This implements the `hyper::server::Handler` trait so that it can be used
+/// to handle incoming HTTP connections with `hyper::server::Server`.
+pub struct ServerHandler<T: Transport> {
+ server: Box<Server<T>>,
+ req_body: Vec<u8>,
+ resp_body: Vec<u8>,
+ written: usize
+}
+
+impl<T: Transport> ServerHandler<T> {
+ /// Instantiate a new `ServerHandler` by passing a `Box<Server<T>` reference.
+ pub fn new(server: Box<Server<T>>) -> Self {
+ ServerHandler {
+ server: server,
+ req_body: Vec::new(),
+ resp_body: Vec::new(),
+ written: 0
+ }
+ }
+}
+
+impl<T: Transport> Handler<T> for ServerHandler<T> {
+ fn on_request(&mut self, req: HyperRequest<T>) -> Next {
+ info!("on_request: {} {}", req.method(), req.uri());
+ self.server.headers(req);
+ Next::read()
+ }
+
+ fn on_request_readable(&mut self, transport: &mut Decoder<T>) -> Next {
+ match io::copy(transport, &mut self.req_body) {
+ Ok(0) => {
+ debug!("on_request_readable bytes read: {}", self.req_body.len());
+ self.server.request(mem::replace(&mut self.req_body, Vec::new()));
+ Next::write().timeout(Duration::from_secs(20))
+ }
+
+ Ok(n) => {
+ trace!("{} more request bytes read", n);
+ Next::read()
+ }
+
+ Err(ref err) if err.kind() == ErrorKind::WouldBlock => {
+ trace!("retry on_request_readable");
+ Next::read()
+ }
+
+ Err(err) => {
+ error!("unable to read request body: {}", err);
+ Next::remove()
+ }
+ }
+ }
+
+ fn on_response(&mut self, resp: &mut HyperResponse) -> Next {
+ let (status, body) = self.server.response();
+ resp.set_status(status);
+ info!("on_response: status {}", resp.status());
+
+ let mut headers = resp.headers_mut();
+ headers.set(ContentType(Mime(TopLevel::Application, SubLevel::Json,
+ vec![(Attr::Charset, Value::Utf8)])));
+ body.map_or_else(Next::end, |body| {
+ headers.set(ContentLength(body.len() as u64));
+ self.resp_body = body;
+ Next::write()
+ })
+ }
+
+ fn on_response_writable(&mut self, transport: &mut Encoder<T>) -> Next {
+ match transport.write(&self.resp_body[self.written..]) {
+ Ok(0) => {
+ debug!("{} bytes written to response body", self.written);
+ Next::end()
+ }
+
+ Ok(n) => {
+ self.written += n;
+ trace!("{} bytes written to response body", n);
+ Next::write()
+ }
+
+ Err(ref err) if err.kind() == ErrorKind::WouldBlock => {
+ trace!("retry on_response_writable");
+ Next::write()
+ }
+
+ Err(err) => {
+ error!("unable to write response body: {}", err);
+ Next::remove()
+ }
+ }
+ }
+}
diff --git a/src/http/mod.rs b/src/http/mod.rs
new file mode 100644
index 0000000..5e990a3
--- /dev/null
+++ b/src/http/mod.rs
@@ -0,0 +1,11 @@
+pub mod auth_client;
+pub mod http_client;
+pub mod http_server;
+pub mod openssl;
+pub mod test_client;
+
+pub use self::auth_client::{AuthClient, AuthHandler};
+pub use self::http_client::{Client, Request, Response};
+pub use self::http_server::{Server, ServerHandler};
+pub use self::openssl::{get_openssl, set_ca_certificates};
+pub use self::test_client::TestClient;
diff --git a/src/http/openssl.rs b/src/http/openssl.rs
new file mode 100644
index 0000000..378b078
--- /dev/null
+++ b/src/http/openssl.rs
@@ -0,0 +1,47 @@
+use hyper::net::Openssl;
+use openssl::ssl::{SSL_OP_NO_SSLV2, SSL_OP_NO_SSLV3};
+use openssl::ssl::{SslContext, SslMethod};
+use std::path::Path;
+use std::sync::{Arc, Mutex};
+
+
+lazy_static! {
+ static ref OPENSSL: Arc<Mutex<Option<Openssl>>> = Arc::new(Mutex::new(None));
+}
+
+// default cipher list taken from the Servo project:
+// https://github.com/servo/servo/blob/master/components/net/connector.rs#L18
+const DEFAULT_CIPHERS: &'static str = concat!(
+ "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:",
+ "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:",
+ "DHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:",
+ "ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:",
+ "ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA:",
+ "ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:",
+ "DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:",
+ "ECDHE-ECDSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:",
+ "AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA"
+);
+
+/// This function *must* be called before any call is made to `get_openssl()`
+pub fn set_ca_certificates(path: &Path) {
+ info!("Setting OpenSSL CA certificates path to {:?}", path);
+ let mut openssl = OPENSSL.lock().unwrap();
+ let mut context = SslContext::new(SslMethod::Sslv23).unwrap();
+ context.set_CA_file(path).unwrap_or_else(|err| {
+ panic!("couldn't set CA certificates: {}", err);
+ });
+ context.set_cipher_list(DEFAULT_CIPHERS).unwrap();
+ context.set_options(SSL_OP_NO_SSLV2 | SSL_OP_NO_SSLV3);
+ *openssl = Some(Openssl { context: context });
+}
+
+/// This function will return a clone of `Openssl` where the CA certificates
+/// have been bound with `set_ca_certificates()`.
+pub fn get_openssl() -> Openssl {
+ if let Some(ref openssl) = *OPENSSL.lock().unwrap() {
+ openssl.clone()
+ } else {
+ panic!("CA certificates not set")
+ }
+}
diff --git a/src/http/test_client.rs b/src/http/test_client.rs
new file mode 100644
index 0000000..7857e0f
--- /dev/null
+++ b/src/http/test_client.rs
@@ -0,0 +1,35 @@
+use chan::Sender;
+use std::cell::RefCell;
+
+use datatype::Error;
+use http::{Client, Request, Response};
+
+
+/// The `TestClient` will return HTTP responses from an existing list of strings.
+pub struct TestClient {
+ responses: RefCell<Vec<String>>
+}
+
+impl Default for TestClient {
+ fn default() -> Self {
+ TestClient { responses: RefCell::new(Vec::new()) }
+ }
+}
+
+impl TestClient {
+ /// Create a new `TestClient` that will return these responses.
+ pub fn from(responses: Vec<String>) -> TestClient {
+ TestClient { responses: RefCell::new(responses) }
+ }
+}
+
+impl Client for TestClient {
+ fn chan_request(&self, req: Request, resp_tx: Sender<Response>) {
+ match self.responses.borrow_mut().pop() {
+ Some(body) => resp_tx.send(Ok(body.as_bytes().to_vec())),
+ None => resp_tx.send(Err(Error::Client(req.url.to_string())))
+ }
+ }
+
+ fn is_testing(&self) -> bool { true }
+}
diff --git a/src/interpreter.rs b/src/interpreter.rs
new file mode 100644
index 0000000..b286ba5
--- /dev/null
+++ b/src/interpreter.rs
@@ -0,0 +1,364 @@
+use chan;
+use chan::{Sender, Receiver};
+use std;
+use std::borrow::Cow;
+
+use datatype::{AccessToken, Auth, ClientId, ClientSecret, Command, Config,
+ Error, Event, Package, UpdateRequestId};
+use gateway::Interpret;
+use http::{AuthClient, Client};
+use oauth2::authenticate;
+use package_manager::PackageManager;
+use rvi::Services;
+use sota::Sota;
+
+
+/// An `Interpreter` loops over any incoming values, on receipt of which it
+/// delegates to the `interpret` function which will respond with output values.
+pub trait Interpreter<I, O> {
+ fn interpret(&mut self, input: I, otx: &Sender<O>);
+
+ fn run(&mut self, irx: Receiver<I>, otx: Sender<O>) {
+ loop {
+ self.interpret(irx.recv().expect("interpreter sender closed"), &otx);
+ }
+ }
+}
+
+
+/// The `EventInterpreter` listens for `Event`s and optionally responds with
+/// `Command`s that may be sent to the `CommandInterpreter`.
+pub struct EventInterpreter {
+ pub package_manager: PackageManager
+}
+
+impl Interpreter<Event, Command> for EventInterpreter {
+ fn interpret(&mut self, event: Event, ctx: &Sender<Command>) {
+ info!("Event received: {}", event);
+ match event {
+ Event::NotAuthenticated => {
+ info!("Trying to authenticate again...");
+ ctx.send(Command::Authenticate(None));
+ }
+
+ Event::NewUpdatesReceived(ids) => {
+ ctx.send(Command::StartDownload(ids));
+ }
+
+ Event::DownloadComplete(dl) => {
+ if self.package_manager != PackageManager::Off {
+ ctx.send(Command::StartInstall(dl));
+ }
+ }
+
+ Event::InstallComplete(report) => {
+ ctx.send(Command::SendUpdateReport(report));
+ }
+
+ Event::UpdateReportSent => {
+ if self.package_manager != PackageManager::Off {
+ self.package_manager.installed_packages().map(|packages| {
+ ctx.send(Command::SendInstalledPackages(packages));
+ }).unwrap_or_else(|err| error!("couldn't send a list of packages: {}", err));
+ }
+ }
+
+ _ => ()
+ }
+ }
+}
+
+
+/// The `CommandInterpreter` wraps each incoming `Command` inside an `Interpret`
+/// type with no response channel for sending to the `GlobalInterpreter`.
+pub struct CommandInterpreter;
+
+impl Interpreter<Command, Interpret> for CommandInterpreter {
+ fn interpret(&mut self, cmd: Command, itx: &Sender<Interpret>) {
+ info!("Command received: {}", cmd);
+ itx.send(Interpret { command: cmd, response_tx: None });
+ }
+}
+
+
+/// The `GlobalInterpreter` interprets the `Command` inside incoming `Interpret`
+/// messages, broadcasting `Event`s globally and (optionally) sending the final
+/// outcome `Event` to the `Interpret` response channel.
+pub struct GlobalInterpreter<'t> {
+ pub config: Config,
+ pub token: Option<Cow<'t, AccessToken>>,
+ pub http_client: Box<Client>,
+ pub rvi: Option<Services>,
+ pub loopback_tx: Sender<Interpret>,
+}
+
+impl<'t> Interpreter<Interpret, Event> for GlobalInterpreter<'t> {
+ fn interpret(&mut self, interpret: Interpret, etx: &Sender<Event>) {
+ info!("Interpreter started: {}", interpret.command);
+
+ let (multi_tx, multi_rx) = chan::async::<Event>();
+ let outcome = match (self.token.as_ref(), self.config.auth.is_none()) {
+ (Some(_), _) | (_, true) => self.authenticated(interpret.command, multi_tx),
+ _ => self.unauthenticated(interpret.command, multi_tx)
+ };
+
+ let mut response_ev: Option<Event> = None;
+ match outcome {
+ Ok(_) => {
+ for ev in multi_rx {
+ etx.send(ev.clone());
+ response_ev = Some(ev);
+ }
+ info!("Interpreter finished.");
+ }
+
+ Err(Error::Authorization(_)) => {
+ let ev = Event::NotAuthenticated;
+ etx.send(ev.clone());
+ response_ev = Some(ev);
+ error!("Interpreter authentication failed");
+ }
+
+ Err(err) => {
+ let ev = Event::Error(format!("{}", err));
+ etx.send(ev.clone());
+ response_ev = Some(ev);
+ error!("Interpreter failed: {}", err);
+ }
+ }
+
+ let ev = response_ev.expect("no response event to send back");
+ interpret.response_tx.map(|tx| tx.lock().unwrap().send(ev));
+ }
+}
+
+impl<'t> GlobalInterpreter<'t> {
+ fn authenticated(&self, cmd: Command, etx: Sender<Event>) -> Result<(), Error> {
+ let mut sota = Sota::new(&self.config, self.http_client.as_ref());
+
+ // always send at least one Event response
+ match cmd {
+ Command::Authenticate(_) => etx.send(Event::Authenticated),
+
+ Command::GetNewUpdates => {
+ let mut updates = try!(sota.get_pending_updates());
+ if updates.is_empty() {
+ etx.send(Event::NoNewUpdates);
+ } else {
+ updates.sort_by_key(|u| u.installPos);
+ let ids = updates.iter().map(|u| u.requestId.clone()).collect::<Vec<UpdateRequestId>>();
+ etx.send(Event::NewUpdatesReceived(ids))
+ }
+ }
+
+ Command::ListInstalledPackages => {
+ let mut packages: Vec<Package> = Vec::new();
+ if self.config.device.package_manager != PackageManager::Off {
+ packages = try!(self.config.device.package_manager.installed_packages());
+ }
+ etx.send(Event::FoundInstalledPackages(packages));
+ }
+
+ Command::RefreshSystemInfo(post) => {
+ let info = try!(self.config.device.system_info.report());
+ etx.send(Event::FoundSystemInfo(info.clone()));
+ if post {
+ let _ = sota.send_system_info(&info)
+ .map_err(|err| etx.send(Event::Error(format!("{}", err))));
+ }
+ }
+
+ Command::SendInstalledPackages(packages) => {
+ let _ = sota.send_installed_packages(&packages)
+ .map_err(|err| error!("couldn't send installed packages: {}", err));
+ etx.send(Event::InstalledPackagesSent);
+ }
+
+ Command::SendInstalledSoftware(sw) => {
+ if let Some(ref rvi) = self.rvi {
+ let _ = rvi.remote.lock().unwrap().send_installed_software(sw);
+ }
+ }
+
+ Command::SendUpdateReport(report) => {
+ if let Some(ref rvi) = self.rvi {
+ let _ = rvi.remote.lock().unwrap().send_update_report(report);
+ } else {
+ let _ = sota.send_update_report(&report)
+ .map_err(|err| error!("couldn't send update report: {}", err));
+ }
+ etx.send(Event::UpdateReportSent);
+ }
+
+ Command::StartDownload(ref ids) => {
+ for id in ids {
+ etx.send(Event::DownloadingUpdate(id.clone()));
+
+ if let Some(ref rvi) = self.rvi {
+ let _ = rvi.remote.lock().unwrap().send_download_started(id.clone());
+ } else {
+ let _ = sota.download_update(id.clone())
+ .map(|dl| etx.send(Event::DownloadComplete(dl)))
+ .map_err(|err| etx.send(Event::DownloadFailed(id.clone(), format!("{}", err))));
+ }
+ }
+ }
+
+ Command::StartInstall(dl) => {
+ let _ = sota.install_update(dl)
+ .map(|report| etx.send(Event::InstallComplete(report)))
+ .map_err(|report| etx.send(Event::InstallFailed(report)));
+ }
+
+ Command::Shutdown => std::process::exit(0),
+ }
+
+ Ok(())
+ }
+
+ fn unauthenticated(&mut self, cmd: Command, etx: Sender<Event>) -> Result<(), Error> {
+ match cmd {
+ Command::Authenticate(_) => {
+ let config = self.config.auth.clone().expect("trying to authenticate without auth config");
+ self.set_client(Auth::Credentials(ClientId(config.client_id),
+ ClientSecret(config.client_secret)));
+ let server = config.server.join("/token").expect("couldn't build authentication url");
+ let token = try!(authenticate(server, self.http_client.as_ref()));
+ self.set_client(Auth::Token(token.clone()));
+ self.token = Some(token.into());
+ etx.send(Event::Authenticated);
+ }
+
+ Command::GetNewUpdates |
+ Command::ListInstalledPackages |
+ Command::RefreshSystemInfo(_) |
+ Command::SendInstalledPackages(_) |
+ Command::SendInstalledSoftware(_) |
+ Command::SendUpdateReport(_) |
+ Command::StartDownload(_) |
+ Command::StartInstall(_) => etx.send(Event::NotAuthenticated),
+
+ Command::Shutdown => std::process::exit(0),
+ }
+
+ Ok(())
+ }
+
+ fn set_client(&mut self, auth: Auth) {
+ if !self.http_client.is_testing() {
+ self.http_client = Box::new(AuthClient::from(auth));
+ }
+ }
+}
+
+
+#[cfg(test)]
+mod tests {
+ use chan;
+ use chan::{Sender, Receiver};
+ use std::thread;
+
+ use super::*;
+ use datatype::{AccessToken, Command, Config, DownloadComplete, Event,
+ UpdateReport, UpdateResultCode};
+ use gateway::Interpret;
+ use http::test_client::TestClient;
+ use package_manager::PackageManager;
+ use package_manager::tpm::assert_rx;
+
+
+ fn new_interpreter(replies: Vec<String>, pkg_mgr: PackageManager) -> (Sender<Command>, Receiver<Event>) {
+ let (etx, erx) = chan::sync::<Event>(0);
+ let (ctx, crx) = chan::sync::<Command>(0);
+ let (itx, _) = chan::sync::<Interpret>(0);
+
+ thread::spawn(move || {
+ let mut gi = GlobalInterpreter {
+ config: Config::default(),
+ token: Some(AccessToken::default().into()),
+ http_client: Box::new(TestClient::from(replies)),
+ rvi: None,
+ loopback_tx: itx,
+ };
+ gi.config.device.package_manager = pkg_mgr;
+
+ loop {
+ match crx.recv() {
+ Some(cmd) => gi.interpret(Interpret { command: cmd, response_tx: None }, &etx),
+ None => break
+ }
+ }
+ });
+
+ (ctx, erx)
+ }
+
+ #[test]
+ fn already_authenticated() {
+ let replies = Vec::new();
+ let pkg_mgr = PackageManager::new_tpm(true);
+ let (ctx, erx) = new_interpreter(replies, pkg_mgr);
+
+ ctx.send(Command::Authenticate(None));
+ assert_rx(erx, &[Event::Authenticated]);
+ }
+
+ #[test]
+ fn download_updates() {
+ let replies = vec!["[]".to_string(); 10];
+ let pkg_mgr = PackageManager::new_tpm(true);
+ let (ctx, erx) = new_interpreter(replies, pkg_mgr);
+
+ ctx.send(Command::StartDownload(vec!["1".to_string(), "2".to_string()]));
+ assert_rx(erx, &[
+ Event::DownloadingUpdate("1".to_string()),
+ Event::DownloadComplete(DownloadComplete {
+ update_id: "1".to_string(),
+ update_image: "/tmp/1".to_string(),
+ signature: "".to_string()
+ }),
+ Event::DownloadingUpdate("2".to_string()),
+ Event::DownloadComplete(DownloadComplete {
+ update_id: "2".to_string(),
+ update_image: "/tmp/2".to_string(),
+ signature: "".to_string()
+ }),
+ ]);
+ }
+
+ #[test]
+ fn install_update() {
+ let replies = vec!["[]".to_string(); 10];
+ let pkg_mgr = PackageManager::new_tpm(true);
+ let (ctx, erx) = new_interpreter(replies, pkg_mgr);
+
+ ctx.send(Command::StartInstall(DownloadComplete {
+ update_id: "1".to_string(),
+ update_image: "/tmp/1".to_string(),
+ signature: "".to_string()
+ }));
+ assert_rx(erx, &[
+ Event::InstallComplete(
+ UpdateReport::single("1".to_string(), UpdateResultCode::OK, "".to_string())
+ )
+ ]);
+ }
+
+ #[test]
+ fn failed_installation() {
+ let replies = vec!["[]".to_string(); 10];
+ let pkg_mgr = PackageManager::new_tpm(false);
+ let (ctx, erx) = new_interpreter(replies, pkg_mgr);
+
+ ctx.send(Command::StartInstall(DownloadComplete {
+ update_id: "1".to_string(),
+ update_image: "/tmp/1".to_string(),
+ signature: "".to_string()
+ }));
+ assert_rx(erx, &[
+ Event::InstallFailed(
+ UpdateReport::single("1".to_string(), UpdateResultCode::INSTALL_FAILED, "failed".to_string())
+ )
+ ]);
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 62b445c..ec6d3af 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,65 +1,27 @@
-//! This is the client in-vehicle portion of the SOTA project. See the [main SOTA Server
-//! project](https://github.com/advancedtelematic/rvi_sota_server) and [associated architecture
-//! document](http://advancedtelematic.github.io/rvi_sota_server/dev/architecture.html) for more
-//! information.
+#[macro_use] extern crate nom; // use before log to avoid error!() macro conflict
+#[macro_use] extern crate chan;
+extern crate crossbeam;
+extern crate crypto;
+extern crate dbus;
extern crate hyper;
+extern crate openssl;
+#[macro_use] extern crate lazy_static;
+#[macro_use] extern crate log;
+extern crate rand;
extern crate rustc_serialize;
extern crate time;
-extern crate url;
-extern crate crypto;
extern crate toml;
-extern crate dbus;
-
-#[macro_use] extern crate log;
-extern crate env_logger;
-
-#[cfg(test)] extern crate rand;
-
-#[cfg(test)]
-#[macro_use]
-mod test_library;
-
-/// Try to unwrap or log the error and run the second argument
-///
-/// # Arguments
-/// 1. Expression to evaluate, needs to return a `Result<T, E> where E: Display` type.
-/// 2. Expression to run on errors, after logging the error as error message.
-#[macro_export]
-macro_rules! try_or {
- ($expr:expr, $finalize:expr) => {
- match $expr {
- Ok(val) => val,
- Err(e) => {
- error!("{}", e);
- $finalize;
- }
- }
- }
-}
-
-/// Try to unwrap or log the provided message and run the third argument
-///
-/// # Arguments
-/// 1. Expression to evaluate, needs to return a `Result<T, E>` type.
-/// 2. Expression that returns a Object implementing the `Display` trait. This object will be
-/// logged as a error message with `error!()`
-/// 3. Expression to run on errors, after printing a error message.
-#[macro_export]
-macro_rules! try_msg_or {
- ($expr:expr, $msg:expr, $finalize:expr) => {
- match $expr {
- Ok(val) => val,
- Err(..) => {
- error!("{}", $msg);
- $finalize;
- }
- }
- }
-}
-
-mod event;
-mod remote;
-
-pub mod configuration;
-pub mod genivi;
+extern crate unix_socket;
+extern crate url;
+extern crate ws;
+
+pub mod broadcast;
+pub mod datatype;
+pub mod gateway;
+pub mod http;
+pub mod interpreter;
+pub mod oauth2;
+pub mod package_manager;
+pub mod rvi;
+pub mod sota;
diff --git a/src/main.rs b/src/main.rs
index c55a339..61fa02a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,85 +1,308 @@
-//! Logic for starting and configuring the sota_client from the command line.
-
-extern crate sota_client;
-#[macro_use] extern crate log;
+#[macro_use] extern crate chan;
+extern crate chan_signal;
+extern crate crossbeam;
extern crate env_logger;
extern crate getopts;
+extern crate hyper;
+#[macro_use] extern crate log;
+extern crate rustc_serialize;
+#[macro_use] extern crate sota;
+extern crate time;
+use chan::{Sender, Receiver};
+use chan_signal::Signal;
+use env_logger::LogBuilder;
+use getopts::Options;
+use log::{LogLevelFilter, LogRecord};
use std::env;
-use getopts::{Options, Matches};
-use sota_client::configuration::Configuration;
-use sota_client::genivi;
-
-/// Helper function to print usage information to stdout.
-///
-/// # Arguments
-/// * `program`: The invoking path or name of the executable
-/// * `opts`: A pointer to a `Options` object, which generates the actual documentation. See the
-/// [getopts documentation](https://doc.rust-lang.org/getopts/getopts/index.html) for details.
-#[cfg_attr(test, allow(dead_code))]
-fn print_usage(program: &str, opts: &Options) {
- let brief = format!("Usage: {} [options]", program);
- print!("{}", opts.usage(&brief));
+use std::collections::HashMap;
+use std::path::Path;
+use std::sync::{Arc, Mutex};
+use std::time::Duration;
+
+use sota::datatype::{Command, Config, Event, SystemInfo};
+use sota::gateway::{Console, DBus, Gateway, Interpret, Http, Socket, Websocket};
+use sota::broadcast::Broadcast;
+use sota::http::{AuthClient, set_ca_certificates};
+use sota::interpreter::{EventInterpreter, CommandInterpreter, Interpreter, GlobalInterpreter};
+use sota::package_manager::PackageManager;
+use sota::rvi::{Edge, Services};
+
+
+macro_rules! exit {
+ ($fmt:expr, $($arg:tt)*) => {{
+ print!(concat!($fmt, "\n"), $($arg)*);
+ std::process::exit(1);
+ }}
}
-/// Parses the command line and matches it against accepted flags and options. Returns a `Matches`
-/// object. See the [getopts documentation](https://doc.rust-lang.org/getopts/getopts/index.html)
-/// for details.
-///
-/// # Arguments
-/// * `args`: A pointer to a Array of Strings. This is supposed to be the commandline as returned
-/// by [`env::args().collect()`](https://doc.rust-lang.org/stable/std/env/fn.args.html).
-/// * `program`: The invoking path or name of the executable
-fn match_args(args: &[String], program: &str) -> Matches {
- let mut options = Options::new();
- options.optflag("h", "help", "print this help message");
- options.optopt("c", "config", "change the path where the configuration \
- is expected", "FILE");
- options.optopt("r", "rvi", "explicitly set the URL, where RVI can be \
- reached", "URL");
- options.optopt("e", "edge", "explicitly set the host and port, where the \
- client should listen for connections from RVI", "HOST:PORT");
-
- let matches = match options.parse(args) {
- Ok(m) => { m }
- Err(f) => {
- error!("{}", f.to_string());
- print_usage(program, &options);
- std::process::exit(1);
+
+fn start_signal_handler(signals: Receiver<Signal>) {
+ loop {
+ match signals.recv() {
+ Some(Signal::INT) | Some(Signal::TERM) => std::process::exit(0),
+ _ => ()
}
- };
+ }
+}
- if matches.opt_present("h") {
- print_usage(program, &options);
- std::process::exit(0);
+fn start_update_poller(interval: u64, itx: Sender<Interpret>) {
+ let (etx, erx) = chan::async::<Event>();
+ let tick = chan::tick(Duration::from_secs(interval));
+ loop {
+ let _ = tick.recv();
+ itx.send(Interpret {
+ command: Command::GetNewUpdates,
+ response_tx: Some(Arc::new(Mutex::new(etx.clone())))
+ });
+ let _ = erx.recv();
+ }
+}
+
+fn send_startup_commands(config: &Config, ctx: &Sender<Command>) {
+ ctx.send(Command::Authenticate(None));
+ ctx.send(Command::RefreshSystemInfo(true));
+
+ if config.device.package_manager != PackageManager::Off {
+ config.device.package_manager.installed_packages().map(|packages| {
+ ctx.send(Command::SendInstalledPackages(packages));
+ }).unwrap_or_else(|err| exit!("Couldn't get list of packages: {}", err));
}
- matches
}
-/// Program entrypoint. Parses command line arguments and starts the main loop accordingly.
-#[cfg_attr(test, allow(dead_code))]
fn main() {
- env_logger::init().unwrap();
- let args: Vec<String> = env::args().collect();
- let program: &str = &args[0];
- let matches = match_args(&args[1..], program);
-
- let conf_file = matches.opt_str("c")
- .unwrap_or(Configuration::default_path());
- let configuration = match Configuration::read(&conf_file) {
- Ok(value) => value,
- Err(e) => {
- error!("Couldn't parse configuration file at {}: {}", conf_file, e);
- std::process::exit(126);
+ setup_logging();
+
+ let config = build_config();
+ set_ca_certificates(Path::new(&config.device.certificates_path));
+
+ let (etx, erx) = chan::async::<Event>();
+ let (ctx, crx) = chan::async::<Command>();
+ let (itx, irx) = chan::async::<Interpret>();
+
+ let mut broadcast = Broadcast::new(erx);
+ send_startup_commands(&config, &ctx);
+
+ crossbeam::scope(|scope| {
+ // Must subscribe to the signal before spawning ANY other threads
+ let signals = chan_signal::notify(&[Signal::INT, Signal::TERM]);
+ scope.spawn(move || start_signal_handler(signals));
+
+ let poll_tick = config.device.polling_interval;
+ let poll_itx = itx.clone();
+ scope.spawn(move || start_update_poller(poll_tick, poll_itx));
+
+ if config.gateway.console {
+ let cons_itx = itx.clone();
+ let cons_sub = broadcast.subscribe();
+ scope.spawn(move || Console.start(cons_itx, cons_sub));
+ }
+
+ if config.gateway.dbus {
+ let dbus_cfg = config.dbus.as_ref().unwrap_or_else(|| exit!("{}", "dbus config required for dbus gateway"));
+ let dbus_itx = itx.clone();
+ let dbus_sub = broadcast.subscribe();
+ let mut dbus = DBus { dbus_cfg: dbus_cfg.clone(), itx: itx.clone() };
+ scope.spawn(move || dbus.start(dbus_itx, dbus_sub));
}
- };
- let rvi_url: String = matches.opt_str("r")
- .unwrap_or(configuration.client.rvi_url.clone()
- .unwrap_or("http://localhost:8901".to_string()));
- let edge_url: String = matches.opt_str("e")
- .unwrap_or(configuration.client.edge_url.clone()
- .unwrap_or("localhost:9080".to_string()));
+ if config.gateway.http {
+ let http_itx = itx.clone();
+ let http_sub = broadcast.subscribe();
+ let mut http = Http { server: config.network.http_server.clone() };
+ scope.spawn(move || http.start(http_itx, http_sub));
+ }
+
+ let mut rvi = None;
+ if config.gateway.rvi {
+ let _ = config.dbus.as_ref().unwrap_or_else(|| exit!("{}", "dbus config required for rvi gateway"));
+ let rvi_cfg = config.rvi.as_ref().unwrap_or_else(|| exit!("{}", "rvi config required for rvi gateway"));
+ let rvi_edge = config.network.rvi_edge_server.clone();
+ let services = Services::new(rvi_cfg.clone(), config.device.uuid.clone(), etx.clone());
+ let mut edge = Edge::new(services.clone(), rvi_edge, rvi_cfg.client.clone());
+ scope.spawn(move || edge.start());
+ rvi = Some(services);
+ }
+
+ if config.gateway.socket {
+ let socket_itx = itx.clone();
+ let socket_sub = broadcast.subscribe();
+ let mut socket = Socket {
+ commands_path: config.network.socket_commands_path.clone(),
+ events_path: config.network.socket_events_path.clone()
+ };
+ scope.spawn(move || socket.start(socket_itx, socket_sub));
+ }
+
+ if config.gateway.websocket {
+ let ws_server = config.network.websocket_server.clone();
+ let ws_itx = itx.clone();
+ let ws_sub = broadcast.subscribe();
+ let mut ws = Websocket { server: ws_server, clients: Arc::new(Mutex::new(HashMap::new())) };
+ scope.spawn(move || ws.start(ws_itx, ws_sub));
+ }
+
+ let event_sub = broadcast.subscribe();
+ let event_ctx = ctx.clone();
+ let event_mgr = config.device.package_manager.clone();
+ scope.spawn(move || EventInterpreter {
+ package_manager: event_mgr
+ }.run(event_sub, event_ctx));
+
+ let cmd_itx = itx.clone();
+ scope.spawn(move || CommandInterpreter.run(crx, cmd_itx));
+
+ scope.spawn(move || GlobalInterpreter {
+ config: config,
+ token: None,
+ http_client: Box::new(AuthClient::default()),
+ rvi: rvi,
+ loopback_tx: itx,
+ }.run(irx, etx));
+
+ scope.spawn(move || broadcast.start());
+ });
+}
+
+fn setup_logging() {
+ let version = option_env!("SOTA_VERSION").unwrap_or("?");
+ let mut builder = LogBuilder::new();
+ builder.format(move |record: &LogRecord| {
+ let timestamp = format!("{}", time::now_utc().rfc3339());
+ format!("{} ({}): {} - {}", timestamp, version, record.level(), record.args())
+ });
+ builder.filter(Some("hyper"), LogLevelFilter::Info);
+
+ let _ = env::var("RUST_LOG").map(|level| builder.parse(&level));
+ builder.init().expect("env_logger::init() called twice, blame the programmers.");
+}
+
+fn build_config() -> Config {
+ let args = env::args().collect::<Vec<String>>();
+ let program = args[0].clone();
+ let mut opts = Options::new();
+
+ opts.optflag("h", "help", "print this help menu");
+ opts.optopt("", "config", "change config path", "PATH");
+
+ opts.optopt("", "auth-server", "change the auth server", "URL");
+ opts.optopt("", "auth-client-id", "change the auth client id", "ID");
+ opts.optopt("", "auth-client-secret", "change the auth client secret", "SECRET");
+ opts.optopt("", "auth-credentials-file", "change the auth credentials file", "PATH");
+
+ opts.optopt("", "core-server", "change the core server", "URL");
+
+ opts.optopt("", "dbus-name", "change the dbus registration name", "NAME");
+ opts.optopt("", "dbus-path", "change the dbus path", "PATH");
+ opts.optopt("", "dbus-interface", "change the dbus interface name", "INTERFACE");
+ opts.optopt("", "dbus-software-manager", "change the dbus software manager name", "NAME");
+ opts.optopt("", "dbus-software-manager-path", "change the dbus software manager path", "PATH");
+ opts.optopt("", "dbus-timeout", "change the dbus installation timeout", "TIMEOUT");
+
+ opts.optopt("", "device-uuid", "change the device uuid", "UUID");
+ opts.optopt("", "device-vin", "change the device vin", "VIN");
+ opts.optopt("", "device-packages-dir", "change downloaded directory for packages", "PATH");
+ opts.optopt("", "device-package-manager", "change the package manager", "MANAGER");
+ opts.optopt("", "device-polling-interval", "change the package polling interval", "INTERVAL");
+ opts.optopt("", "device-certificates-path", "change the OpenSSL CA certificates file", "PATH");
+ opts.optopt("", "device-system-info", "change the system information command", "PATH");
+
+ opts.optopt("", "gateway-console", "toggle the console gateway", "BOOL");
+ opts.optopt("", "gateway-dbus", "toggle the dbus gateway", "BOOL");
+ opts.optopt("", "gateway-http", "toggle the http gateway", "BOOL");
+ opts.optopt("", "gateway-rvi", "toggle the rvi gateway", "BOOL");
+ opts.optopt("", "gateway-socket", "toggle the unix domain socket gateway", "BOOL");
+ opts.optopt("", "gateway-websocket", "toggle the websocket gateway", "BOOL");
+
+ opts.optopt("", "network-http-server", "change the http server gateway address", "ADDR");
+ opts.optopt("", "network-rvi-edge-server", "change the rvi edge server gateway address", "ADDR");
+ opts.optopt("", "network-socket-commands-path", "change the socket path for reading commands", "PATH");
+ opts.optopt("", "network-socket-events-path", "change the socket path for sending events", "PATH");
+ opts.optopt("", "network-websocket-server", "change the websocket gateway address", "ADDR");
+
+ opts.optopt("", "rvi-client", "change the rvi client URL", "URL");
+ opts.optopt("", "rvi-storage-dir", "change the rvi storage directory", "PATH");
+ opts.optopt("", "rvi-timeout", "change the rvi timeout", "TIMEOUT");
+
+ let matches = opts.parse(&args[1..]).unwrap_or_else(|err| panic!(err.to_string()));
+ if matches.opt_present("h") {
+ exit!("{}", opts.usage(&format!("Usage: {} [options]", program)));
+ }
+
+ let config_file = matches.opt_str("config").unwrap_or_else(|| {
+ env::var("SOTA_CONFIG").unwrap_or_else(|_| exit!("{}", "No config file provided."))
+ });
+ let mut config = Config::load(&config_file).unwrap_or_else(|err| exit!("{}", err));
+
+ config.auth.as_mut().map(|auth_cfg| {
+ matches.opt_str("auth-client-id").map(|id| auth_cfg.client_id = id);
+ matches.opt_str("auth-client-secret").map(|secret| auth_cfg.client_secret = secret);
+ matches.opt_str("auth-server").map(|text| {
+ auth_cfg.server = text.parse().unwrap_or_else(|err| exit!("Invalid auth-server URL: {}", err));
+ });
+ });
+
+ matches.opt_str("core-server").map(|text| {
+ config.core.server = text.parse().unwrap_or_else(|err| exit!("Invalid core-server URL: {}", err));
+ });
+
+ config.dbus.as_mut().map(|dbus_cfg| {
+ matches.opt_str("dbus-name").map(|name| dbus_cfg.name = name);
+ matches.opt_str("dbus-path").map(|path| dbus_cfg.path = path);
+ matches.opt_str("dbus-interface").map(|interface| dbus_cfg.interface = interface);
+ matches.opt_str("dbus-software-manager").map(|mgr| dbus_cfg.software_manager = mgr);
+ matches.opt_str("dbus-software-manager-path").map(|mgr_path| dbus_cfg.software_manager_path = mgr_path);
+ matches.opt_str("dbus-timeout").map(|timeout| {
+ dbus_cfg.timeout = timeout.parse().unwrap_or_else(|err| exit!("Invalid dbus timeout: {}", err));
+ });
+ });
+
+ matches.opt_str("device-uuid").map(|uuid| config.device.uuid = uuid);
+ matches.opt_str("device-vin").map(|vin| config.device.vin = vin);
+ matches.opt_str("device-packages-dir").map(|path| config.device.packages_dir = path);
+ matches.opt_str("device-package-manager").map(|text| {
+ config.device.package_manager = text.parse().unwrap_or_else(|err| exit!("Invalid device package manager: {}", err));
+ });
+ matches.opt_str("device-polling-interval").map(|interval| {
+ config.device.polling_interval = interval.parse().unwrap_or_else(|err| exit!("Invalid device polling interval: {}", err));
+ });
+ matches.opt_str("device-certificates-path").map(|certs| config.device.certificates_path = certs);
+ matches.opt_str("device-system-info").map(|cmd| config.device.system_info = SystemInfo::new(cmd));
+
+ matches.opt_str("gateway-console").map(|console| {
+ config.gateway.console = console.parse().unwrap_or_else(|err| exit!("Invalid console gateway boolean: {}", err));
+ });
+ matches.opt_str("gateway-dbus").map(|dbus| {
+ config.gateway.dbus = dbus.parse().unwrap_or_else(|err| exit!("Invalid dbus gateway boolean: {}", err));
+ });
+ matches.opt_str("gateway-http").map(|http| {
+ config.gateway.http = http.parse().unwrap_or_else(|err| exit!("Invalid http gateway boolean: {}", err));
+ });
+ matches.opt_str("gateway-rvi").map(|rvi| {
+ config.gateway.rvi = rvi.parse().unwrap_or_else(|err| exit!("Invalid rvi gateway boolean: {}", err));
+ });
+ matches.opt_str("gateway-socket").map(|socket| {
+ config.gateway.socket = socket.parse().unwrap_or_else(|err| exit!("Invalid socket gateway boolean: {}", err));
+ });
+ matches.opt_str("gateway-websocket").map(|websocket| {
+ config.gateway.websocket = websocket.parse().unwrap_or_else(|err| exit!("Invalid websocket gateway boolean: {}", err));
+ });
+
+ matches.opt_str("network-http-server").map(|server| config.network.http_server = server);
+ matches.opt_str("network-rvi-edge-server").map(|server| config.network.rvi_edge_server = server);
+ matches.opt_str("network-socket-commands-path").map(|path| config.network.socket_commands_path = path);
+ matches.opt_str("network-socket-events-path").map(|path| config.network.socket_events_path = path);
+ matches.opt_str("network-websocket-server").map(|server| config.network.websocket_server = server);
+
+ config.rvi.as_mut().map(|rvi_cfg| {
+ matches.opt_str("rvi-client").map(|url| {
+ rvi_cfg.client = url.parse().unwrap_or_else(|err| exit!("Invalid rvi-client URL: {}", err));
+ });
+ matches.opt_str("rvi-storage-dir").map(|dir| rvi_cfg.storage_dir = dir);
+ matches.opt_str("rvi-timeout").map(|timeout| {
+ rvi_cfg.timeout = Some(timeout.parse().unwrap_or_else(|err| exit!("Invalid rvi timeout: {}", err)));
+ });
+ });
- genivi::start::start(&configuration, rvi_url, edge_url);
+ config
}
diff --git a/src/oauth2.rs b/src/oauth2.rs
new file mode 100644
index 0000000..0c5f152
--- /dev/null
+++ b/src/oauth2.rs
@@ -0,0 +1,53 @@
+use rustc_serialize::json;
+
+use datatype::{AccessToken, Error, Url};
+use http::Client;
+
+
+/// Authenticate with the specified OAuth2 server to retrieve a new `AccessToken`.
+pub fn authenticate(server: Url, client: &Client) -> Result<AccessToken, Error> {
+ debug!("authenticating at {}", server);
+ let resp_rx = client.post(server, None);
+ let resp = resp_rx.recv().expect("no authenticate response received");
+ let data = try!(resp);
+ let body = try!(String::from_utf8(data));
+ Ok(try!(json::decode(&body)))
+}
+
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use datatype::{AccessToken, Url};
+ use http::TestClient;
+
+
+ fn test_server() -> Url {
+ "http://localhost:8000".parse().unwrap()
+ }
+
+ #[test]
+ fn test_authenticate() {
+ let token = r#"{
+ "access_token": "token",
+ "token_type": "type",
+ "expires_in": 10,
+ "scope": "scope1 scope2"
+ }"#;
+ let client = TestClient::from(vec![token.to_string()]);
+ let expect = AccessToken {
+ access_token: "token".to_string(),
+ token_type: "type".to_string(),
+ expires_in: 10,
+ scope: "scope1 scope2".to_string()
+ };
+ assert_eq!(expect, authenticate(test_server(), &client).unwrap());
+ }
+
+ #[test]
+ fn test_authenticate_bad_json() {
+ let client = TestClient::from(vec![r#"{"apa": 1}"#.to_string()]);
+ let expect = r#"Failed to decode JSON: MissingFieldError("access_token")"#;
+ assert_eq!(expect, format!("{}", authenticate(test_server(), &client).unwrap_err()));
+ }
+}
diff --git a/src/package_manager/deb.rs b/src/package_manager/deb.rs
new file mode 100644
index 0000000..bba86e6
--- /dev/null
+++ b/src/package_manager/deb.rs
@@ -0,0 +1,48 @@
+use std::process::Command;
+
+use datatype::{Error, Package, UpdateResultCode};
+use package_manager::package_manager::{InstallOutcome, parse_package};
+
+
+/// Returns a list of installed DEB packages with
+/// `dpkg-query -f='${Package} ${Version}\n -W`.
+pub fn installed_packages() -> Result<Vec<Package>, Error> {
+ Command::new("dpkg-query").arg("-f='${Package} ${Version}\n'").arg("-W")
+ .output()
+ .map_err(|e| Error::Package(format!("Error fetching packages: {}", e)))
+ .and_then(|c| {
+ String::from_utf8(c.stdout)
+ .map_err(|e| Error::Parse(format!("Error parsing package: {}", e)))
+ .map(|s| s.lines().map(String::from).collect::<Vec<String>>())
+ })
+ .and_then(|lines| {
+ lines.iter()
+ .map(|line| parse_package(line))
+ .filter(|pkg| pkg.is_ok())
+ .collect::<Result<Vec<Package>, _>>()
+ })
+}
+
+/// Installs a new DEB package.
+pub fn install_package(path: &str) -> Result<InstallOutcome, InstallOutcome> {
+ let output = try!(Command::new("dpkg").arg("-E").arg("-i").arg(path)
+ .output()
+ .map_err(|e| (UpdateResultCode::GENERAL_ERROR, format!("{:?}", e))));
+
+ let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
+ let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
+
+ match output.status.code() {
+ Some(0) => {
+ if (&stdout).contains("already installed") {
+ Ok((UpdateResultCode::ALREADY_PROCESSED, stdout))
+ } else {
+ Ok((UpdateResultCode::OK, stdout))
+ }
+ }
+ _ => {
+ let out = format!("stdout: {}\nstderr: {}", stdout, stderr);
+ Err((UpdateResultCode::INSTALL_FAILED, out))
+ }
+ }
+}
diff --git a/src/package_manager/mod.rs b/src/package_manager/mod.rs
new file mode 100644
index 0000000..6596686
--- /dev/null
+++ b/src/package_manager/mod.rs
@@ -0,0 +1,8 @@
+pub mod deb;
+pub mod package_manager;
+pub mod rpm;
+pub mod tpm;
+pub mod otb;
+
+pub use self::package_manager::PackageManager;
+pub use self::tpm::{assert_rx, TestDir};
diff --git a/src/package_manager/otb.rs b/src/package_manager/otb.rs
new file mode 100644
index 0000000..fece27b
--- /dev/null
+++ b/src/package_manager/otb.rs
@@ -0,0 +1,53 @@
+use std::process::Command;
+
+use datatype::{Error, Package, UpdateResultCode};
+use package_manager::package_manager::{InstallOutcome, parse_package};
+
+
+/// Returns a list of installed `OSTree` packages with
+/// `otbpkg --repo=${repodir} --query`.
+pub fn installed_packages(repodir: &str) -> Result<Vec<Package>, Error> {
+ Command::new("otbpkg")
+ .arg(format!{"--repo={}", repodir})
+ .arg("--query")
+ .output()
+ .map_err(|e| Error::Package(format!("Error fetching packages: {}", e)))
+ .and_then(|c| {
+ String::from_utf8(c.stdout)
+ .map_err(|e| Error::Parse(format!("Error parsing package: {}", e)))
+ .map(|s| s.lines().map(String::from).collect::<Vec<String>>())
+ })
+ .and_then(|lines| {
+ lines.iter()
+ .map(|line| parse_package(line))
+ .filter(|pkg| pkg.is_ok())
+ .collect::<Result<Vec<Package>, _>>()
+ })
+}
+
+/// Installs a new `OSTree` package.
+pub fn install_package(repodir: &str, path: &str) -> Result<InstallOutcome, InstallOutcome> {
+ let output = try!(Command::new("otbpkg")
+ .arg("--install")
+ .arg(format!("--repo={}", repodir))
+ .arg(path)
+ .output()
+ .map_err(|e| (UpdateResultCode::GENERAL_ERROR, format!("{:?}", e))));
+
+ let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
+ let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
+
+ match output.status.code() {
+ Some(0) => {
+ if (&stdout).contains("already installed") {
+ Ok((UpdateResultCode::ALREADY_PROCESSED, stdout))
+ } else {
+ Ok((UpdateResultCode::OK, stdout))
+ }
+ }
+ _ => {
+ let out = format!("stdout: {}\nstderr: {}", stdout, stderr);
+ Err((UpdateResultCode::INSTALL_FAILED, out))
+ }
+ }
+}
diff --git a/src/package_manager/package_manager.rs b/src/package_manager/package_manager.rs
new file mode 100644
index 0000000..09556a0
--- /dev/null
+++ b/src/package_manager/package_manager.rs
@@ -0,0 +1,135 @@
+use rustc_serialize::{Decoder, Decodable};
+use std::str::FromStr;
+
+use datatype::{Error, Package, UpdateResultCode};
+use package_manager::{deb, otb, rpm, tpm};
+
+
+/// The outcome when installing a package as a tuple of the `UpdateResultCode`
+/// and any stdout/stderr output.
+pub type InstallOutcome = (UpdateResultCode, String);
+
+/// An enumeration of the available package managers for querying and installing
+/// new packages.
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum PackageManager {
+ Off,
+ Deb,
+ Rpm,
+ File { filename: String, succeeds: bool },
+ OSTree { repodir: String }
+}
+
+impl PackageManager {
+ /// Delegates to the package manager specific function for returning a list
+ /// of installed packages.
+ pub fn installed_packages(&self) -> Result<Vec<Package>, Error> {
+ match *self {
+ PackageManager::Off => panic!("no package manager"),
+ PackageManager::Deb => deb::installed_packages(),
+ PackageManager::Rpm => rpm::installed_packages(),
+ PackageManager::File { ref filename, .. } => tpm::installed_packages(filename),
+ PackageManager::OSTree { ref repodir } => otb::installed_packages(repodir),
+ }
+ }
+
+ /// Delegates to the package manager specific function for installing a new
+ /// package on the device.
+ pub fn install_package(&self, path: &str) -> Result<InstallOutcome, InstallOutcome> {
+ match *self {
+ PackageManager::Off => panic!("no package manager"),
+ PackageManager::Deb => deb::install_package(path),
+ PackageManager::Rpm => rpm::install_package(path),
+ PackageManager::File { ref filename, succeeds } => {
+ tpm::install_package(filename, path, succeeds)
+ }
+ PackageManager::OSTree { ref repodir } => {
+ otb::install_package(repodir, path)
+ }
+ }
+ }
+
+ /// Returns a string representation of the package manager's extension.
+ pub fn extension(&self) -> String {
+ match *self {
+ PackageManager::Off => panic!("no package manager"),
+ PackageManager::Deb => "deb".to_string(),
+ PackageManager::Rpm => "rpm".to_string(),
+ PackageManager::File { ref filename, .. } => filename.to_string(),
+ PackageManager::OSTree {..} => "otb".to_string(),
+ }
+ }
+}
+
+impl FromStr for PackageManager {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<PackageManager, Error> {
+ match s.to_lowercase().as_str() {
+ "off" => Ok(PackageManager::Off),
+ "deb" => Ok(PackageManager::Deb),
+ "rpm" => Ok(PackageManager::Rpm),
+
+ file if file.len() > 5 && file[..5].as_bytes() == b"file:" => {
+ Ok(PackageManager::File { filename: file[5..].to_string(), succeeds: true })
+ },
+
+ repo if repo.len() > 4 && repo[..4].as_bytes() == b"otb:" => {
+ Ok(PackageManager::OSTree { repodir: repo[4..].to_string() })
+ }
+
+ _ => Err(Error::Parse(format!("unknown package manager: {}", s)))
+ }
+ }
+}
+
+impl Decodable for PackageManager {
+ fn decode<D: Decoder>(d: &mut D) -> Result<PackageManager, D::Error> {
+ d.read_str().and_then(|s| Ok(s.parse::<PackageManager>().expect("couldn't parse PackageManager")))
+ }
+}
+
+pub fn parse_package(line: &str) -> Result<Package, Error> {
+ match line.splitn(2, ' ').collect::<Vec<_>>() {
+ ref parts if parts.len() == 2 => {
+ // HACK: strip left single quotes from stdout
+ Ok(Package {
+ name: String::from(parts[0].trim_left_matches('\'')),
+ version: String::from(parts[1])
+ })
+ },
+ _ => Err(Error::Parse(format!("Couldn't parse package: {}", line)))
+ }
+}
+
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use datatype::Package;
+
+
+ #[test]
+ fn test_parses_normal_package() {
+ assert_eq!(parse_package("uuid-runtime 2.20.1-5.1ubuntu20.7").unwrap(),
+ Package {
+ name: "uuid-runtime".to_string(),
+ version: "2.20.1-5.1ubuntu20.7".to_string()
+ });
+ }
+
+ #[test]
+ fn test_separates_name_and_version_correctly() {
+ assert_eq!(parse_package("vim 2.1 foobar").unwrap(),
+ Package {
+ name: "vim".to_string(),
+ version: "2.1 foobar".to_string()
+ });
+ }
+
+ #[test]
+ fn test_rejects_bogus_input() {
+ assert_eq!(format!("{}", parse_package("foobar").unwrap_err()),
+ "Parse error: Couldn't parse package: foobar".to_string());
+ }
+}
diff --git a/src/package_manager/rpm.rs b/src/package_manager/rpm.rs
new file mode 100644
index 0000000..99aacbf
--- /dev/null
+++ b/src/package_manager/rpm.rs
@@ -0,0 +1,46 @@
+use std::process::Command;
+
+use datatype::{Error, Package, UpdateResultCode};
+use package_manager::package_manager::{InstallOutcome, parse_package};
+
+
+/// Returns a list of installed RPM packages with
+/// `rpm -qa ==queryformat ${NAME} ${VERSION}\n`.
+pub fn installed_packages() -> Result<Vec<Package>, Error> {
+ Command::new("rpm").arg("-qa").arg("--queryformat").arg("%{NAME} %{VERSION}\n")
+ .output()
+ .map_err(|e| Error::Package(format!("Error fetching packages: {}", e)))
+ .and_then(|c| {
+ String::from_utf8(c.stdout)
+ .map_err(|e| Error::Parse(format!("Error parsing package: {}", e)))
+ .map(|s| s.lines().map(String::from).collect::<Vec<String>>())
+ })
+ .and_then(|lines| {
+ lines.iter()
+ .map(|line| parse_package(line))
+ .filter(|item| item.is_ok())
+ .collect::<Result<Vec<Package>, _>>()
+ })
+}
+
+/// Installs a new RPM package.
+pub fn install_package(path: &str) -> Result<InstallOutcome, InstallOutcome> {
+ let output = try!(Command::new("rpm").arg("-Uvh").arg("--force").arg(path)
+ .output()
+ .map_err(|e| (UpdateResultCode::GENERAL_ERROR, format!("{:?}", e))));
+
+ let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
+ let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
+
+ match output.status.code() {
+ Some(0) => Ok((UpdateResultCode::OK, stdout)),
+ _ => {
+ let out = format!("stdout: {}\nstderr: {}", stdout, stderr);
+ if (&stderr).contains("already installed") {
+ Ok((UpdateResultCode::ALREADY_PROCESSED, out))
+ } else {
+ Err((UpdateResultCode::INSTALL_FAILED, out))
+ }
+ }
+ }
+}
diff --git a/src/package_manager/tpm.rs b/src/package_manager/tpm.rs
new file mode 100644
index 0000000..feafe0a
--- /dev/null
+++ b/src/package_manager/tpm.rs
@@ -0,0 +1,162 @@
+use chan::Receiver;
+use std::fmt::Debug;
+use std::fs;
+use std::fs::File;
+use std::fs::OpenOptions;
+use std::io::BufReader;
+use std::io::prelude::*;
+use time;
+
+use datatype::{Error, Package, UpdateResultCode};
+use package_manager::package_manager::{InstallOutcome, PackageManager};
+
+
+impl PackageManager {
+ /// Creates a new Test Package Manager that writes to a temporary file.
+ pub fn new_tpm(succeeds: bool) -> Self {
+ let name = format!("/tmp/sota-tpm-{}", time::precise_time_ns().to_string());
+ if succeeds {
+ let _ = File::create(name.clone()).expect("couldn't create Test Package Manager file");
+ }
+ PackageManager::File { filename: name, succeeds: succeeds }
+ }
+}
+
+
+/// Encapsulate a directory whose contents will be destroyed when it drops out of scope.
+pub struct TestDir(pub String);
+
+impl TestDir {
+ /// Create a new test directory that will be destroyed when it drops out of scope.
+ pub fn new(reason: &str) -> TestDir {
+ let dir = format!("/tmp/{}-{}", reason, time::precise_time_ns().to_string());
+ fs::create_dir_all(dir.clone()).expect("couldn't create TempDir");
+ TestDir(dir)
+ }
+}
+
+impl Drop for TestDir {
+ fn drop(&mut self) {
+ fs::remove_dir_all(&self.0.clone()).expect("couldn't remove TempDir");
+ }
+}
+
+
+/// For each item in the list, assert that it equals the next `Receiver` value.
+pub fn assert_rx<X: PartialEq + Debug>(rx: Receiver<X>, xs: &[X]) {
+ let n = xs.len();
+ let mut xs = xs.iter();
+ for _ in 0..n {
+ let val = rx.recv().expect("assert_rx expected another val");
+ let x = xs.next().expect(&format!("assert_rx: no match for val: {:?}", val));
+ assert_eq!(val, *x);
+ }
+}
+
+
+/// Returns a list of installed packages from a format of `<name> <version>`.
+pub fn installed_packages(path: &str) -> Result<Vec<Package>, Error> {
+ let f = try!(File::open(path));
+ let reader = BufReader::new(f);
+ let mut pkgs = Vec::new();
+
+ for line in reader.lines() {
+ let line = try!(line);
+ let parts = line.split(' ');
+
+ if parts.clone().count() == 2 {
+ if let Some(name) = parts.clone().nth(0) {
+ if let Some(version) = parts.clone().nth(1) {
+ pkgs.push(Package {
+ name: name.to_string(),
+ version: version.to_string()
+ });
+ }
+ }
+ }
+ }
+
+ Ok(pkgs)
+}
+
+/// Installs a package to the specified path when succeeds is true, or fails otherwise.
+pub fn install_package(path: &str, pkg: &str, succeeds: bool) -> Result<InstallOutcome, InstallOutcome> {
+ if !succeeds {
+ return Err((UpdateResultCode::INSTALL_FAILED, "failed".to_string()))
+ }
+
+ let outcome = || -> Result<(), Error> {
+ let mut f = OpenOptions::new().create(true).write(true).append(true).open(path).unwrap();
+ try!(f.write(pkg.as_bytes()));
+ try!(f.write(b"\n"));
+ Ok(())
+ }();
+
+ match outcome {
+ Ok(_) => Ok((UpdateResultCode::OK, "".to_string())),
+ Err(err) => Err((UpdateResultCode::INSTALL_FAILED, format!("{:?}", err)))
+ }
+}
+
+
+#[cfg(test)]
+mod tests {
+ use std::fs::File;
+ use std::io::prelude::*;
+
+ use super::*;
+ use datatype::Package;
+
+
+ fn pkg1() -> Package {
+ Package {
+ name: "apa".to_string(),
+ version: "0.0.0".to_string()
+ }
+ }
+
+ fn pkg2() -> Package {
+ Package {
+ name: "bepa".to_string(),
+ version: "1.0.0".to_string()
+ }
+ }
+
+
+ #[test]
+ fn get_installed_packages() {
+ let dir = TestDir::new("sota-tpm-test-1");
+ let path = format!("{}/tpm", dir.0);
+ let mut f = File::create(path.clone()).unwrap();
+ f.write(b"apa 0.0.0\n").unwrap();
+ f.write(b"bepa 1.0.0").unwrap();
+ assert_eq!(installed_packages(&path).unwrap(), vec![pkg1(), pkg2()]);
+ }
+
+ #[test]
+ fn ignore_bad_installed_packages() {
+ let dir = TestDir::new("sota-tpm-test-2");
+ let path = format!("{}/tpm", dir.0);
+ let mut f = File::create(path.clone()).unwrap();
+ f.write(b"cepa-2.0.0\n").unwrap();
+ assert_eq!(installed_packages(&path).unwrap(), Vec::new());
+ }
+
+ #[test]
+ fn install_packages() {
+ let dir = TestDir::new("sota-tpm-test-3");
+ let path = format!("{}/tpm", dir.0);
+ install_package(&path, "apa 0.0.0", true).unwrap();
+ install_package(&path, "bepa 1.0.0", true).unwrap();
+ assert_eq!(installed_packages(&path).unwrap(), vec![pkg1(), pkg2()]);
+ }
+
+ #[test]
+ fn failed_installation() {
+ let dir = TestDir::new("sota-tpm-test-4");
+ let path = format!("{}/tpm", dir.0);
+ assert!(install_package(&path, "apa 0.0.0", false).is_err());
+ install_package(&path, "bepa 1.0.0", true).unwrap();
+ assert_eq!(installed_packages(&path).unwrap(), vec![pkg2()]);
+ }
+}
diff --git a/src/remote/dw.rs b/src/remote/dw.rs
deleted file mode 100644
index 0fd8b8e..0000000
--- a/src/remote/dw.rs
+++ /dev/null
@@ -1,581 +0,0 @@
-//! Handles caching and storage on disk for in-progress transfers and the assembly and verification
-//! of finished transfers
-
-use std::fs;
-use std::fs::{OpenOptions, DirEntry, File};
-use std::io::prelude::*;
-use std::path::PathBuf;
-use std::vec::Vec;
-use std::str::FromStr;
-
-use time;
-
-#[cfg(test)] use rand;
-#[cfg(test)] use rand::Rng;
-#[cfg(test)] use test_library::PathPrefix;
-
-use crypto::sha1::Sha1;
-use crypto::digest::Digest;
-
-use rustc_serialize::base64::FromBase64;
-
-use event::UpdateId;
-
-/// Type for storing the metadata of a in-progress transfer, which is defined as one package.
-/// Will clear out the chunks on disk when freed.
-pub struct Transfer {
- pub update_id: UpdateId,
- /// SHA1 checksum of the fully assembled package.
- pub checksum: String,
- /// `Vector` of transferred chunks.
- pub transferred_chunks: Vec<u64>,
- /// Path to the directory, where chunks will be cached and finished packages will be stored.
- pub prefix_dir: String,
- /// Timestamp, when the last chunk was received. Given as a unix epoch timestamp.
- pub last_chunk_received: i64
-}
-
-impl Transfer {
- /// Return a new `Transfer`
- ///
- /// # Arguments
- /// * `prefix`: Path where transferred chunks and assembled package will be stored.
- /// * `package`: [`PackageId`](../message/struct.PackageId.html) of this transfer.
- /// * `checksum`: SHA1 checksum of the fully assembled package.
- pub fn new(prefix: String, id: UpdateId, checksum: String)
- -> Transfer {
- Transfer {
- update_id: id,
- checksum: checksum,
- transferred_chunks: Vec::new(),
- prefix_dir: prefix,
- last_chunk_received: time::get_time().sec
- }
- }
-
- /// Create a transfer with empty values. To be used in tests.
- ///
- /// # Arguments
- /// * `prefix`: Path where transferred chunks and assembled package will be stored. This should
- /// be a temporary directory for tests.
- #[cfg(test)]
- pub fn new_test(prefix: &PathPrefix) -> Transfer {
- Transfer {
- update_id: UpdateId::new(),
- checksum: "".to_string(),
- transferred_chunks: Vec::new(),
- prefix_dir: prefix.to_string(),
- last_chunk_received: time::get_time().sec
- }
- }
-
- /// Randomize a existing transfer, by creating a random
- /// [`PackageId`](../message/struct.PackageId.html). Returns the created `PackageId`, so it can
- /// be used in assertions.
- ///
- /// # Arguments
- /// * `i`: Size of the name and version strings.
- #[cfg(test)]
- pub fn randomize(&mut self, i: usize) -> UpdateId {
- let update_id = rand::thread_rng()
- .gen_ascii_chars().take(i).collect::<String>();
-
- trace!("Testing with:");
- trace!(" update_id: {}", update_id);
- self.update_id = update_id.clone();
- update_id
- }
-
- /// Write a transferred chunk to disk. Returns false and logs an error if something goes wrong.
- ///
- /// # Arguments
- /// * `msg`: Base64 encoded data of this chunk.
- /// * `index`: Index of this chunk
- pub fn write_chunk(&mut self,
- msg: &str,
- index: u64) -> bool {
- let success = msg.from_base64().map_err(|e| {
- error!("Could not decode chunk {} for update_id {}", index, self.update_id);
- error!("{}", e)
- }).and_then(|msg| self.get_chunk_path(index).map_err(|e| {
- error!("Could not get path for chunk {}", index);
- error!("{}", e)
- }).map(|path| {
- trace!("Saving chunk to {}", path.display());
- if write_new_file(&path, &msg) {
- self.transferred_chunks.push(index);
- self.transferred_chunks.sort();
- self.transferred_chunks.dedup();
- true
- } else {
- error!("Couldn't write chunk {} for update_id {}", index, self.update_id);
- false
- }
- })).unwrap_or(false);
-
- self.last_chunk_received = time::get_time().sec;
- success
- }
-
- /// Assemble the transferred chunks to a package and verify it with the provided checksum.
- /// Returns `false` and prints a error message if either the package can't be assembled or the
- /// checksum doesn't match.
- pub fn assemble_package(&self) -> Result<PathBuf, String> {
- trace!("Finalizing package {}", self.update_id);
- self.assemble_chunks().
- and_then(|_| {
- if self.checksum() {
- self.get_package_path()
- } else {
- Err(format!("Cannot assemble_package for update_id: {}", self.update_id))
- }
- })
- }
-
- /// Collect all chunks and concatenate them into one file. Returns a `String` with a error
- /// message, should something go wrong.
- fn assemble_chunks(&self) -> Result<(), String> {
- let package_path = try!(self.get_package_path());
-
- trace!("Saving update_id {} to {}", self.update_id, package_path.display());
-
- let mut file = try!(OpenOptions::new()
- .write(true).append(true)
- .create(true).truncate(true)
- .open(package_path)
- .map_err(|x| format!("Couldn't open file: {}", x)));
-
- let path: PathBuf = try!(self.get_chunk_dir());
-
- // Make sure all indices are valid and sort them
- let mut indices = Vec::new();
- for entry in try!(read_dir(&path)) {
- let entry = try!(entry.map_err(|x| format!("No entries: {}", x)));
- indices.push(try!(parse_index(entry)));
- }
- indices.sort();
-
- // Append indices to the final file
- for index in indices {
- try!(self.copy_chunk(&path, index, &mut file));
- }
- Ok(())
- }
-
- /// Read a chunk file file and append it to a package file. Returns a `String` with a error
- /// message should something go wrong.
- ///
- /// # Arguments
- /// * `path`: Pointer to a [`PathBuf`]
- /// (https://doc.rust-lang.org/stable/std/path/struct.PathBuf.html) where the chunks are
- /// cached.
- /// * `index`: Index of the chunk to append.
- /// * `file`: Pointer to a `File` where the chunk should be appended. Should be created with
- /// `OpenOptions` and the append only option. See the documentation for [`OpenOptions`]
- /// (https://doc.rust-lang.org/stable/std/fs/struct.OpenOptions.html), [`File`]
- /// (https://doc.rust-lang.org/stable/std/fs/struct.File.html), and the implementation of
- /// [`assemble_chunks`](#method.assemble_chunks) for details.
- fn copy_chunk(&self, path: &PathBuf, index: u64, file: &mut File)
- -> Result<(), String> {
- let name = index.to_string();
- let mut chunk_path = path.clone();
- chunk_path.push(&name);
- let mut chunk =
- try!(OpenOptions::new().open(chunk_path)
- .map_err(|x| format!("Couldn't open file: {}", x)));
-
- let mut buf = Vec::new();
- try!(chunk.read_to_end(&mut buf)
- .map_err(|x| format!("Couldn't read file {}: {}", name, x)));
- try!(file.write(&mut buf)
- .map_err(|x| format!("Couldn't write chunk {} to file {}: {}",
- name, self.update_id, x)));
-
- trace!("Wrote chunk {} to update_id {}", name, self.update_id);
- Ok(())
- }
-
- /// Verify the checksum of this transfer. Assumes the package was already assembled. Prints a
- /// error message showing the mismatched checksums and returns false on errors.
- fn checksum(&self) -> bool {
- let path = try_or!(self.get_package_path(), return false);
- let mut file = try_or!(OpenOptions::new().open(path), return false);
- let mut data = Vec::new();
-
- // TODO: avoid reading in the whole file at once
- try_msg_or!(file.read_to_end(&mut data),
- "Couldn't read file to check",
- return false);
-
- let mut hasher = Sha1::new();
- hasher.input(&data);
- let hash = hasher.result_str();
-
- if hash == self.checksum {
- true
- } else {
- error!("Checksums didn't match for update_id {}", self.update_id);
- error!(" Expected: {}", self.checksum);
- error!(" Got: {}", hash);
- false
- }
- }
-
- /// Get the full path for the specified chunk index. Returns a
- /// [`PathBuf`](https://doc.rust-lang.org/stable/std/path/struct.PathBuf.html) on success or a
- /// `String` on errors detailing what went wrong.
- ///
- /// # Arguments
- /// * `index`: The index for which the path should be constructed
- fn get_chunk_path(&self, index: u64) -> Result<PathBuf, String> {
- let mut path = try!(self.get_chunk_dir());
- let filename = index.to_string();
-
- trace!("Using filename {}", filename);
- path.push(filename);
- Ok(path)
- }
-
- /// Get the full path for the package of this `Transfer`. Returns a
- /// [`PathBuf`](https://doc.rust-lang.org/stable/std/path/struct.PathBuf.html) on success or a
- /// `String` on errors detailing what went wrong.
- fn get_package_path(&self) -> Result<PathBuf, String> {
- let mut path = try!(self.get_package_dir());
- path.push(format!("{}.spkg", self.update_id));
- Ok(path)
- }
-
- /// Get the directory, where this `Transfer` caches chunks. Returns a
- /// [`PathBuf`](https://doc.rust-lang.org/stable/std/path/struct.PathBuf.html) on success or a
- /// `String` on errors detailing what went wrong.
- fn get_chunk_dir(&self) -> Result<PathBuf, String> {
- let mut path = PathBuf::from(&self.prefix_dir);
- path.push("downloads");
- path.push(format!("{}", self.update_id));
-
- fs::create_dir_all(&path).map_err(|e| {
- let path_str = path.to_str().unwrap_or("unknown");
- format!("Couldn't create chunk dir at '{}': {}", path_str, e)
- }).map(|_| path)
- }
-
- /// Get the directory, where this `Transfer` stores the assembled package. Returns a
- /// [`PathBuf`](https://doc.rust-lang.org/stable/std/path/struct.PathBuf.html) on success or a
- /// `String` on errors detailing what went wrong.
- fn get_package_dir(&self) -> Result<PathBuf, String> {
- let mut path = PathBuf::from(&self.prefix_dir);
- path.push("packages");
-
- fs::create_dir_all(&path).map_err(|e| {
- let path_str = path.to_str().unwrap_or("unknown");
- format!("Couldn't create packges dir at '{}': {}", path_str, e)
- }).map(|_| path)
- }
-}
-
-impl Drop for Transfer {
- /// When a `Transfer` is freed it will also clear out the associated chunk cache on disk.
- fn drop(&mut self) {
- let dir = try_or!(self.get_chunk_dir(), return);
- trace!("Dropping transfer for package {}", self.update_id);
-
- for entry in try_or!(read_dir(&dir), return) {
- let entry = try_or!(entry, continue);
- let _ = entry.file_name().into_string().map_err(|_|
- error!("Found a malformed entry!")
- ).map(|name| {
- trace!("Dropping chunk file {}", name);
- try_or!(fs::remove_file(entry.path()), return);
- });
- }
-
- try_or!(fs::remove_dir(dir), return);
- }
-}
-
-/// Write the provided `data` to the file at `path`. Will create the file if it doesn't exist and
-/// overwrite existing files. Returns `false` on errors, after logging a error message.
-///
-/// # Arguments
-/// * `path`: Pointer to a [`PathBuf`]
-/// (https://doc.rust-lang.org/stable/std/path/struct.PathBuf.html) where the data will be
-/// written to. Needs to point to a (possibly nonexistent) file.
-/// * `data`: The data to be written to disk.
-fn write_new_file(path: &PathBuf, data: &Vec<u8>) -> bool {
- let mut file = try_or!(OpenOptions::new()
- .write(true).create(true)
- .truncate(true).open(path),
- return false);
-
- try_or!(file.write_all(data), return false);
- try_or!(file.flush(), return false);
- true
-}
-
-/// Read the contents of a directory. Returns a
-/// [`ReadDir`](https://doc.rust-lang.org/stable/std/fs/struct.ReadDir.html) iterator on success or
-/// a `String` with a detailed error message on failure.
-fn read_dir(path: &PathBuf) -> Result<fs::ReadDir, String> {
- fs::read_dir(path).map_err(|e| {
- let path_str = path.to_str().unwrap_or("unknown");
- format!("Couldn't read dir at '{}': {}", path_str, e)
- })
-}
-
-/// Parse a [`DirEntry`](https://doc.rust-lang.org/stable/std/fs/struct.DirEntry.html) to a `u64`.
-/// Returns the parsed number on success or a `String` with a detailed error message on failure.
-///
-/// # Arguments
-/// * `entry`: `DirEntry` to be parsed.
-fn parse_index(entry: DirEntry) -> Result<u64, String> {
- let name = entry.file_name().into_string()
- .unwrap_or("unknown".to_string());
- u64::from_str(&name)
- .map_err(|_| "Couldn't parse chunk index from filename".to_string())
-}
-
-use std::collections::HashMap;
-
-/// Type alias to hide the internal `HashMap`, that is used to store
-/// [`Transfer`](../persistence/struct.Transfer.html)s.
-pub struct Transfers {
- items: HashMap<UpdateId, Transfer>,
- storage_dir: String
-}
-
-impl Transfers {
- pub fn new(dir: String) -> Transfers {
- Transfers {
- items: HashMap::new(),
- storage_dir: dir
- }
- }
-
- pub fn get(&self, pkg: &UpdateId) -> Option<&Transfer> {
- self.items.get(pkg)
- }
-
- pub fn get_mut(&mut self, pkg: &UpdateId) -> Option<&mut Transfer> {
- self.items.get_mut(pkg)
- }
-
- pub fn push(&mut self, pkg: UpdateId, cksum: String) {
- self.items.insert(
- pkg.clone(),
- Transfer::new(self.storage_dir.to_string(), pkg, cksum));
- }
-
- #[cfg(test)]
- pub fn push_test(&mut self, tr: Transfer) {
- self.items.insert(tr.update_id.clone(), tr);
- }
-
- #[cfg(test)]
- pub fn is_empty(&self) -> bool {
- self.items.is_empty()
- }
-
- pub fn remove(&mut self, pkg: &UpdateId) {
- self.items.remove(pkg);
- }
-
- pub fn clear(&mut self) {
- self.items.clear();
- }
-
- pub fn prune(&mut self, now: i64, timeout: i64) {
- self.items.iter()
- .filter(|&(_, v)| now - v.last_chunk_received > timeout)
- .map(|(k, _)| k.clone())
- .collect::<Vec<UpdateId>>()
- .iter().map(|k| {
- self.items.remove(k);
- info!("Transfer for update_id {} timed out after {} ms", k, timeout)})
- .collect::<Vec<()>>();
- }
-}
-
-
-#[cfg(test)]
-mod test {
- use super::*;
- use test_library::*;
-
- use std::path::PathBuf;
- use std::fs;
- use std::fs::OpenOptions;
- use std::io::prelude::*;
-
- use rand;
- use rand::Rng;
- use rustc_serialize::base64;
- use rustc_serialize::base64::ToBase64;
-
- fn create_tmp_directories(prefix: &PathPrefix) {
- for i in 1..20 {
- let mut transfer = Transfer::new_test(prefix);
- let update_id = transfer.randomize(i);
- let chunk_dir: PathBuf = transfer.get_chunk_dir().unwrap();
- let path = format!("{}/downloads/{}", prefix, update_id);
- assert_eq!(chunk_dir.to_str().unwrap(), path);
-
- let path = PathBuf::from(path);
- // This also makes sure it's a directory
- let dir = fs::read_dir(&path).unwrap();
-
- for _ in dir {
- panic!("Found non-empty directory!");
- }
- }
- }
-
- #[test]
- fn it_creates_a_tmp_directory() {
- test_init!();
- let prefix = PathPrefix::new();
- create_tmp_directories(&prefix);
- }
-
- #[test]
- fn it_cleans_up_the_tmp_directories() {
- test_init!();
- let prefix = PathPrefix::new();
- create_tmp_directories(&prefix);
- let path = PathBuf::from(format!("{}/downloads/", prefix));
- let dir = fs::read_dir(&path).unwrap();
-
- for _ in dir {
- panic!("Found non-empty directory!");
- }
- }
-
- #[test]
- fn it_creates_a_persistent_directory_per_package() {
- test_init!();
- let prefix = PathPrefix::new();
- for i in 1..20 {
- let mut transfer = Transfer::new_test(&prefix);
- let update_id = transfer.randomize(i);
-
- let chunk_dir: PathBuf = transfer.get_package_path().unwrap();
- let path = format!("{}/packages/{}.spkg", prefix, update_id);
- assert_eq!(chunk_dir.to_str().unwrap(), path);
- }
- }
-
- macro_rules! assert_chunk_written {
- ($transfer:ident,
- $prefix:ident,
- $update_id:ident,
- $index:ident,
- $data:ident) => {{
- trace!("Testing with: {}", $data);
-
- let b64_data = $data.as_bytes().to_base64(
- base64::Config {
- char_set: base64::CharacterSet::UrlSafe,
- newline: base64::Newline::LF,
- pad: true,
- line_length: None
- });
-
- trace!("Encoded as: {}", b64_data);
-
- $transfer.write_chunk(&b64_data, $index as u64);
-
- let path = format!("{}/downloads/{}/{}", $prefix, $update_id, $index);
-
- trace!("Expecting file at: {}", path);
-
- let mut from_disk = Vec::new();
- OpenOptions::new()
- .open(PathBuf::from(path))
- .unwrap()
- .read_to_end(&mut from_disk)
- .unwrap();
-
- assert_eq!($data.into_bytes(), from_disk);
- }}
- }
-
- #[test]
- fn it_writes_decoded_data_to_disk() {
- test_init!();
- let prefix = PathPrefix::new();
- for i in 1..20 {
- let mut transfer = Transfer::new_test(&prefix);
- let update_id = transfer.randomize(i);
- for i in 1..20 {
- let data = rand::thread_rng()
- .gen_ascii_chars().take(i).collect::<String>();
- assert_chunk_written!(transfer, prefix, update_id, i, data);
- }
- }
- }
-
- #[test]
- fn it_correctly_assembles_stored_chunks() {
- test_init!();
- let prefix = PathPrefix::new();
- for i in 1..20 {
- let mut transfer = Transfer::new_test(&prefix);
- let update_id = transfer.randomize(i);
- let mut full_data = String::new();
- for i in 1..20 {
- let data = rand::thread_rng()
- .gen_ascii_chars().take(i).collect::<String>();
- full_data.push_str(&data);
-
- assert_chunk_written!(transfer, prefix, update_id, i, data);
- }
-
- transfer.assemble_chunks().unwrap();
-
- let path = format!("{}/packages/{}.spkg", prefix, update_id);
-
- trace!("Expecting assembled file at: {}", path);
-
- let mut from_disk = Vec::new();
- OpenOptions::new()
- .open(PathBuf::from(path))
- .unwrap()
- .read_to_end(&mut from_disk)
- .unwrap();
-
- assert_eq!(full_data.into_bytes(), from_disk);
- }
- }
-
- fn checksum_matching(data: String, checksum: String) -> bool {
- let prefix = PathPrefix::new();
- let mut transfer = Transfer::new_test(&prefix);
- let update_id = transfer.randomize(20);
- let index = 0;
- assert_chunk_written!(transfer, prefix, update_id, index, data);
- transfer.assemble_chunks().unwrap();
-
- transfer.checksum = checksum;
- transfer.checksum()
- }
-
- #[test]
- fn it_returns_true_for_correct_checksums() {
- test_init!();
- assert!(checksum_matching("test\n".to_string(),
- "4e1243bd22c66e76c2ba9eddc1f91394e57f9f83".to_string()));
- }
-
- #[test]
- fn it_returns_false_for_incorrect_checksums() {
- test_init!();
- assert!(!checksum_matching("test\n".to_string(),
- "fa7c4d75bae3a641d1f9ab5df028175bfb8a69ca".to_string()));
- }
-
- #[test]
- fn it_returns_false_for_invalid_checksums() {
- test_init!();
- assert!(!checksum_matching("test\n".to_string(),
- "invalid".to_string()));
- }
-}
diff --git a/src/remote/jsonrpc.rs b/src/remote/jsonrpc.rs
deleted file mode 100644
index 5676a37..0000000
--- a/src/remote/jsonrpc.rs
+++ /dev/null
@@ -1,150 +0,0 @@
-//! RVI specific implementation of the jsonrpc protocol
-use time;
-
-/// Type to encode a generic jsonrpc call.
-#[derive(RustcDecodable,RustcEncodable,Debug)]
-pub struct Request<T> {
- /// The version of jsonrpc to use, has to be set to `"2.0"`.
- pub jsonrpc: String,
- /// The identifier of the request. Only unsigned numbers are accepted
- // TODO: id can be any type
- pub id: u64,
- /// The method to call on the receiving side.
- pub method: String,
- /// Arguments for the method.
- pub params: T
-}
-
-impl<T> Request<T> {
- /// Returns a new `Request`.
- ///
- /// # Arguments
- /// * `s`: The name of the method to call.
- /// * `p`: The arguments of said method.
- pub fn new(s: &str, p: T) -> Request<T> {
- Request::<T> {
- jsonrpc: "2.0".to_string(),
- id: time::precise_time_ns(),
- method: s.to_string(),
- params: p
- }
- }
-}
-
-/// Response to a jsonrpc call, indicating a successful method call.
-#[derive(RustcDecodable,RustcEncodable)]
-pub struct OkResponse<T> {
- /// The version of jsonrpc to use, has to be set to `"2.0"`.
- pub jsonrpc: String,
- /// The identifier of the jsonrpc call this response belongs to. Only unsigned numbers are
- /// accepted
- // TODO: id can be any type
- pub id: u64,
- /// The result of the method call, if any.
- pub result: Option<T>
-}
-
-impl<T> OkResponse<T> {
- /// Returns a new `OkResponse`
- ///
- /// # Arguments
- /// * `id`: The identifier of the jsonrpc call the returned response belongs to.
- /// * `result`: The result of the method call, if any.
- pub fn new(id: u64, result: Option<T>) -> OkResponse<T> {
- OkResponse {
- jsonrpc: "2.0".to_string(),
- id: id,
- result: result
- }
- }
-}
-
-/// Response to a jsonrpc call, indicating failure.
-#[derive(RustcDecodable,RustcEncodable)]
-pub struct ErrResponse {
- /// The version of jsonrpc to use, has to be set to `"2.0"`.
- pub jsonrpc: String,
- /// The identifier of the jsonrpc call this response belongs to. Only unsigned numbers are
- /// accepted
- // TODO: id can be any type
- pub id: u64,
- /// The error code and message.
- pub error: ErrorCode
-}
-
-impl ErrResponse {
- /// Returns a new `ErrResponse`
- ///
- /// # Arguments
- /// * `id`: The identifier of the jsonrpc call the returned response belongs to.
- /// * `error`: The error code and message. See [`ErrorCode`](./struct.ErrorCode.html).
- pub fn new(id: u64, error: ErrorCode) -> ErrResponse {
- ErrResponse {
- jsonrpc: "2.0".to_string(),
- id: id,
- error: error
- }
- }
-
- /// Returns a new `ErrResponse`, indicating a ["Invalid
- /// Request"](http://www.jsonrpc.org/specification#error_object) error.
- pub fn invalid_request(id: u64) -> ErrResponse {
- ErrResponse::new(id,
- ErrorCode {
- code: -32600,
- message: "Invalid Request".to_string()
- })
- }
-
- /// Returns a new `ErrResponse`, indicating a ["Method not
- /// found"](http://www.jsonrpc.org/specification#error_object) error.
- pub fn method_not_found(id: u64) -> ErrResponse {
- ErrResponse::new(id,
- ErrorCode {
- code: -32601,
- message: "Method not found".to_string()
- })
- }
-
- /// Returns a new `ErrResponse`, indicating a ["Parse
- /// error"](http://www.jsonrpc.org/specification#error_object) error.
- pub fn parse_error() -> ErrResponse {
- ErrResponse::new(0,
- ErrorCode {
- code: -32700,
- message: "Parse error".to_string()
- })
- }
-
- /// Returns a new `ErrResponse`, indicating a ["Invalid
- /// params"](http://www.jsonrpc.org/specification#error_object) error.
- pub fn invalid_params(id: u64) -> ErrResponse {
- ErrResponse::new(
- id,
- ErrorCode {
- code: -32602,
- message: "Invalid params".to_string()
- })
- }
-
- /// Returns a new `ErrResponse`, indicating a unspecified error.
- pub fn unspecified(id: u64) -> ErrResponse {
- ErrResponse::new(
- id,
- ErrorCode {
- code: -32100,
- message: "Couldn't handle request".to_string()
- })
- }
-}
-
-/// Type to encode a jsonrpc error.
-#[derive(RustcDecodable,RustcEncodable)]
-pub struct ErrorCode {
- /// The error code as [specified by
- /// jsonrpc](http://www.jsonrpc.org/specification#error_object).
- pub code: i32,
- /// The error message as [specified by
- /// jsonrpc](http://www.jsonrpc.org/specification#error_object).
- pub message: String
-}
diff --git a/src/remote/mod.rs b/src/remote/mod.rs
deleted file mode 100644
index 42a2c7b..0000000
--- a/src/remote/mod.rs
+++ /dev/null
@@ -1,5 +0,0 @@
-mod dw;
-mod jsonrpc;
-mod parm;
-pub mod rvi;
-pub mod svc;
diff --git a/src/remote/parm.rs b/src/remote/parm.rs
deleted file mode 100644
index 5c2db8b..0000000
--- a/src/remote/parm.rs
+++ /dev/null
@@ -1,189 +0,0 @@
-use event::UpdateId;
-use event::inbound::{InboundEvent, UpdateAvailable, GetInstalledSoftware, DownloadComplete};
-
-use super::dw::Transfers;
-use super::svc::{BackendServices, RemoteServices};
-
-use std::result;
-use std::sync::Mutex;
-
-#[derive(Debug)]
-pub enum Error {
- UnknownPackage,
- IoFailure,
- SendFailure
-}
-pub type Result = result::Result<Option<InboundEvent>, Error>;
-
-/// Trait that every message handler needs to implement.
-pub trait ParamHandler {
- /// Handle the message.
- ///
- /// Return a [`Event`](../message/enum.Event.html) to be passed to the
- /// [`main_loop`](../main_loop/index.html) if apropriate.
- fn handle(&self,
- services: &Mutex<RemoteServices>,
- transfers: &Mutex<Transfers>)
- -> Result;
-}
-
-
-/// Type for "Notify" messages.
-#[derive(RustcDecodable, Clone)]
-pub struct NotifyParams {
- /// A `Vector` of packages, that are available for download.
- pub update_available: UpdateAvailable,
- /// The service URLs, that the SOTA server supports.
- pub services: BackendServices,
-}
-
-impl ParamHandler for NotifyParams {
- fn handle(&self,
- services: &Mutex<RemoteServices>,
- _: &Mutex<Transfers>) -> Result {
- let mut services = services.lock().unwrap();
- services.set(self.services.clone());
-
- Ok(Some(InboundEvent::UpdateAvailable(self.update_available.clone())))
- }
-}
-
-
-/// Type for "Start Transfer" messages.
-#[derive(RustcDecodable)]
-pub struct StartParams {
- pub update_id: UpdateId,
- /// The amount of chunks this `Transfer` will have.
- pub chunkscount: u64,
- /// The SHA1 checksum of the assembled package.
- pub checksum: String
-}
-
-impl ParamHandler for StartParams {
- fn handle(&self,
- services: &Mutex<RemoteServices>,
- transfers: &Mutex<Transfers>) -> Result {
- let services = services.lock().unwrap();
- let mut transfers = transfers.lock().unwrap();
-
- info!("Starting transfer for update_id {}", self.update_id);
-
- transfers.push(self.update_id.clone(), self.checksum.clone());
- services.send_chunk_received(
- ChunkReceived {
- update_id: self.update_id.clone(),
- chunks: Vec::new(),
- vin: services.vin.clone() })
- .map_err(|e| {
- error!("Error on sending start ACK: {}", e);
- Error::SendFailure })
- .map(|_| None)
- }
-}
-
-/// Encodes the "Chunk Received" message, indicating that a chunk was successfully transferred.
-#[derive(RustcEncodable)]
-pub struct ChunkReceived {
- /// The transfer to which the transferred chunk belongs.
- pub update_id: UpdateId,
- /// A list of the successfully transferred chunks.
- pub chunks: Vec<u64>,
- /// The VIN of this device.
- pub vin: String
-}
-
-
-/// Type for messages transferring single chunks.
-#[derive(RustcDecodable)]
-pub struct ChunkParams {
- /// The package transfer this chunk belongs to.
- pub update_id: UpdateId,
- /// The data of the transferred chunk.
- pub bytes: String,
- /// The index of this chunk.
- pub index: u64
-}
-
-impl ParamHandler for ChunkParams {
- fn handle(&self,
- services: &Mutex<RemoteServices>,
- transfers: &Mutex<Transfers>) -> Result {
- let services = services.lock().unwrap();
- let mut transfers = transfers.lock().unwrap();
- transfers.get_mut(&self.update_id).map(|t| {
- if t.write_chunk(&self.bytes, self.index) {
- info!("Wrote chunk {} for package {}", self.index, self.update_id);
- services.send_chunk_received(
- ChunkReceived {
- update_id: self.update_id.clone(),
- chunks: t.transferred_chunks.clone(),
- vin: services.vin.clone() })
- .map_err(|e| {
- error!("Error on sending ChunkReceived: {}", e);
- Error::SendFailure })
- .map(|_| None)
- } else {
- Err(Error::IoFailure)
- }
- }).unwrap_or_else(|| {
- error!("Couldn't find transfer for update_id {}", self.update_id);
- Err(Error::UnknownPackage)
- })
- }
-}
-
-
-/// Type for "Finish Transfer" messages.
-#[derive(RustcDecodable)]
-pub struct FinishParams {
- /// The package transfer to finalize.
- pub update_id: UpdateId,
- pub signature: String
-}
-
-impl ParamHandler for FinishParams {
- fn handle(&self,
- _: &Mutex<RemoteServices>,
- transfers: &Mutex<Transfers>) -> Result {
- let mut transfers = transfers.lock().unwrap();
- transfers.get(&self.update_id).ok_or(Error::UnknownPackage)
- .and_then(|t| {
- t.assemble_package().map_err(|_| Error::IoFailure) })
- .and_then(|p| {
- p.into_os_string().into_string().map_err(|_| Error::IoFailure) })
- .map(|p| {
- transfers.remove(&self.update_id);
- info!("Finished transfer of {}", self.update_id);
- Some(InboundEvent::DownloadComplete(DownloadComplete {
- update_id: self.update_id.clone(),
- update_image: p,
- signature: self.signature.clone() })) })
- }
-}
-
-
-/// Type for "Abort Transfer" messages.
-#[derive(RustcDecodable)]
-pub struct AbortParams;
-
-impl ParamHandler for AbortParams {
- fn handle(&self,
- _: &Mutex<RemoteServices>,
- transfers: &Mutex<Transfers>) -> Result {
- let mut transfers = transfers.lock().unwrap();
- transfers.clear();
- Ok(None)
- }
-}
-
-
-/// Type for "Get All Packages" messages.
-pub type ReportParams = GetInstalledSoftware;
-
-impl ParamHandler for ReportParams {
- fn handle(&self,
- _: &Mutex<RemoteServices>,
- _: &Mutex<Transfers>) -> Result {
- Ok(Some(InboundEvent::GetInstalledSoftware(self.clone())))
- }
-}
diff --git a/src/remote/rvi/edge.rs b/src/remote/rvi/edge.rs
deleted file mode 100644
index 6eec01f..0000000
--- a/src/remote/rvi/edge.rs
+++ /dev/null
@@ -1,162 +0,0 @@
-//! Implements the RVI facing webservice.
-
-use std::io::{Read, Write};
-use std::thread;
-use hyper::Server;
-use hyper::server::{Handler, Request, Response};
-use rustc_serialize::json;
-use rustc_serialize::json::Json;
-
-use remote::jsonrpc;
-use remote::jsonrpc::{OkResponse, ErrResponse};
-
-use remote::rvi::send;
-use remote::rvi::message::{RegisterServiceRequest, RegisterServiceResponse};
-
-pub trait ServiceHandler: Sync + Send {
- fn handle_service(&self, id: u64, service: &str, message: &str)
- -> Result<OkResponse<i32>, ErrResponse>;
- fn register_services<F: Fn(&str) -> String>(&self, reg: F);
-}
-
-/// Encodes the service edge of the webservice.
-pub struct ServiceEdge<H: ServiceHandler + 'static> {
- /// The full URL where RVI can be reached.
- rvi_url: String,
- /// The `host:port` to bind and listen for incoming RVI messages.
- edge_url: String,
- hdlr: H
-}
-
-impl<H: ServiceHandler + 'static> ServiceEdge<H> {
- /// Create a new service edge.
- ///
- /// # Arguments
- /// * `r`: The full URL where RVI can be reached.
- /// * `e`: The `host:port` combination where the edge should bind.
- /// * `s`: A sender to communicate back the service URLs.
- pub fn new(r: String, e: String, h: H) -> ServiceEdge<H> {
- ServiceEdge {
- rvi_url: r,
- edge_url: e,
- hdlr: h
- }
- }
-
- /// Register a service. Returns the full service URL as provided by RVI. Panics if the
- /// registration in RVI failed. This can be handled by starting the RVI edge in a separate
- /// thread.
- ///
- /// # Arguments
- /// * `s`: The service to register. Will get prepended with the device identifier by RVI.
- pub fn register_service(&self, s: &str) -> String {
- let json_rpc = jsonrpc::Request::new(
- "register_service",
- RegisterServiceRequest {
- network_address: self.edge_url.to_string(),
- service: s.to_string()
- });
-
- let resp = send(&self.rvi_url, &json_rpc)
- .map_err(|e| error!("Couldn't send registration to RVI\n{}", e))
- .and_then(|r| json::decode::<jsonrpc::OkResponse<RegisterServiceResponse>>(&r)
- .map_err(|e| error!("Couldn't parse response when registering in RVI\n{}", e)))
- .unwrap();
-
- resp.result
- .expect("Didn't get full service name when registering")
- .service
- }
-
- /// Starts the service edge.
- ///
- /// It binds on the provided `host:port` combination, registers all services and then waits for
- /// incoming RVI messages. On incoming messages it forks another thread and passes the message
- /// to the provided `Handler`. For details about how to implement a `Handler` see the
- /// [`hyper`](../../hyper/index.html) documentation and the [reference
- /// implementation](../handler/index.html).
- ///
- /// Panics if it can't reach or register in RVI.
- ///
- /// # Arguments
- /// * `h`: The `Handler` all messages are passed to.
- /// * `s`: A `Vector` of service strings to register in RVI.
- pub fn start(self) {
- let url = self.edge_url.clone();
- self.hdlr.register_services(|s| self.register_service(s));
- thread::spawn(move || {
- Server::http(&*url).and_then(|srv| {
- info!("Ready to accept connections.");
- srv.handle(self) })
- .map_err(|e| error!("Couldn't start server\n{}", e))
- .unwrap()
- });
- }
-
- /// Try to parse the type of a message and forward it to the appropriate message handler.
- /// Returns the result of the message handling or a `jsonrpc` result indicating a parser error.
- ///
- /// Needs to be extended to support new services.
- ///
- /// # Arguments
- /// * `message`: The message that will be parsed.
- fn handle_message(&self, message: &str)
- -> Result<OkResponse<i32>, ErrResponse> {
-
- let data = try!(
- Json::from_str(message)
- .map_err(|_| ErrResponse::parse_error()));
- let obj = try!(
- data.as_object().ok_or(ErrResponse::parse_error()));
- let rpc_id = try!(
- obj.get("id").and_then(|x| x.as_u64())
- .ok_or(ErrResponse::parse_error()));
-
- let method = try!(
- obj.get("method").and_then(|x| x.as_string())
- .ok_or(ErrResponse::invalid_request(rpc_id)));
-
- if method == "services_available" {
- Ok(OkResponse::new(rpc_id, None))
- }
- else if method != "message" {
- Err(ErrResponse::method_not_found(rpc_id))
- } else {
- let service = try!(obj.get("params")
- .and_then(|x| x.as_object())
- .and_then(|x| x.get("service_name"))
- .and_then(|x| x.as_string())
- .ok_or(ErrResponse::invalid_request(rpc_id)));
-
- self.hdlr.handle_service(rpc_id, service, message)
- }
- }
-}
-
-impl<H: ServiceHandler + 'static> Handler for ServiceEdge<H> {
- fn handle(&self, mut req: Request, resp: Response) {
- let mut rbody = String::new();
- try_or!(req.read_to_string(&mut rbody), return);
- debug!(">>> Received Message: {}", rbody);
- let mut resp = try_or!(resp.start(), return);
-
- macro_rules! send_response {
- ($rtype:ty, $resp:ident) => {
- match json::encode::<$rtype>(&$resp) {
- Ok(decoded_msg) => {
- try_or!(resp.write_all(decoded_msg.as_bytes()), return);
- debug!("<<< Sent Response: {}", decoded_msg);
- },
- Err(p) => { error!("{}", p); }
- }
- };
- }
-
- match self.handle_message(&rbody) {
- Ok(msg) => send_response!(OkResponse<i32>, msg),
- Err(msg) => send_response!(ErrResponse, msg)
- }
-
- try_or!(resp.end(), return);
- }
-}
diff --git a/src/remote/rvi/message.rs b/src/remote/rvi/message.rs
deleted file mode 100644
index 8adf596..0000000
--- a/src/remote/rvi/message.rs
+++ /dev/null
@@ -1,61 +0,0 @@
-//! Wrappers for messages exchanged with RVI.
-
-use std::vec::Vec;
-use time;
-
-/// A generic incoming message.
-#[derive(RustcDecodable, RustcEncodable)]
-pub struct Message<T> {
- /// The service that got called.
- pub service_name: String,
- /// The paramaters to the service call.
- pub parameters: Vec<T>
-}
-
-/// A generic outgoing message.
-#[derive(RustcDecodable, RustcEncodable)]
-pub struct RVIMessage<T> {
- /// The service name to call.
- pub service_name: String,
- /// A timestamp when this message should expire. In UTC UNIX epoch.
- pub timeout: i64,
- /// The parameters to the service call.
- pub parameters: Vec<T>
-}
-
-impl<T> RVIMessage<T> {
- /// Create a new outgoing RVI message.
- ///
- /// # Arguments
- /// * `service`: The service name to call.
- /// * `parameters`: The parameters to the service call.
- /// * `tdelta`: Amount of seconds before the message will expire.
- pub fn new(service: &str,
- parameters: Vec<T>,
- tdelta: i64) -> RVIMessage<T> {
- let timeout = time::Duration::seconds(tdelta);
- RVIMessage {
- timeout: (time::get_time() + timeout).sec,
- service_name: service.to_string(),
- parameters: parameters
- }
- }
-}
-
-/// Encodes a registration request.
-#[derive(RustcEncodable)]
-pub struct RegisterServiceRequest {
- /// The network address where RVI can be reached.
- pub network_address: String,
- /// The service (short name) to register.
- pub service: String
-}
-
-/// Encodes a registration response.
-#[derive(RustcDecodable)]
-pub struct RegisterServiceResponse {
- /// Status number indicating success or failure. See the RVI documentation for details.
- pub status: i32,
- /// The full service URL, that RVI assigned.
- pub service: String
-}
diff --git a/src/remote/rvi/mod.rs b/src/remote/rvi/mod.rs
deleted file mode 100644
index a6791bc..0000000
--- a/src/remote/rvi/mod.rs
+++ /dev/null
@@ -1,18 +0,0 @@
-//! RVI bindings for Rust.
-//!
-//! RVI - Remote Vehicle Interaction - is the next generation of connected vehicle services. Based
-//! on the discussions inside and outside the Automotive Grade Linux expert group.
-//!
-//! This module implements Rust bindings to simplify the interaction with it.
-//!
-//! It is intended to be split out into a separate crate at some point in the future.
-
-mod edge;
-mod send;
-mod message;
-
-// Export public interface
-pub use super::rvi::edge::{ServiceEdge, ServiceHandler};
-pub use super::rvi::send::send;
-pub use super::rvi::send::send_message;
-pub use super::rvi::message::Message;
diff --git a/src/remote/rvi/send.rs b/src/remote/rvi/send.rs
deleted file mode 100644
index 8308203..0000000
--- a/src/remote/rvi/send.rs
+++ /dev/null
@@ -1,61 +0,0 @@
-//! Helper functions for sending messages to RVI.
-
-use std::io::Read;
-use hyper::Client;
-use rustc_serialize::{json, Encodable};
-
-use remote::jsonrpc;
-use remote::rvi::message::RVIMessage;
-
-/// Send a object to RVI. Either returns the full response from RVI or a error message.
-///
-/// The object will get encoded to json. Apart from that no sanity checks are made. You usually
-/// don't need this function.
-///
-/// # Arguments
-/// * `url`: The full URL where RVI can be reached.
-/// * `b`: The object to encode and send to RVI.
-pub fn send<E: Encodable>(url: &str, b: &E) -> Result<String, String> {
- let client = Client::new();
-
- let mut resp = try!(json::encode(b)
- .map_err(|e| format!("{}", e))
- .and_then(|j| {
- debug!("<<< Sent Message: {}", j);
- client.post(url).body(&j).send()
- .map_err(|e| format!("{}", e))
- }));
-
- let mut rbody = String::new();
- try!(resp.read_to_string(&mut rbody)
- .map_err(|e| format!("{}", e)));
- debug!(">>> Received Response: {}", rbody);
- Ok(rbody)
-}
-
-/// Prepare a message and send it to RVI. Returns the full response from RVI on success or a error
-/// message on failure.
-///
-/// This wraps the provided object into a proper RVI message and encodes it to json. You usually
-/// should call this function.
-///
-/// **NOTE:** This currently implements a workaround for RVI, that will get fixed in the upcoming
-/// RVI version `0.5.0`, which will break this current implementation. For the new protocol you
-/// don't have to wrap the `params` in a one element `Vector` any more.
-///
-/// # Arguments
-/// * `url`: The full URL where RVI can be reached.
-/// * `b`: The object to wrap into a RVI Message, encode and send to RVI.
-/// * `addr`: The full RVI address (service URL) where this message should be sent to.
-#[cfg(not(test))]
-pub fn send_message<E: Encodable>(url: &str, b: E, addr: &str) -> Result<String, String> {
- let mut params = Vec::new();
- params.push(b);
- let message = RVIMessage::<E>::new(addr, params, 90);
- let json_rpc = jsonrpc::Request::new("message", message);
- send(url, &json_rpc)
-}
-#[cfg(test)]
-pub fn send_message<E: Encodable>(url: &str, _: E, addr: &str) -> Result<String, String> {
- Ok(format!("Faked sending to RVI: {}, {}", url, addr))
-}
diff --git a/src/remote/svc.rs b/src/remote/svc.rs
deleted file mode 100644
index 873cb80..0000000
--- a/src/remote/svc.rs
+++ /dev/null
@@ -1,272 +0,0 @@
-//! The main service handler
-//!
-//! Parses incoming messages and delegates them to the appropriate individual message handlers,
-//! passing on the results to the [`main_loop`](../main_loop/index.html)
-
-use std::ops::Deref;
-use std::sync::{Arc, Mutex};
-use std::sync::mpsc::Sender;
-use std::thread;
-use std::thread::sleep_ms;
-
-use rustc_serialize::{json, Decodable};
-use time;
-
-use event::{Event, UpdateId};
-use event::inbound::InboundEvent;
-use event::outbound::{UpdateReport, InstalledSoftware};
-
-use super::parm::{NotifyParams, StartParams, ChunkParams, ChunkReceived, FinishParams};
-use super::parm::{ReportParams, AbortParams, ParamHandler};
-use super::dw::Transfers;
-
-use super::jsonrpc;
-use super::jsonrpc::{OkResponse, ErrResponse};
-use super::rvi;
-
-use configuration::ClientConfiguration;
-
-/// Encodes the list of service URLs the client registered.
-///
-/// Needs to be extended to introduce new services.
-#[derive(RustcEncodable, Clone)]
-pub struct LocalServices {
- /// "Start Download" URL.
- pub start: String,
- /// "Chunk" URL.
- pub chunk: String,
- /// "Abort Download" URL.
- pub abort: String,
- /// "Finish Download" URL.
- pub finish: String,
- /// "Get All Packages" URL.
- pub getpackages: String,
-}
-
-impl LocalServices {
- /// Returns the VIN of this device.
- ///
- /// # Arguments
- /// * `vin_match`: The index, where to look for the VIN in the service URL.
- pub fn get_vin(&self, vin_match: i32) -> String {
- self.start.split("/").nth(vin_match as usize).unwrap().to_string()
- }
-}
-
-/// Encodes the service URLs, that the server provides.
-#[derive(RustcDecodable, Clone)]
-pub struct BackendServices {
- /// URL for the "Start Download" call.
- pub start: String,
- /// URL for the "Chunk Received" call.
- pub ack: String,
- /// URL for the "Installation Report" call.
- pub report: String,
- /// URL for the "Get All Packages" call.
- pub packages: String
-}
-
-#[derive(RustcEncodable, Clone)]
-struct StartDownload {
- vin: String,
- update_id: UpdateId,
- services: LocalServices,
-}
-
-#[derive(RustcEncodable, Clone)]
-struct UpdateResult {
- vin: String,
- update_report: UpdateReport
-}
-
-#[derive(RustcEncodable, Clone)]
-struct InstalledSoftwareResult {
- vin: String,
- installed_software: InstalledSoftware
-}
-
-pub struct RemoteServices {
- pub vin: String,
- url: String,
- local_svcs: Option<LocalServices>,
- svcs: Option<BackendServices>
-}
-
-impl RemoteServices {
- pub fn new(url: String) -> RemoteServices {
- RemoteServices {
- vin: String::new(),
- url: url,
- local_svcs: None,
- svcs: None
- }
- }
-
- pub fn set_remote(&mut self, vin: String, svcs: LocalServices) {
- self.vin = vin;
- self.local_svcs = Some(svcs);
- }
-
- pub fn set(&mut self, svcs: BackendServices) {
- self.svcs = Some(svcs);
- }
-
- pub fn send_chunk_received(&self, m: ChunkReceived) -> Result<String, String> {
- self.svcs.iter().next().ok_or(format!("RemoteServices not set"))
- .and_then(|ref svcs| rvi::send_message(&self.url, m, &svcs.ack))
- }
-
- fn make_start_download(&self, id: UpdateId) -> StartDownload {
- StartDownload {
- vin: self.vin.clone(),
- services: self.local_svcs.iter().next().cloned().unwrap(),
- update_id: id
- }
- }
-
- pub fn send_start_download(&self, id: UpdateId) -> Result<String, String> {
- self.svcs.iter().next().ok_or(format!("RemoteServices not set"))
- .and_then(|ref svcs| rvi::send_message(
- &self.url,
- self.make_start_download(id),
- &svcs.start))
- }
-
- pub fn send_update_report(&self, m: UpdateReport) -> Result<String, String> {
- self.svcs.iter().next().ok_or(format!("RemoteServices not set"))
- .and_then(|ref svcs| rvi::send_message(
- &self.url,
- UpdateResult {
- vin: self.vin.clone(),
- update_report: m },
- &svcs.report))
- }
-
- pub fn send_installed_software(&self, m: InstalledSoftware) -> Result<String, String> {
- self.svcs.iter().next().ok_or(format!("RemoteServices not set"))
- .and_then(|ref svcs| rvi::send_message(
- &self.url,
- InstalledSoftwareResult {
- vin: self.vin.clone(),
- installed_software: m },
- &svcs.packages))
- }
-}
-
-
-/// Type that encodes a single service handler.
-///
-/// Holds the necessary state, like in-progress transfers, that are needed for handling incoming
-/// messages and sending replies to RVI. Needs to be thread safe as
-/// [`hyper`](../../../hyper/index.html) handles requests asynchronously.
-pub struct ServiceHandler {
- /// A `Sender` that connects the handlers with the `main_loop`.
- sender: Mutex<Sender<Event>>,
- /// The currently in-progress `Transfer`s.
- transfers: Arc<Mutex<Transfers>>,
- /// The service URLs that the SOTA server advertised.
- remote_services: Arc<Mutex<RemoteServices>>,
- /// The full `Configuration` of sota_client.
- conf: ClientConfiguration
-}
-
-impl ServiceHandler {
- /// Create a new `ServiceHandler`.
- ///
- /// # Arguments
- /// * `transfers`: A `Transfers` object to store the in-progress `Transfer`s.
- /// * `sender`: A `Sender` to call back into the `main_loop`.
- /// * `url`: The full URL, where RVI can be reached.
- /// * `c`: The full `Configuration` of sota_client.
- pub fn new(sender: Sender<Event>,
- r: Arc<Mutex<RemoteServices>>,
- c: ClientConfiguration) -> ServiceHandler {
- let transfers = Arc::new(Mutex::new(Transfers::new(c.storage_dir.clone())));
- let tc = transfers.clone();
- c.timeout
- .map(|t| {
- let _ = thread::spawn(move || ServiceHandler::start_timer(tc.deref(), t));
- info!("Transfers timeout after {}", t)})
- .unwrap_or(info!("No timeout configured, transfers will never time out."));
-
- ServiceHandler {
- sender: Mutex::new(sender),
- transfers: transfers,
- remote_services: r,
- conf: c
- }
- }
-
- /// Starts a infinite loop to expire timed out transfers. Checks once a second for timed out
- /// transfers.
- ///
- /// # Arguments
- /// * `transfers`: Pointer to a `Transfers` object, that stores the transfers to be checked for
- /// expired timeouts.
- /// * `timeout`: The timeout in seconds.
- pub fn start_timer(transfers: &Mutex<Transfers>,
- timeout: i64) {
- loop {
- sleep_ms(1000);
- let mut transfers = transfers.lock().unwrap();
- transfers.prune(time::get_time().sec, timeout);
- }
- }
-
- /// Helper function to send a `Event` to the `main_loop`.
- ///
- /// # Arguments
- /// * `e`: `Event` to send.
- fn push_notify(&self, e: InboundEvent) {
- try_or!(self.sender.lock().unwrap().send(Event::Inbound(e)), return);
- }
-
- /// Create a message handler `D`, and let it process the `message`. If it returns a
- /// Event, forward it to the `main_loop`. Returns a `jsonrpc` response indicating
- /// success or failure.
- ///
- /// # Arguments
- /// * `message`: The message, that should be handled.
- fn handle_message_params<D>(&self, id: u64, message: &str)
- -> Result<OkResponse<i32>, ErrResponse>
- where D: Decodable + ParamHandler {
- json::decode::<jsonrpc::Request<rvi::Message<D>>>(&message)
- .map_err(|_| ErrResponse::invalid_params(id))
- .and_then(|p| {
- let handler = &p.params.parameters[0];
- handler.handle(&self.remote_services, &self.transfers)
- .map_err(|_| ErrResponse::unspecified(p.id))
- .map(|r| {
- r.map(|m| self.push_notify(m));
- OkResponse::new(p.id, None) })
- })
- }
-}
-
-impl rvi::ServiceHandler for ServiceHandler {
- fn handle_service(&self, id: u64, service: &str, message: &str)
- -> Result<OkResponse<i32>, ErrResponse> {
- match service {
- "/sota/notify" => self.handle_message_params::<NotifyParams>(id, message),
- "/sota/start" => self.handle_message_params::<StartParams>(id, message),
- "/sota/chunk" => self.handle_message_params::<ChunkParams>(id, message),
- "/sota/finish" => self.handle_message_params::<FinishParams>(id, message),
- "/sota/abort" => self.handle_message_params::<AbortParams>(id, message),
- "/sota/getpackages" => self.handle_message_params::<ReportParams>(id, message),
- _ => Err(ErrResponse::invalid_request(id))
- }
- }
-
- fn register_services<F: Fn(&str) -> String>(&self, reg: F) {
- reg("/sota/notify");
- let mut remote_svcs = self.remote_services.lock().unwrap();
- let svcs = LocalServices {
- start: reg("/sota/start"),
- chunk: reg("/sota/chunk"),
- abort: reg("/sota/abort"),
- finish: reg("/sota/finish"),
- getpackages: reg("/sota/getpackages")
- };
- remote_svcs.set_remote(svcs.get_vin(self.conf.vin_match), svcs);
- }
-}
diff --git a/src/rvi/edge.rs b/src/rvi/edge.rs
new file mode 100644
index 0000000..cadea74
--- /dev/null
+++ b/src/rvi/edge.rs
@@ -0,0 +1,127 @@
+use hyper::StatusCode;
+use hyper::net::{HttpStream, Transport};
+use hyper::server::{Server as HyperServer, Request as HyperRequest};
+use rustc_serialize::json;
+use rustc_serialize::json::Json;
+use std::{mem, str};
+use std::net::ToSocketAddrs;
+
+use datatype::{RpcRequest, RpcOk, RpcErr, Url};
+use http::{Server, ServerHandler};
+use super::services::Services;
+
+
+/// The HTTP server endpoint for `RVI` client communication.
+pub struct Edge {
+ rvi_edge: Url,
+ services: Services,
+}
+
+impl Edge {
+ /// Create a new `Edge` by registering each `RVI` service.
+ pub fn new(mut services: Services, rvi_edge: String, rvi_client: Url) -> Self {
+ services.register_services(|service| {
+ let req = RpcRequest::new("register_service", RegisterServiceRequest {
+ network_address: rvi_edge.clone(),
+ service: service.to_string(),
+ });
+ let resp = req.send(rvi_client.clone())
+ .unwrap_or_else(|err| panic!("RegisterServiceRequest failed: {}", err));
+ let rpc_ok = json::decode::<RpcOk<RegisterServiceResponse>>(&resp)
+ .unwrap_or_else(|err| panic!("couldn't decode RegisterServiceResponse: {}", err));
+ rpc_ok.result.expect("expected rpc_ok result").service
+ });
+
+ Edge { rvi_edge: rvi_edge.parse().expect("couldn't parse edge server as url"), services: services }
+ }
+
+ /// Start the HTTP server listening for incoming RVI client connections.
+ pub fn start(&mut self) {
+ let mut addrs = self.rvi_edge.to_socket_addrs()
+ .unwrap_or_else(|err| panic!("couldn't parse edge url: {}", err));
+ let server = HyperServer::http(&addrs.next().expect("no SocketAddr found"))
+ .unwrap_or_else(|err| panic!("couldn't start rvi edge server: {}", err));
+ let (addr, server) = server.handle(move |_| EdgeHandler::new(self.services.clone())).unwrap();
+ info!("RVI server edge listening at http://{}.", addr);
+ server.run();
+ }
+}
+
+
+#[derive(RustcEncodable)]
+struct RegisterServiceRequest {
+ pub network_address: String,
+ pub service: String,
+}
+
+#[derive(RustcDecodable)]
+struct RegisterServiceResponse {
+ pub service: String,
+ pub status: i32,
+}
+
+
+
+struct EdgeHandler {
+ services: Services,
+ resp_code: StatusCode,
+ resp_body: Option<Vec<u8>>
+}
+
+impl EdgeHandler {
+ fn new(services: Services) -> ServerHandler<HttpStream> {
+ ServerHandler::new(Box::new(EdgeHandler {
+ services: services,
+ resp_code: StatusCode::InternalServerError,
+ resp_body: None,
+ }))
+ }
+}
+
+impl<T: Transport> Server<T> for EdgeHandler {
+ fn headers(&mut self, _: HyperRequest<T>) {}
+
+ fn request(&mut self, body: Vec<u8>) {
+ let outcome = || -> Result<RpcOk<i32>, RpcErr> {
+ let text = try!(str::from_utf8(&body).map_err(|err| RpcErr::parse_error(err.to_string())));
+ let data = try!(Json::from_str(text).map_err(|err| RpcErr::parse_error(err.to_string())));
+ let object = try!(data.as_object().ok_or(RpcErr::parse_error("not an object".to_string())));
+ let id = try!(object.get("id").and_then(|x| x.as_u64())
+ .ok_or(RpcErr::parse_error("expected id".to_string())));
+ let method = try!(object.get("method").and_then(|x| x.as_string())
+ .ok_or(RpcErr::invalid_request(id, "expected method".to_string())));
+
+ match method {
+ "services_available" => Ok(RpcOk::new(id, None)),
+
+ "message" => {
+ let params = try!(object.get("params").and_then(|p| p.as_object())
+ .ok_or(RpcErr::invalid_request(id, "expected params".to_string())));
+ let service = try!(params.get("service_name").and_then(|s| s.as_string())
+ .ok_or(RpcErr::invalid_request(id, "expected params.service_name".to_string())));
+ self.services.handle_service(service, id, text)
+ }
+
+ _ => Err(RpcErr::method_not_found(id, format!("unknown method: {}", method)))
+ }
+ }();
+
+ match outcome {
+ Ok(msg) => {
+ let body = json::encode::<RpcOk<i32>>(&msg).expect("couldn't encode RpcOk response");
+ self.resp_code = StatusCode::Ok;
+ self.resp_body = Some(body.into_bytes());
+ }
+
+ Err(err) => {
+ let body = json::encode::<RpcErr>(&err).expect("couldn't encode RpcErr response");
+ self.resp_code = StatusCode::BadRequest;
+ self.resp_body = Some(body.into_bytes());
+ }
+ }
+ }
+
+ fn response(&mut self) -> (StatusCode, Option<Vec<u8>>) {
+ (self.resp_code, mem::replace(&mut self.resp_body, None))
+ }
+}
diff --git a/src/rvi/mod.rs b/src/rvi/mod.rs
new file mode 100644
index 0000000..ba1d40a
--- /dev/null
+++ b/src/rvi/mod.rs
@@ -0,0 +1,9 @@
+pub mod edge;
+pub mod parameters;
+pub mod services;
+pub mod transfers;
+
+pub use self::edge::Edge;
+pub use self::parameters::Parameter;
+pub use self::services::{RemoteServices, Services};
+pub use self::transfers::Transfer;
diff --git a/src/rvi/parameters.rs b/src/rvi/parameters.rs
new file mode 100644
index 0000000..1fa1a87
--- /dev/null
+++ b/src/rvi/parameters.rs
@@ -0,0 +1,136 @@
+use std::str;
+use std::sync::Mutex;
+
+use datatype::{ChunkReceived, Event, DownloadComplete, UpdateRequestId, UpdateAvailable};
+use super::services::{BackendServices, RemoteServices};
+use super::transfers::Transfers;
+
+
+/// Each `Parameter` implementation handles a specific kind of RVI client request,
+/// optionally responding with an `Event` on completion.
+pub trait Parameter {
+ fn handle(&self, remote: &Mutex<RemoteServices>, transfers: &Mutex<Transfers>)
+ -> Result<Option<Event>, String>;
+}
+
+
+#[derive(RustcDecodable, RustcEncodable)]
+pub struct Notify {
+ update_available: UpdateAvailable,
+ services: BackendServices
+}
+
+impl Parameter for Notify {
+ fn handle(&self, remote: &Mutex<RemoteServices>, _: &Mutex<Transfers>) -> Result<Option<Event>, String> {
+ remote.lock().unwrap().backend = Some(self.services.clone());
+ Ok(Some(Event::NewUpdateAvailable(self.update_available.clone())))
+ }
+}
+
+
+#[derive(RustcDecodable, RustcEncodable)]
+pub struct Start {
+ update_id: UpdateRequestId,
+ chunkscount: u64,
+ checksum: String
+}
+
+impl Parameter for Start {
+ fn handle(&self, remote: &Mutex<RemoteServices>, transfers: &Mutex<Transfers>) -> Result<Option<Event>, String> {
+ info!("Starting transfer for update_id {}", self.update_id);
+ let mut transfers = transfers.lock().unwrap();
+ transfers.push(self.update_id.clone(), self.checksum.clone());
+
+ let remote = remote.lock().unwrap();
+ let chunk = ChunkReceived {
+ device: remote.device_id.clone(),
+ update_id: self.update_id.clone(),
+ chunks: Vec::new()
+ };
+ remote.send_chunk_received(chunk)
+ .map(|_| None)
+ .map_err(|err| format!("error sending start ack: {}", err))
+ }
+}
+
+
+#[derive(RustcDecodable, RustcEncodable)]
+pub struct Chunk {
+ update_id: UpdateRequestId,
+ bytes: String,
+ index: u64
+}
+
+impl Parameter for Chunk {
+ fn handle(&self, remote: &Mutex<RemoteServices>, transfers: &Mutex<Transfers>) -> Result<Option<Event>, String> {
+ let remote = remote.lock().unwrap();
+
+ let mut transfers = transfers.lock().unwrap();
+ let transfer = try!(transfers.get_mut(self.update_id.clone())
+ .ok_or(format!("couldn't find transfer for update_id {}", self.update_id)));
+ transfer.write_chunk(&self.bytes, self.index)
+ .map_err(|err| format!("couldn't write chunk: {}", err))
+ .and_then(|_| {
+ trace!("wrote chunk {} for package {}", self.index, self.update_id);
+ let chunk = ChunkReceived {
+ device: remote.device_id.clone(),
+ update_id: self.update_id.clone(),
+ chunks: transfer.transferred_chunks.clone(),
+ };
+ remote.send_chunk_received(chunk)
+ .map(|_| None)
+ .map_err(|err| format!("error sending ChunkReceived: {}", err))
+ })
+ }
+}
+
+
+#[derive(RustcDecodable, RustcEncodable)]
+pub struct Finish {
+ update_id: UpdateRequestId,
+ signature: String
+}
+
+impl Parameter for Finish {
+ fn handle(&self, _: &Mutex<RemoteServices>, transfers: &Mutex<Transfers>) -> Result<Option<Event>, String> {
+ let mut transfers = transfers.lock().unwrap();
+ let image = {
+ let transfer = try!(transfers.get(self.update_id.clone())
+ .ok_or(format!("unknown package: {}", self.update_id)));
+ let package = try!(transfer.assemble_package()
+ .map_err(|err| format!("couldn't assemble package: {}", err)));
+ try!(package.into_os_string().into_string()
+ .map_err(|err| format!("couldn't get image: {:?}", err)))
+ };
+ transfers.remove(self.update_id.clone());
+ info!("Finished transfer of {}", self.update_id);
+
+ let complete = DownloadComplete {
+ update_id: self.update_id.clone(),
+ update_image: image,
+ signature: self.signature.clone()
+ };
+ Ok(Some(Event::DownloadComplete(complete)))
+ }
+}
+
+
+#[derive(RustcDecodable, RustcEncodable)]
+pub struct Report;
+
+impl Parameter for Report {
+ fn handle(&self, _: &Mutex<RemoteServices>, _: &Mutex<Transfers>) -> Result<Option<Event>, String> {
+ Ok(Some(Event::InstalledSoftwareNeeded))
+ }
+}
+
+
+#[derive(RustcDecodable, RustcEncodable)]
+pub struct Abort;
+
+impl Parameter for Abort {
+ fn handle(&self, _: &Mutex<RemoteServices>, transfers: &Mutex<Transfers>) -> Result<Option<Event>, String> {
+ transfers.lock().unwrap().clear();
+ Ok(None)
+ }
+}
diff --git a/src/rvi/services.rs b/src/rvi/services.rs
new file mode 100644
index 0000000..fd5a640
--- /dev/null
+++ b/src/rvi/services.rs
@@ -0,0 +1,185 @@
+use chan;
+use chan::Sender;
+use rustc_serialize::{json, Decodable, Encodable};
+use std::thread;
+use std::sync::{Arc, Mutex};
+use std::time::Duration;
+use time;
+
+use datatype::{ChunkReceived, DownloadStarted, Event, InstalledSoftware,
+ RpcRequest, RpcOk, RpcErr, RviConfig, UpdateReport, UpdateRequestId,
+ Url};
+use super::parameters::{Abort, Chunk, Finish, Notify, Parameter, Report, Start};
+use super::transfers::Transfers;
+
+
+/// Hold references to RVI service endpoints, currently active `Transfers`, and
+/// where to broadcast outcome `Event`s to.
+#[derive(Clone)]
+pub struct Services {
+ pub remote: Arc<Mutex<RemoteServices>>,
+ pub sender: Arc<Mutex<Sender<Event>>>,
+ pub transfers: Arc<Mutex<Transfers>>,
+}
+
+impl Services {
+ /// Set up a new RVI service handler, pruning any inactive `Transfer`s each second.
+ pub fn new(rvi_cfg: RviConfig, device_id: String, sender: Sender<Event>) -> Self {
+ let transfers = Arc::new(Mutex::new(Transfers::new(rvi_cfg.storage_dir)));
+ rvi_cfg.timeout.map_or_else(|| info!("Transfers will never time out."), |timeout| {
+ info!("Transfers timeout after {} seconds.", timeout);
+ let transfers = transfers.clone();
+ thread::spawn(move || {
+ let tick = chan::tick(Duration::from_secs(1));
+ loop {
+ let _ = tick.recv();
+ let mut transfers = transfers.lock().unwrap();
+ transfers.prune(time::get_time().sec, timeout);
+ }
+ });
+ });
+
+ Services {
+ remote: Arc::new(Mutex::new(RemoteServices::new(device_id, rvi_cfg.client))),
+ sender: Arc::new(Mutex::new(sender)),
+ transfers: transfers,
+ }
+ }
+
+ /// Register each RVI endpoint with the provided registration function which
+ /// should return a `String` representation of the URL used to contact that
+ /// service.
+ pub fn register_services<F: Fn(&str) -> String>(&mut self, register: F) {
+ let _ = register("/sota/notify");
+ let mut remote = self.remote.lock().unwrap();
+ remote.local = Some(LocalServices {
+ start: register("/sota/start"),
+ chunk: register("/sota/chunk"),
+ abort: register("/sota/abort"),
+ finish: register("/sota/finish"),
+ getpackages: register("/sota/getpackages")
+ });
+ }
+
+ /// Handle an incoming message for a specific service endpoint.
+ pub fn handle_service(&self, service: &str, id: u64, msg: &str) -> Result<RpcOk<i32>, RpcErr> {
+ match service {
+ "/sota/notify" => self.handle_message::<Notify>(id, msg),
+ "/sota/start" => self.handle_message::<Start>(id, msg),
+ "/sota/chunk" => self.handle_message::<Chunk>(id, msg),
+ "/sota/finish" => self.handle_message::<Finish>(id, msg),
+ "/sota/getpackages" => self.handle_message::<Report>(id, msg),
+ "/sota/abort" => self.handle_message::<Abort>(id, msg),
+ _ => Err(RpcErr::invalid_request(id, format!("unknown service: {}", service)))
+ }
+ }
+
+ /// Parse the message as an `RpcRequest<RviMessage<Parameter>>` then delegate
+ /// to the specific `Parameter.handle()` function, forwarding any returned
+ /// `Event` to the `Services` sender.
+ fn handle_message<P>(&self, id: u64, msg: &str) -> Result<RpcOk<i32>, RpcErr>
+ where P: Parameter + Encodable + Decodable
+ {
+ let request = try!(json::decode::<RpcRequest<RviMessage<P>>>(&msg).map_err(|err| {
+ error!("couldn't decode message: {}", err);
+ RpcErr::invalid_params(id, format!("couldn't decode message: {}", err))
+ }));
+ let event = try!(request.params.parameters[0].handle(&self.remote, &self.transfers).map_err(|err| {
+ error!("couldn't handle parameters: {}", err);
+ RpcErr::unspecified(request.id, format!("couldn't handle parameters: {}", err))
+ }));
+ event.map(|ev| self.sender.lock().unwrap().send(ev));
+ Ok(RpcOk::new(request.id, None))
+ }
+}
+
+
+pub struct RemoteServices {
+ pub device_id: String,
+ pub rvi_client: Url,
+ pub local: Option<LocalServices>,
+ pub backend: Option<BackendServices>
+}
+
+impl RemoteServices {
+ pub fn new(device_id: String, rvi_client: Url) -> RemoteServices {
+ RemoteServices { device_id: device_id, rvi_client: rvi_client, local: None, backend: None }
+ }
+
+ fn send_message<E: Encodable>(&self, body: E, addr: &str) -> Result<String, String> {
+ RpcRequest::new("message", RviMessage::new(addr, vec![body], 60)).send(self.rvi_client.clone())
+ }
+
+ pub fn send_download_started(&self, update_id: UpdateRequestId) -> Result<String, String> {
+ let backend = try!(self.backend.as_ref().ok_or("BackendServices not set"));
+ let local = try!(self.local.as_ref().ok_or("LocalServices not set"));
+ let start = DownloadStarted { device: self.device_id.clone(), update_id: update_id, services: local.clone() };
+ self.send_message(start, &backend.start)
+ }
+
+ pub fn send_chunk_received(&self, chunk: ChunkReceived) -> Result<String, String> {
+ let backend = try!(self.backend.as_ref().ok_or("BackendServices not set"));
+ self.send_message(chunk, &backend.ack)
+ }
+
+ pub fn send_update_report(&self, report: UpdateReport) -> Result<String, String> {
+ let backend = try!(self.backend.as_ref().ok_or("BackendServices not set"));
+ let result = UpdateReportResult { device: self.device_id.clone(), update_report: report };
+ self.send_message(result, &backend.report)
+ }
+
+ pub fn send_installed_software(&self, installed: InstalledSoftware) -> Result<String, String> {
+ let backend = try!(self.backend.as_ref().ok_or("BackendServices not set"));
+ let result = InstalledSoftwareResult { device_id: self.device_id.clone(), installed: installed };
+ self.send_message(result, &backend.packages)
+ }
+}
+
+
+#[derive(Clone, RustcDecodable, RustcEncodable)]
+pub struct LocalServices {
+ pub start: String,
+ pub abort: String,
+ pub chunk: String,
+ pub finish: String,
+ pub getpackages: String,
+}
+
+#[derive(Clone, RustcDecodable, RustcEncodable)]
+pub struct BackendServices {
+ pub start: String,
+ pub ack: String,
+ pub report: String,
+ pub packages: String
+}
+
+
+#[derive(RustcDecodable, RustcEncodable)]
+struct UpdateReportResult {
+ pub device: String,
+ pub update_report: UpdateReport
+}
+
+#[derive(RustcDecodable, RustcEncodable)]
+struct InstalledSoftwareResult {
+ device_id: String,
+ installed: InstalledSoftware
+}
+
+
+#[derive(RustcDecodable, RustcEncodable)]
+pub struct RviMessage<E: Encodable> {
+ pub service_name: String,
+ pub parameters: Vec<E>,
+ pub timeout: Option<i64>
+}
+
+impl<E: Encodable> RviMessage<E> {
+ pub fn new(service: &str, parameters: Vec<E>, expire_in: i64) -> RviMessage<E> {
+ RviMessage {
+ service_name: service.to_string(),
+ parameters: parameters,
+ timeout: Some((time::get_time() + time::Duration::seconds(expire_in)).sec)
+ }
+ }
+}
diff --git a/src/rvi/transfers.rs b/src/rvi/transfers.rs
new file mode 100644
index 0000000..859c2c1
--- /dev/null
+++ b/src/rvi/transfers.rs
@@ -0,0 +1,278 @@
+use crypto::digest::Digest;
+use crypto::sha1::Sha1;
+use rustc_serialize::base64::FromBase64;
+use std::fs;
+use std::collections::HashMap;
+use std::fs::File;
+use std::io::prelude::*;
+use std::path::PathBuf;
+use std::str::FromStr;
+use std::vec::Vec;
+use time;
+
+use datatype::UpdateRequestId;
+
+
+/// Holds all currently active transfers where each is referenced by `UpdateRequestId`.
+pub struct Transfers {
+ items: HashMap<UpdateRequestId, Transfer>,
+ storage_dir: String
+}
+
+impl Transfers {
+ pub fn new(storage_dir: String) -> Transfers {
+ Transfers { items: HashMap::new(), storage_dir: storage_dir }
+ }
+
+ pub fn get(&self, update_id: UpdateRequestId) -> Option<&Transfer> {
+ self.items.get(&update_id)
+ }
+
+ pub fn get_mut(&mut self, update_id: UpdateRequestId) -> Option<&mut Transfer> {
+ self.items.get_mut(&update_id)
+ }
+
+ pub fn push(&mut self, update_id: UpdateRequestId, checksum: String) {
+ let transfer = Transfer::new(self.storage_dir.to_string(), update_id.clone(), checksum);
+ self.items.insert(update_id, transfer);
+ }
+
+ pub fn remove(&mut self, update_id: UpdateRequestId) {
+ self.items.remove(&update_id);
+ }
+
+ pub fn clear(&mut self) {
+ self.items.clear();
+ }
+
+ pub fn prune(&mut self, now: i64, timeout: i64) {
+ let mut timeouts = Vec::new();
+ for (id, transfer) in &mut self.items {
+ if now - transfer.last_chunk_received > timeout {
+ timeouts.push(id.clone());
+ }
+ }
+
+ for id in timeouts {
+ self.items.remove(&id);
+ info!("Transfer for update_id {} timed out.", id)
+ }
+ }
+}
+
+
+/// Holds the details of the transferred chunks relating to an `UpdateRequestId`.
+pub struct Transfer {
+ pub update_id: UpdateRequestId,
+ pub checksum: String,
+ pub transferred_chunks: Vec<u64>,
+ pub storage_dir: String,
+ pub last_chunk_received: i64
+}
+
+impl Transfer {
+ /// Prepare for the transfer of a new package.
+ pub fn new(storage_dir: String, update_id: UpdateRequestId, checksum: String) -> Transfer {
+ Transfer {
+ update_id: update_id,
+ checksum: checksum,
+ transferred_chunks: Vec::new(),
+ storage_dir: storage_dir,
+ last_chunk_received: time::get_time().sec
+ }
+ }
+
+ /// Write the received chunk to disk and store metadata inside `Transfer`.
+ pub fn write_chunk(&mut self, data: &str, index: u64) -> Result<(), String> {
+ self.last_chunk_received = time::get_time().sec;
+ let mut path = try!(self.get_chunk_dir().map_err(|err| format!("couldn't get chunk dir: {}", err)));
+ path.push(index.to_string());
+ let mut file = try!(File::create(path).map_err(|err| format!("couldn't open chunk file: {}", err)));
+
+ let data = try!(data.from_base64().map_err(|err| format!("couldn't decode chunk {}: {}", index, err)));
+ try!(file.write_all(&data)
+ .map_err(|err| format!("couldn't write chunk {} for update_id {}: {}", index, self.update_id, err)));
+ try!(file.flush().map_err(|err| format!("couldn't flush file: {}", err)));
+
+ self.transferred_chunks.push(index);
+ self.transferred_chunks.sort();
+ self.transferred_chunks.dedup();
+ Ok(())
+ }
+
+ /// Assemble all received chunks into a complete package.
+ pub fn assemble_package(&self) -> Result<PathBuf, String> {
+ debug!("finalizing package {}", self.update_id);
+ try!(self.assemble_chunks());
+ self.verify()
+ .and_then(|_| self.get_package_path())
+ .map_err(|err| format!("couldn't assemble_package for update_id {}: {}", self.update_id, err))
+ }
+
+ fn assemble_chunks(&self) -> Result<(), String> {
+ let pkg_path = try!(self.get_package_path());
+ debug!("saving update_id {} to {}", self.update_id, pkg_path.display());
+ let mut file = try!(File::create(pkg_path).map_err(|err| format!("couldn't open package file: {}", err)));
+
+ let chunk_dir = try!(self.get_chunk_dir());
+ let entries = try!(fs::read_dir(chunk_dir.clone()).map_err(|err| format!("couldn't read dir: {}", err)));
+ let mut indices = Vec::new();
+ for entry in entries {
+ let entry = try!(entry.map_err(|err| format!("bad entry: {}", err)));
+ let name = try!(entry.file_name().into_string().map_err(|err| format!("bad entry name: {:?}", err)));
+ let index = try!(u64::from_str(&name).map_err(|err| format!("couldn't parse chunk index: {}", err)));
+ indices.push(index);
+ }
+ indices.sort();
+
+ for index in indices {
+ try!(self.append_chunk(&mut file, chunk_dir.clone(), index));
+ }
+ Ok(debug!("assembled chunks for update_id {}", self.update_id))
+ }
+
+ fn append_chunk(&self, file: &mut File, mut chunk_dir: PathBuf, index: u64) -> Result<(), String> {
+ chunk_dir.push(&index.to_string());
+ let mut chunk = try!(File::open(chunk_dir).map_err(|err| format!("couldn't open chunk: {}", err)));
+ let mut buf = Vec::new();
+ try!(chunk.read_to_end(&mut buf).map_err(|err| format!("couldn't read file {}: {}", index, err)));
+ try!(file.write(&buf).map_err(|err| format!("couldn't write chunk {}: {}", index, err)));
+ Ok(trace!("wrote chunk {} for update_id {}", index, self.update_id))
+ }
+
+ fn verify(&self) -> Result<(), String> {
+ let path = try!(self.get_package_path());
+ let mut file = try!(File::open(path).map_err(|err| format!("couldn't open package path: {}", err)));
+ let mut data = Vec::new();
+ try!(file.read_to_end(&mut data).map_err(|err| format!("couldn't read file: {}", err)));
+
+ let mut hash = Sha1::new();
+ hash.input(&data);
+ if hash.result_str() == self.checksum {
+ Ok(())
+ } else {
+ Err(format!("update_id {} checksum failed: expected {}, got {}", self.update_id, self.checksum, hash.result_str()))
+ }
+ }
+
+ fn get_chunk_dir(&self) -> Result<PathBuf, String> {
+ let mut path = PathBuf::from(&self.storage_dir);
+ path.push("downloads");
+ path.push(self.update_id.clone());
+ fs::create_dir_all(&path)
+ .map(|_| path)
+ .map_err(|err| format!("couldn't create chunk dir: {}", err))
+ }
+
+ fn get_package_path(&self) -> Result<PathBuf, String> {
+ let mut path = PathBuf::from(&self.storage_dir);
+ path.push("packages");
+ try!(fs::create_dir_all(&path)
+ .map_err(|err| format!("couldn't create package dir {:?}: {}", path, err)));
+ path.push(format!("{}.spkg", self.update_id));
+ Ok(path)
+ }
+}
+
+impl Drop for Transfer {
+ fn drop(&mut self) {
+ let _ = self.get_chunk_dir().map(|dir| {
+ fs::read_dir(&dir)
+ .or_else(|err| Err(error!("couldn't read dir {:?}: {}", &dir, err)))
+ .and_then(|entries| {
+ for entry in entries {
+ let _ = entry.map(|entry| fs::remove_file(entry.path()))
+ .map_err(|err| error!("found a malformed entry: {}", err));
+ }
+ Ok(fs::remove_dir(dir).map_err(|err| error!("couldn't remove dir: {}", err)))
+ })
+ });
+ }
+}
+
+
+#[cfg(test)]
+mod test {
+ use rand;
+ use rand::Rng;
+ use rustc_serialize::base64;
+ use rustc_serialize::base64::ToBase64;
+ use std::path::PathBuf;
+ use std::fs::File;
+ use std::io::prelude::*;
+ use time;
+
+ use super::*;
+ use package_manager::TestDir;
+
+
+ impl Transfer {
+ pub fn new_test(test_dir: &TestDir) -> Transfer {
+ Transfer {
+ update_id: rand::thread_rng().gen_ascii_chars().take(10).collect::<String>(),
+ checksum: "".to_string(),
+ transferred_chunks: Vec::new(),
+ storage_dir: test_dir.0.clone(),
+ last_chunk_received: time::get_time().sec
+ }
+ }
+
+ pub fn assert_chunk_written(&mut self, test_dir: &TestDir, index: u64, data: &[u8]) {
+ let encoded = data.to_base64(base64::Config {
+ char_set: base64::CharacterSet::UrlSafe,
+ newline: base64::Newline::LF,
+ pad: true,
+ line_length: None
+ });
+ self.write_chunk(encoded.as_ref(), index).expect("couldn't write chunk");
+
+ let path = PathBuf::from(format!("{}/downloads/{}/{}", test_dir.0.clone(), self.update_id, index));
+ let mut file = File::open(path).map_err(|err| panic!("couldn't open file: {}", err)).unwrap();
+ let mut buf = Vec::new();
+ let _ = file.read_to_end(&mut buf).expect("couldn't read file");
+ assert_eq!(data.to_vec(), buf);
+ }
+ }
+
+
+ #[test]
+ fn test_package_directory_created() {
+ let test_dir = TestDir::new("sota-test-transfers");
+ let transfer = Transfer::new_test(&test_dir);
+ let chunk_dir = transfer.get_package_path().unwrap();
+ let path = format!("{}/packages/{}.spkg", test_dir.0, transfer.update_id);
+ assert_eq!(chunk_dir.to_str().unwrap(), path);
+ }
+
+ #[test]
+ fn test_checksum() {
+ let test_dir = TestDir::new("sota-test-transfers");
+ let mut transfer = Transfer::new_test(&test_dir);
+ transfer.assert_chunk_written(&test_dir, 0, "test\n".to_string().as_bytes());
+ transfer.assemble_chunks().expect("couldn't assemble chunks");
+
+ transfer.checksum = "4e1243bd22c66e76c2ba9eddc1f91394e57f9f83".to_string();
+ assert!(transfer.verify().is_ok());
+
+ transfer.checksum = "invalid".to_string();
+ assert!(!transfer.verify().is_ok());
+ }
+
+ #[test]
+ fn test_assemble_chunks() {
+ let test_dir = TestDir::new("sota-test-transfers");
+ let mut transfer = Transfer::new_test(&test_dir);
+ let mut assembly = String::new();
+ for index in 1..20 {
+ let data = rand::thread_rng().gen_ascii_chars().take(index).collect::<String>();
+ assembly.push_str(&data);
+ transfer.assert_chunk_written(&test_dir, index as u64, data.as_bytes());
+ }
+
+ transfer.assemble_chunks().expect("couldn't assemble chunks");
+ let path = format!("{}/packages/{}.spkg", test_dir.0, transfer.update_id);
+ let mut buf = Vec::new();
+ let _ = File::open(PathBuf::from(path)).unwrap().read_to_end(&mut buf).unwrap();
+ assert_eq!(assembly.into_bytes(), buf);
+ }
+}
diff --git a/src/sota.rs b/src/sota.rs
new file mode 100644
index 0000000..3dec33d
--- /dev/null
+++ b/src/sota.rs
@@ -0,0 +1,141 @@
+use rustc_serialize::json;
+use std::fs::File;
+use std::io;
+use std::path::PathBuf;
+
+use datatype::{Config, DeviceReport, DownloadComplete, Error, Package,
+ PendingUpdateRequest, UpdateRequestId, UpdateReport, Url};
+use http::Client;
+
+
+/// Encapsulate the client configuration and HTTP client used for
+/// software-over-the-air updates.
+pub struct Sota<'c, 'h> {
+ config: &'c Config,
+ client: &'h Client,
+}
+
+impl<'c, 'h> Sota<'c, 'h> {
+ /// Creates a new instance for Sota communication.
+ pub fn new(config: &'c Config, client: &'h Client) -> Sota<'c, 'h> {
+ Sota { config: config, client: client }
+ }
+
+ /// Takes a path and returns a new endpoint of the format
+ /// `<Core server>/api/v1/device_updates/<uuid>/<path>`.
+ pub fn endpoint(&self, path: &str) -> Url {
+ let endpoint = if path.is_empty() {
+ format!("/api/v1/device_updates/{}", self.config.device.uuid)
+ } else {
+ format!("/api/v1/device_updates/{}/{}", self.config.device.uuid, path)
+ };
+ self.config.core.server.join(&endpoint).expect("couldn't build endpoint url")
+ }
+
+ /// Query the Core server to identify any new package updates available.
+ pub fn get_pending_updates(&mut self) -> Result<Vec<PendingUpdateRequest>, Error> {
+ let resp_rx = self.client.get(self.endpoint(""), None);
+ let resp = resp_rx.recv().expect("no get_package_updates response received");
+ let data = try!(resp);
+ let text = try!(String::from_utf8(data));
+ Ok(try!(json::decode::<Vec<PendingUpdateRequest>>(&text)))
+ }
+
+ /// Download a specific update from the Core server.
+ pub fn download_update(&mut self, id: UpdateRequestId) -> Result<DownloadComplete, Error> {
+ let resp_rx = self.client.get(self.endpoint(&format!("{}/download", id)), None);
+ let resp = resp_rx.recv().expect("no download_package_update response received");
+ let data = try!(resp);
+
+ let mut path = PathBuf::new();
+ path.push(&self.config.device.packages_dir);
+ path.push(id.clone()); // TODO: Use Content-Disposition filename from request?
+ let mut file = try!(File::create(path.as_path()));
+
+ let _ = io::copy(&mut &*data, &mut file);
+ let path = try!(path.to_str().ok_or(Error::Parse(format!("Path is not valid UTF-8: {:?}", path))));
+
+ Ok(DownloadComplete {
+ update_id: id,
+ update_image: path.to_string(),
+ signature: "".to_string()
+ })
+ }
+
+ /// Install an update using the package manager.
+ pub fn install_update(&mut self, download: DownloadComplete) -> Result<UpdateReport, UpdateReport> {
+ let ref pacman = self.config.device.package_manager;
+ pacman.install_package(&download.update_image).and_then(|(code, output)| {
+ Ok(UpdateReport::single(download.update_id.clone(), code, output))
+ }).or_else(|(code, output)| {
+ Err(UpdateReport::single(download.update_id.clone(), code, output))
+ })
+ }
+
+ /// Get a list of the currently installed packages from the package manager.
+ pub fn get_installed_packages(&mut self) -> Result<Vec<Package>, Error> {
+ Ok(try!(self.config.device.package_manager.installed_packages()))
+ }
+
+ /// Send a list of the currently installed packages to the Core server.
+ pub fn send_installed_packages(&mut self, packages: &Vec<Package>) -> Result<(), Error> {
+ let body = try!(json::encode(packages));
+ let resp_rx = self.client.put(self.endpoint("installed"), Some(body.into_bytes()));
+ let _ = resp_rx.recv().expect("no update_installed_packages response received")
+ .map_err(|err| error!("update_installed_packages failed: {}", err));
+ Ok(())
+ }
+
+ /// Send the outcome of a package update to the Core server.
+ pub fn send_update_report(&mut self, update_report: &UpdateReport) -> Result<(), Error> {
+ let report = DeviceReport::new(&self.config.device.uuid, update_report);
+ let body = try!(json::encode(&report));
+ let url = self.endpoint(report.device);
+ let resp_rx = self.client.post(url, Some(body.into_bytes()));
+ let resp = resp_rx.recv().expect("no send_install_report response received");
+ let _ = try!(resp);
+ Ok(())
+ }
+
+ /// Send system information from the device to the Core server.
+ pub fn send_system_info(&mut self, body: &str) -> Result<(), Error> {
+ let resp_rx = self.client.put(self.endpoint("system_info"), Some(body.as_bytes().to_vec()));
+ let resp = resp_rx.recv().expect("no send_system_info response received");
+ let _ = try!(resp);
+ Ok(())
+ }
+}
+
+
+#[cfg(test)]
+mod tests {
+ use rustc_serialize::json;
+
+ use super::*;
+ use datatype::{Config, Package, PendingUpdateRequest};
+ use http::TestClient;
+
+
+ #[test]
+ fn test_get_pending_updates() {
+ let pending_update = PendingUpdateRequest {
+ requestId: "someid".to_string(),
+ installPos: 0,
+ packageId: Package {
+ name: "fake-pkg".to_string(),
+ version: "0.1.1".to_string()
+ },
+ createdAt: "2010-01-01".to_string()
+ };
+
+ let json = format!("[{}]", json::encode(&pending_update).unwrap());
+ let mut sota = Sota {
+ config: &Config::default(),
+ client: &mut TestClient::from(vec![json.to_string()]),
+ };
+
+ let updates: Vec<PendingUpdateRequest> = sota.get_pending_updates().unwrap();
+ let ids: Vec<String> = updates.iter().map(|p| p.requestId.clone()).collect();
+ assert_eq!(ids, vec!["someid".to_string()])
+ }
+}
diff --git a/src/test_library.rs b/src/test_library.rs
deleted file mode 100644
index b5af298..0000000
--- a/src/test_library.rs
+++ /dev/null
@@ -1,72 +0,0 @@
-//! Helper functions for testing `sota_client`.
-
-use std::path::PathBuf;
-use std::fmt;
-use std::fs;
-
-use time;
-use log;
-use log::{LogRecord, LogLevel, LogMetadata};
-
-/// Initiates logging in tests. Can safely be called multiple times.
-macro_rules! test_init {
- () => {
- use test_library::SimpleLogger;
- use log::LogLevelFilter;
- use log;
- match log::set_logger(|max_log_level| {
- max_log_level.set(LogLevelFilter::Trace);
- Box::new(SimpleLogger)
- }) {
- Ok(..) => {},
- Err(..) => {}
- }
- }
-}
-
-/// Implements a simple logger printing all log messages to stdout.
-pub struct SimpleLogger;
-
-impl log::Log for SimpleLogger {
- fn enabled(&self, metadata: &LogMetadata) -> bool {
- metadata.level() <= LogLevel::Info
- }
-
- fn log(&self, record: &LogRecord) {
- if self.enabled(record.metadata()) {
- println!("{} - {}", record.level(), record.args());
- }
- }
-}
-
-/// Wrapper for storing test data in a temporary directory. The created directory will be deleted,
-/// when dropped.
-pub struct PathPrefix { prefix: String }
-
-impl PathPrefix {
- pub fn new() -> PathPrefix {
- PathPrefix {
- prefix: format!("/tmp/rust-test-{}",
- time::precise_time_ns()
- .to_string())
- }
- }
-
- pub fn to_string(&self) -> String {
- return self.prefix.clone();
- }
-}
-
-impl Drop for PathPrefix {
- fn drop(&mut self) {
- let dir = PathBuf::from(&self.prefix);
- fs::remove_dir_all(dir).unwrap();
- }
-}
-
-impl fmt::Display for PathPrefix {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(f, "{}", self.prefix)
- }
-}
-
diff --git a/tests/sota.toml b/tests/sota.toml
new file mode 100644
index 0000000..0801509
--- /dev/null
+++ b/tests/sota.toml
@@ -0,0 +1,45 @@
+[auth]
+server = "http://127.0.0.1:9001"
+client_id = "client-id"
+client_secret = "client-secret"
+credentials_file = "/tmp/sota_credentials.toml"
+
+[core]
+server = "http://127.0.0.1:8080"
+
+[dbus]
+name = "org.genivi.SotaClient"
+path = "/org/genivi/SotaClient"
+interface = "org.genivi.SotaClient"
+software_manager = "org.genivi.SoftwareLoadingManager"
+software_manager_path = "/org/genivi/SoftwareLoadingManager"
+timeout = 60
+
+[device]
+uuid = "123e4567-e89b-12d3-a456-426655440000"
+vin = "V1234567890123456"
+packages_dir = "/tmp/"
+package_manager = "deb"
+system_info = "system_info.sh"
+polling_interval = 10
+certificates_path = "/tmp/sota_certificates"
+
+[gateway]
+console = false
+dbus = false
+http = false
+rvi = false
+socket = false
+websocket = true
+
+[network]
+http_server = "http://127.0.0.1:8888"
+rvi_edge_server = "http://127.0.0.1:9080"
+socket_commands_path = "/tmp/sota-commands.socket"
+socket_events_path = "/tmp/sota-events.socket"
+websocket_server = "ws://127.0.0.1:3012"
+
+[rvi]
+client = "http://127.0.0.1:8901"
+storage_dir = "/var/sota"
+timeout = 20
diff --git a/tests/sota_client_tests.rs b/tests/sota_client_tests.rs
new file mode 100644
index 0000000..fcdbd0d
--- /dev/null
+++ b/tests/sota_client_tests.rs
@@ -0,0 +1,134 @@
+use std::env;
+use std::fs::File;
+use std::io::Write;
+use std::path::Path;
+use std::process::Command;
+
+
+fn run_client(args: &[&str]) -> String {
+ let output = Command::new(format!("{}/sota_client", bin_dir()))
+ .args(args)
+ .output()
+ .unwrap_or_else(|err| panic!("failed to execute child: {}", err));
+ String::from_utf8(output.stdout).unwrap()
+}
+
+fn run_client_with_config(filename: &str, args: &[&str], config: &str) -> String {
+ let mut file = File::create(filename.to_string()).expect("couldn't create test config file");
+ let _ = file.write_all(config.as_bytes()).unwrap();
+ let _ = file.flush().unwrap();
+ let arg = "--config=".to_string() + filename;
+ let mut args = args.to_vec();
+ args.push(&arg);
+ run_client(&args)
+}
+
+fn bin_dir() -> String {
+ let out_dir = env::var("OUT_DIR").unwrap();
+ let bin_dir = Path::new(&out_dir)
+ .parent().unwrap()
+ .parent().unwrap()
+ .parent().unwrap();
+ String::from(bin_dir.to_str().unwrap())
+}
+
+
+#[test]
+fn help() {
+ assert_eq!(run_client(&["-h"]),
+ format!(r#"Usage: {}/sota_client [options]
+
+Options:
+ -h, --help print this help menu
+ --config PATH change config path
+ --auth-server URL
+ change the auth server
+ --auth-client-id ID
+ change the auth client id
+ --auth-client-secret SECRET
+ change the auth client secret
+ --auth-credentials-file PATH
+ change the auth credentials file
+ --core-server URL
+ change the core server
+ --dbus-name NAME
+ change the dbus registration name
+ --dbus-path PATH
+ change the dbus path
+ --dbus-interface INTERFACE
+ change the dbus interface name
+ --dbus-software-manager NAME
+ change the dbus software manager name
+ --dbus-software-manager-path PATH
+ change the dbus software manager path
+ --dbus-timeout TIMEOUT
+ change the dbus installation timeout
+ --device-uuid UUID
+ change the device uuid
+ --device-vin VIN
+ change the device vin
+ --device-packages-dir PATH
+ change downloaded directory for packages
+ --device-package-manager MANAGER
+ change the package manager
+ --device-polling-interval INTERVAL
+ change the package polling interval
+ --device-certificates-path PATH
+ change the OpenSSL CA certificates file
+ --device-system-info PATH
+ change the system information command
+ --gateway-console BOOL
+ toggle the console gateway
+ --gateway-dbus BOOL
+ toggle the dbus gateway
+ --gateway-http BOOL
+ toggle the http gateway
+ --gateway-rvi BOOL
+ toggle the rvi gateway
+ --gateway-socket BOOL
+ toggle the unix domain socket gateway
+ --gateway-websocket BOOL
+ toggle the websocket gateway
+ --network-http-server ADDR
+ change the http server gateway address
+ --network-rvi-edge-server ADDR
+ change the rvi edge server gateway address
+ --network-socket-commands-path PATH
+ change the socket path for reading commands
+ --network-socket-events-path PATH
+ change the socket path for sending events
+ --network-websocket-server ADDR
+ change the websocket gateway address
+ --rvi-client URL
+ change the rvi client URL
+ --rvi-storage-dir PATH
+ change the rvi storage directory
+ --rvi-timeout TIMEOUT
+ change the rvi timeout
+
+"#, bin_dir()));
+}
+
+#[test]
+fn bad_ota_server_url() {
+ assert_eq!(run_client(&["--config", "tests/sota.toml", "--core-server", "bad-url"]),
+ "Invalid core-server URL: Url parse error: relative URL without a base\n")
+}
+
+#[test]
+fn bad_section() {
+ assert_eq!(run_client_with_config("/tmp/sota-test-config-1", &[""], "[foo]\n"),
+ "Parse error: invalid section: core\n")
+}
+
+#[test]
+fn bad_toml() {
+ assert_eq!(run_client_with_config("/tmp/sota-test-config-2", &[""], "auth]"),
+ "Toml parser errors: [ParserError { lo: 4, hi: 5, desc: \"expected `=`, but found `]`\" }]\n")
+}
+
+#[test]
+fn bad_path_dir() {
+ assert_eq!(run_client(&["--config=/"]),
+ "IO error: Is a directory (os error 21)\n")
+}