Introduce test infrastructure for QuicTransport (#22844)
This change introduces test infrastructure for QuicTransport.
See also: https://github.com/web-platform-tests/rfcs/blob/master/rfcs/quic.md
tools/quic contains the test server and files needed by the server such as
certificate files (TODO: we will switch to the same certificate used by
wptserve once aioquic 0.8.8 is released).
tools/quic/quic_transport_server.py is based on
https://github.com/aiortc/aioquic/blob/master/examples/http3_server.py
webtransport/quic contains a test example and a sample custom handler.
This change doesn't contain a means to run the QuicTransport server
automatically.
Tracking issue: #19114
diff --git a/tools/quic/README.md b/tools/quic/README.md
new file mode 100644
index 0000000..5a64b70
--- /dev/null
+++ b/tools/quic/README.md
@@ -0,0 +1,30 @@
+This directory contains
+[QUIC](https://tools.ietf.org/html/draft-ietf-quic-transport) related tools.
+
+# QuicTransport
+[quic_transport_server.py](./quic_transport_server.py) implements a simple
+[QuicTransport](https://tools.ietf.org/html/draft-vvv-webtransport-quic) server
+for testing. It uses [aioquic](https://github.com/aiortc/aioquic/), and test
+authors can implement custom handlers by putting python scripts in
+[wpt/webtransport/quic/handlers/](../../webtransport/quic/handlers/).
+
+## Custom Handlers
+The QuicTransportServer calls functions defined in each handler script.
+
+ - handle_client_indication is called during the client indication process.
+ This function is called with three arguments:
+
+ - connection: aioquic.asyncio.QuicConnectionProtocol
+ - origin: str The origin of the initiator.
+ - query: Dict[str, str] The dictionary of query parameters of the URL of the
+ connection.
+
+ A handler can abort the client indication process either by raising an
+ exception or closing the connection.
+
+ - handle_event is called when a QuicEvent arrives.
+ - connection: aioquic.asyncio.QuicConnectionProtocol
+ - event: aioquic.quic.events.QuicEvent
+
+ This function is not called until the client indication process finishes
+ successfully.
diff --git a/tools/quic/certs/README.md b/tools/quic/certs/README.md
new file mode 100644
index 0000000..216cf13
--- /dev/null
+++ b/tools/quic/certs/README.md
@@ -0,0 +1,12 @@
+To generate cert.key and cert.pem:
+
+ 1. Remove web-platform.test.key and web-platform.test.pem in ../../certs.
+ 1. From the root, run
+ `./wpt serve --config tools/quic/certs/config.json` and terminate it
+ after it has started up.
+ 1. Move tools/certs/web-platform.test.key to tools/quic/certs/cert.key.
+ 1. Move tools/certs/web-platform.test.pem to tools/quic/certs/cert.pem.
+ 1. Recover the original web-platform.test.key and web-platform.test.pem in
+ ../../certs.
+
+See also: ../../certs/README.md
\ No newline at end of file
diff --git a/tools/quic/certs/cert.key b/tools/quic/certs/cert.key
new file mode 100644
index 0000000..993cdfa
--- /dev/null
+++ b/tools/quic/certs/cert.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDbyPuiBGpxhavF
+3j7pI6g+A0gC4BTLTqMObKSTkQWsjq1GOd2LA1lwTPLObwrvhIUFzbwoIbOwoPMe
+MkjssFCHG3FMj56cKiAQ2DFI6dK5PjGUVNSRxk/F4Hh2Zx9DTENl/Eb/cRT2yuu+
+W9HCu/BWfbWwlwwN5vxyneCoh5cBB/jd1KTORguYpuatHb85AD5BRhYLXwHF7yVH
+NVxHeuGlK31yuYCHNKvBHDgZF5Tp8FqKXnVU+PlKXCSU5602c2U5xHIFvKTyiZlM
+cYXNSUp0lcNw/iTSVtsPE4k1stu6qkWdT7H/uU+GUB+aLqO+svA/s9GGX8HZgt+r
+vSFE8lIJAgMBAAECggEBAJA96D9djIoygxhaEompmCoStzkD3UHMuyClVqFuRP4J
+qVh0c5xfN1yHc7bdk5y8KR00966S574c81G3CLslv8Pb09C+VQcCcob7i+ThaCWg
+1qMVxWhicUpZVlXGufLN41HUbrgIfAy4Al2tHw4hj8sDt7FMgGHDXZzPVnjke8r1
+O9YiJl1Qx4L7vMWruGa9QWjFgHnG+uhaKjsL2v7JQOGy5t8aboVyb7h8rGg/mC+e
+HIYOucV1aEMgYVaAnhGsKMHkx5A1xWpXBSruG+GRBx/kXWZ+kCNckLXuVdrhq4HI
+AdbxIzqQTPMXpO3RAujyrxkHabENMPA/FGH4szmdLoECgYEA9z8pe7/vSlWgfhsF
+z5QnwWHyFjruhgD/2sa4LB/cmwTQdGw8E5TNHDbCgmS499DZUXIZuBOTekdEVDQa
+ng8VyL3o7Dms+5iPi5cqscp1KkjLEMyPpqs4JTuixRpjmMfycdxVTpXhcuqnJpTL
+QC9pR5N/zZcAMDlBv0Fzc8T78XkCgYEA45DueWGHVf2u4uMYyWxyZhaNDagl13yx
+/oSSUTzoLvSpGQxKkv+fxSNqL3nu5Ia6uD4Gu5NubP4Hr/VeSKRfmkT1luvFcVfC
+kn8r8bssZq855AVJxXa5K1auWjCuFHj0pYf56sfhkPxpY0RQEgkvuE3iosQ12gFX
+vw147FtQURECgYEA85RpVP45S31iOPp8Vg16wRyyeE4ksSYI6kr+JJJbLummSBxd
+b1kYXSRhqj56r8I0ZvXG+r9men/9hAs08eSgrHzUHO2RSuj4+ie6Kx/vH/JJBErT
+dvqVvLCs4gvmdRz+8EeGT35/dkxQ0kSinKBY0ugwb6XEzL2L1VUw3awCHdkCgYEA
+qtQIgOv6uU2ndEDAQax8MDCrkF3yklHUGFkSsZNERMN7EQeOD81+9XFBbARflgOh
+tV8ylKr3ETCdOrS6I1PpRJiRt8qjvBMCSBDZPyygBzFxBsAFggs+s87tMV0rwMiP
+9pcdv+ZuaPVic5c7eF6XCQbGpCMgvdeWNCB77woZP9ECgYEAlobkPGDYCy/RaViU
+Fbq5Go6w0pMVnLzYbn4Gh1AJPeQKISqXtJZ7tqpdW+i7qzkLw74ELaYCBR2ZElrj
+EVe5aROx6TFN9RnjkFnyv9LeyYL+YPc8AIwVUCeSPikSGLFpJfa/jwDmWh3vHmmA
+NRUP40wbtBi42C2udrTxUWsHxqc=
+-----END PRIVATE KEY-----
diff --git a/tools/quic/certs/cert.pem b/tools/quic/certs/cert.pem
new file mode 100644
index 0000000..7e70f1b
--- /dev/null
+++ b/tools/quic/certs/cert.pem
@@ -0,0 +1,240 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number: 919492 (0xe07c4)
+ Signature Algorithm: sha256WithRSAEncryption
+ Issuer: CN=web-platform-tests
+ Validity
+ Not Before: Apr 20 11:20:56 2020 GMT
+ Not After : Apr 18 11:20:56 2030 GMT
+ Subject: CN=web-platform.test
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ RSA Public-Key: (2048 bit)
+ Modulus:
+ 00:db:c8:fb:a2:04:6a:71:85:ab:c5:de:3e:e9:23:
+ a8:3e:03:48:02:e0:14:cb:4e:a3:0e:6c:a4:93:91:
+ 05:ac:8e:ad:46:39:dd:8b:03:59:70:4c:f2:ce:6f:
+ 0a:ef:84:85:05:cd:bc:28:21:b3:b0:a0:f3:1e:32:
+ 48:ec:b0:50:87:1b:71:4c:8f:9e:9c:2a:20:10:d8:
+ 31:48:e9:d2:b9:3e:31:94:54:d4:91:c6:4f:c5:e0:
+ 78:76:67:1f:43:4c:43:65:fc:46:ff:71:14:f6:ca:
+ eb:be:5b:d1:c2:bb:f0:56:7d:b5:b0:97:0c:0d:e6:
+ fc:72:9d:e0:a8:87:97:01:07:f8:dd:d4:a4:ce:46:
+ 0b:98:a6:e6:ad:1d:bf:39:00:3e:41:46:16:0b:5f:
+ 01:c5:ef:25:47:35:5c:47:7a:e1:a5:2b:7d:72:b9:
+ 80:87:34:ab:c1:1c:38:19:17:94:e9:f0:5a:8a:5e:
+ 75:54:f8:f9:4a:5c:24:94:e7:ad:36:73:65:39:c4:
+ 72:05:bc:a4:f2:89:99:4c:71:85:cd:49:4a:74:95:
+ c3:70:fe:24:d2:56:db:0f:13:89:35:b2:db:ba:aa:
+ 45:9d:4f:b1:ff:b9:4f:86:50:1f:9a:2e:a3:be:b2:
+ f0:3f:b3:d1:86:5f:c1:d9:82:df:ab:bd:21:44:f2:
+ 52:09
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Basic Constraints:
+ CA:FALSE
+ X509v3 Subject Key Identifier:
+ 79:96:E1:96:F3:7C:41:2B:3B:9F:1B:0D:D6:A9:B8:B7:4F:E6:61:5C
+ X509v3 Authority Key Identifier:
+ keyid:F6:B9:DA:16:07:05:BB:6F:C9:63:30:C6:67:40:CB:03:D3:1E:90:EC
+
+ X509v3 Key Usage:
+ Digital Signature, Non Repudiation, Key Encipherment
+ X509v3 Extended Key Usage:
+ TLS Web Server Authentication
+ X509v3 Subject Alternative Name:
+ DNS:web-platform.test, DNS:op8.web-platform.test, DNS:op7.web-platform.test, DNS:op9.web-platform.test, DNS:op4.web-platform.test, DNS:not-web-platform.test, DNS:op6.web-platform.test, DNS:op3.web-platform.test, DNS:op2.web-platform.test, DNS:op1.web-platform.test, DNS:www.web-platform.test, DNS:op5.web-platform.test, DNS:op88.web-platform.test, DNS:op98.web-platform.test, DNS:op85.web-platform.test, DNS:op89.web-platform.test, DNS:op66.web-platform.test, DNS:op72.web-platform.test, DNS:op24.web-platform.test, DNS:op41.web-platform.test, DNS:op79.web-platform.test, DNS:op91.web-platform.test, DNS:op59.web-platform.test, DNS:op39.web-platform.test, DNS:op60.web-platform.test, DNS:op58.web-platform.test, DNS:op28.web-platform.test, DNS:www1.web-platform.test, DNS:op14.web-platform.test, DNS:op69.web-platform.test, DNS:op40.web-platform.test, DNS:op74.web-platform.test, DNS:op31.web-platform.test, DNS:op18.web-platform.test, DNS:op73.web-platform.test, DNS:op77.web-platform.test, DNS:op12.web-platform.test, DNS:op54.web-platform.test, DNS:op63.web-platform.test, DNS:op71.web-platform.test, DNS:op95.web-platform.test, DNS:op16.web-platform.test, DNS:op36.web-platform.test, DNS:op27.web-platform.test, DNS:op29.web-platform.test, DNS:op94.web-platform.test, DNS:op44.web-platform.test, DNS:op33.web-platform.test, DNS:op84.web-platform.test, DNS:op32.web-platform.test, DNS:op61.web-platform.test, DNS:op70.web-platform.test, DNS:www2.web-platform.test, DNS:op43.web-platform.test, DNS:op78.web-platform.test, DNS:op26.web-platform.test, DNS:op76.web-platform.test, DNS:op52.web-platform.test, DNS:op99.web-platform.test, DNS:op86.web-platform.test, DNS:op46.web-platform.test, DNS:op17.web-platform.test, DNS:op90.web-platform.test, DNS:op93.web-platform.test, DNS:op10.web-platform.test, DNS:op55.web-platform.test, DNS:op47.web-platform.test, DNS:op51.web-platform.test, DNS:op45.web-platform.test, DNS:op80.web-platform.test, DNS:op68.web-platform.test, DNS:op49.web-platform.test, DNS:op57.web-platform.test, DNS:op35.web-platform.test, DNS:op67.web-platform.test, DNS:op92.web-platform.test, DNS:op15.web-platform.test, DNS:op13.web-platform.test, DNS:op75.web-platform.test, DNS:op64.web-platform.test, DNS:op97.web-platform.test, DNS:op37.web-platform.test, DNS:op56.web-platform.test, DNS:op62.web-platform.test, DNS:op82.web-platform.test, DNS:op25.web-platform.test, DNS:op11.web-platform.test, DNS:op50.web-platform.test, DNS:op38.web-platform.test, DNS:op83.web-platform.test, DNS:op81.web-platform.test, DNS:op20.web-platform.test, DNS:op21.web-platform.test, DNS:op23.web-platform.test, DNS:op42.web-platform.test, DNS:op22.web-platform.test, DNS:op65.web-platform.test, DNS:op96.web-platform.test, DNS:op87.web-platform.test, DNS:op19.web-platform.test, DNS:op53.web-platform.test, DNS:op30.web-platform.test, DNS:op48.web-platform.test, DNS:op34.web-platform.test, DNS:op6.not-web-platform.test, DNS:op3.not-web-platform.test, DNS:op2.not-web-platform.test, DNS:op5.not-web-platform.test, DNS:www.not-web-platform.test, DNS:www.www.web-platform.test, DNS:op7.not-web-platform.test, DNS:op4.not-web-platform.test, DNS:op8.not-web-platform.test, DNS:op9.not-web-platform.test, DNS:op1.not-web-platform.test, DNS:op36.not-web-platform.test, DNS:op53.not-web-platform.test, DNS:op50.not-web-platform.test, DNS:op24.not-web-platform.test, DNS:op31.not-web-platform.test, DNS:op95.not-web-platform.test, DNS:op83.not-web-platform.test, DNS:www2.not-web-platform.test, DNS:op73.not-web-platform.test, DNS:op19.not-web-platform.test, DNS:op21.not-web-platform.test, DNS:op81.not-web-platform.test, DNS:op70.not-web-platform.test, DNS:op78.not-web-platform.test, DNS:op40.not-web-platform.test, DNS:op25.not-web-platform.test, DNS:op65.not-web-platform.test, DNS:www.www2.web-platform.test, DNS:op80.not-web-platform.test, DNS:op52.not-web-platform.test, DNS:op68.not-web-platform.test, DNS:op45.not-web-platform.test, DNS:op71.not-web-platform.test, DNS:op72.not-web-platform.test, DNS:op90.not-web-platform.test, DNS:op89.not-web-platform.test, DNS:op49.not-web-platform.test, DNS:op77.not-web-platform.test, DNS:op79.not-web-platform.test, DNS:op82.not-web-platform.test, DNS:www.www1.web-platform.test, DNS:op12.not-web-platform.test, DNS:op39.not-web-platform.test, DNS:op44.not-web-platform.test, DNS:www1.not-web-platform.test, DNS:op58.not-web-platform.test, DNS:op14.not-web-platform.test, DNS:op30.not-web-platform.test, DNS:op62.not-web-platform.test, DNS:op61.not-web-platform.test, DNS:op92.not-web-platform.test, DNS:op29.not-web-platform.test, DNS:op98.not-web-platform.test, DNS:op64.not-web-platform.test, DNS:op26.not-web-platform.test, DNS:op22.not-web-platform.test, DNS:op94.not-web-platform.test, DNS:op38.not-web-platform.test, DNS:op33.not-web-platform.test, DNS:op23.not-web-platform.test, DNS:op57.not-web-platform.test, DNS:op54.not-web-platform.test, DNS:op85.not-web-platform.test, DNS:op46.not-web-platform.test, DNS:op97.not-web-platform.test, DNS:op32.not-web-platform.test, DNS:op60.not-web-platform.test, DNS:op96.not-web-platform.test, DNS:op51.not-web-platform.test, DNS:op41.not-web-platform.test, DNS:op35.not-web-platform.test, DNS:op99.not-web-platform.test, DNS:op42.not-web-platform.test, DNS:op67.not-web-platform.test, DNS:op37.not-web-platform.test, DNS:op48.not-web-platform.test, DNS:op55.not-web-platform.test, DNS:op56.not-web-platform.test, DNS:op84.not-web-platform.test, DNS:op34.not-web-platform.test, DNS:op69.not-web-platform.test, DNS:op11.not-web-platform.test, DNS:op93.not-web-platform.test, DNS:www1.www.web-platform.test, DNS:op86.not-web-platform.test, DNS:op13.not-web-platform.test, DNS:op20.not-web-platform.test, DNS:op76.not-web-platform.test, DNS:op27.not-web-platform.test, DNS:op17.not-web-platform.test, DNS:op75.not-web-platform.test, DNS:op15.not-web-platform.test, DNS:op47.not-web-platform.test, DNS:op18.not-web-platform.test, DNS:op63.not-web-platform.test, DNS:op28.not-web-platform.test, DNS:op43.not-web-platform.test, DNS:op66.not-web-platform.test, DNS:www2.www.web-platform.test, DNS:op91.not-web-platform.test, DNS:op74.not-web-platform.test, DNS:op59.not-web-platform.test, DNS:op88.not-web-platform.test, DNS:op87.not-web-platform.test, DNS:op10.not-web-platform.test, DNS:op16.not-web-platform.test, DNS:www1.www2.web-platform.test, DNS:www2.www2.web-platform.test, DNS:www2.www1.web-platform.test, DNS:www1.www1.web-platform.test, DNS:www.www.not-web-platform.test, DNS:xn--lve-6lad.web-platform.test, DNS:www1.www.not-web-platform.test, DNS:www.www2.not-web-platform.test, DNS:www2.www.not-web-platform.test, DNS:www.www1.not-web-platform.test, DNS:www2.www2.not-web-platform.test, DNS:www2.www1.not-web-platform.test, DNS:www1.www1.not-web-platform.test, DNS:www1.www2.not-web-platform.test, DNS:xn--lve-6lad.www.web-platform.test, DNS:xn--lve-6lad.not-web-platform.test, DNS:www.xn--lve-6lad.web-platform.test, DNS:www2.xn--lve-6lad.web-platform.test, DNS:xn--lve-6lad.www2.web-platform.test, DNS:xn--lve-6lad.www1.web-platform.test, DNS:www1.xn--lve-6lad.web-platform.test, DNS:xn--lve-6lad.www.not-web-platform.test, DNS:www.xn--lve-6lad.not-web-platform.test, DNS:xn--lve-6lad.www1.not-web-platform.test, DNS:www2.xn--lve-6lad.not-web-platform.test, DNS:www1.xn--lve-6lad.not-web-platform.test, DNS:xn--lve-6lad.www2.not-web-platform.test, DNS:xn--n8j6ds53lwwkrqhv28a.web-platform.test, DNS:xn--lve-6lad.xn--lve-6lad.web-platform.test, DNS:www.xn--n8j6ds53lwwkrqhv28a.web-platform.test, DNS:xn--n8j6ds53lwwkrqhv28a.not-web-platform.test, DNS:xn--n8j6ds53lwwkrqhv28a.www.web-platform.test, DNS:www1.xn--n8j6ds53lwwkrqhv28a.web-platform.test, DNS:xn--n8j6ds53lwwkrqhv28a.www2.web-platform.test, DNS:xn--n8j6ds53lwwkrqhv28a.www1.web-platform.test, DNS:www2.xn--n8j6ds53lwwkrqhv28a.web-platform.test, DNS:xn--lve-6lad.xn--lve-6lad.not-web-platform.test, DNS:www.xn--n8j6ds53lwwkrqhv28a.not-web-platform.test, DNS:xn--n8j6ds53lwwkrqhv28a.www.not-web-platform.test, DNS:xn--n8j6ds53lwwkrqhv28a.www2.not-web-platform.test, DNS:www1.xn--n8j6ds53lwwkrqhv28a.not-web-platform.test, DNS:www2.xn--n8j6ds53lwwkrqhv28a.not-web-platform.test, DNS:xn--n8j6ds53lwwkrqhv28a.www1.not-web-platform.test, DNS:xn--n8j6ds53lwwkrqhv28a.xn--lve-6lad.web-platform.test, DNS:xn--lve-6lad.xn--n8j6ds53lwwkrqhv28a.web-platform.test, DNS:xn--n8j6ds53lwwkrqhv28a.xn--lve-6lad.not-web-platform.test, DNS:xn--lve-6lad.xn--n8j6ds53lwwkrqhv28a.not-web-platform.test, DNS:xn--n8j6ds53lwwkrqhv28a.xn--n8j6ds53lwwkrqhv28a.web-platform.test, DNS:xn--n8j6ds53lwwkrqhv28a.xn--n8j6ds53lwwkrqhv28a.not-web-platform.test
+ Signature Algorithm: sha256WithRSAEncryption
+ 0b:8b:55:6a:b4:65:a8:2a:3b:c0:62:0e:85:d5:dc:99:9f:3f:
+ d0:b7:ff:cb:b6:68:e5:ae:d3:a2:4e:c8:d4:aa:34:95:e4:9e:
+ 4b:e4:74:e4:c4:ec:11:31:67:e5:a6:da:85:6b:0f:d7:64:b7:
+ e7:eb:fe:09:b6:88:e1:74:1d:0c:15:a3:2e:01:03:fc:67:9c:
+ 7c:ba:e6:fb:52:1d:58:cd:68:a3:55:8c:9b:57:bb:89:32:10:
+ 41:71:99:a3:1a:dc:25:2d:d0:b9:1a:af:e6:76:40:d7:f5:23:
+ a9:d8:59:45:8a:ab:53:53:87:cc:14:b3:87:de:1d:01:99:7a:
+ 4f:72:47:69:9c:98:73:94:79:90:25:70:02:ec:75:b2:77:11:
+ 28:97:cd:43:3d:d5:68:38:73:71:fd:a9:0b:a2:ed:2d:1c:6e:
+ e1:05:d4:30:a6:64:04:10:ca:e8:e6:e0:7a:a9:f4:26:94:6b:
+ a4:28:aa:0c:eb:3e:e0:ef:d2:be:0b:70:e4:dd:4c:fc:a2:87:
+ 0c:24:83:72:e2:e4:6f:25:a8:ee:c5:12:89:d9:dd:8f:82:14:
+ c3:d6:06:ae:1d:bb:7b:3c:fc:9b:27:56:50:b4:28:44:60:80:
+ 17:87:1c:b1:ce:6d:77:b4:d1:2b:32:e8:d0:ba:06:6f:4d:04:
+ ff:60:fe:e3
+-----BEGIN CERTIFICATE-----
+MIIgvDCCH6SgAwIBAgIDDgfEMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMMEndl
+Yi1wbGF0Zm9ybS10ZXN0czAeFw0yMDA0MjAxMTIwNTZaFw0zMDA0MTgxMTIwNTZa
+MBwxGjAYBgNVBAMMEXdlYi1wbGF0Zm9ybS50ZXN0MIIBIjANBgkqhkiG9w0BAQEF
+AAOCAQ8AMIIBCgKCAQEA28j7ogRqcYWrxd4+6SOoPgNIAuAUy06jDmykk5EFrI6t
+RjndiwNZcEzyzm8K74SFBc28KCGzsKDzHjJI7LBQhxtxTI+enCogENgxSOnSuT4x
+lFTUkcZPxeB4dmcfQ0xDZfxG/3EU9srrvlvRwrvwVn21sJcMDeb8cp3gqIeXAQf4
+3dSkzkYLmKbmrR2/OQA+QUYWC18Bxe8lRzVcR3rhpSt9crmAhzSrwRw4GReU6fBa
+il51VPj5SlwklOetNnNlOcRyBbyk8omZTHGFzUlKdJXDcP4k0lbbDxOJNbLbuqpF
+nU+x/7lPhlAfmi6jvrLwP7PRhl/B2YLfq70hRPJSCQIDAQABo4IeBDCCHgAwCQYD
+VR0TBAIwADAdBgNVHQ4EFgQUeZbhlvN8QSs7nxsN1qm4t0/mYVwwHwYDVR0jBBgw
+FoAU9rnaFgcFu2/JYzDGZ0DLA9MekOwwCwYDVR0PBAQDAgXgMBMGA1UdJQQMMAoG
+CCsGAQUFBwMBMIIdjwYDVR0RBIIdhjCCHYKCEXdlYi1wbGF0Zm9ybS50ZXN0ghVv
+cDgud2ViLXBsYXRmb3JtLnRlc3SCFW9wNy53ZWItcGxhdGZvcm0udGVzdIIVb3A5
+LndlYi1wbGF0Zm9ybS50ZXN0ghVvcDQud2ViLXBsYXRmb3JtLnRlc3SCFW5vdC13
+ZWItcGxhdGZvcm0udGVzdIIVb3A2LndlYi1wbGF0Zm9ybS50ZXN0ghVvcDMud2Vi
+LXBsYXRmb3JtLnRlc3SCFW9wMi53ZWItcGxhdGZvcm0udGVzdIIVb3AxLndlYi1w
+bGF0Zm9ybS50ZXN0ghV3d3cud2ViLXBsYXRmb3JtLnRlc3SCFW9wNS53ZWItcGxh
+dGZvcm0udGVzdIIWb3A4OC53ZWItcGxhdGZvcm0udGVzdIIWb3A5OC53ZWItcGxh
+dGZvcm0udGVzdIIWb3A4NS53ZWItcGxhdGZvcm0udGVzdIIWb3A4OS53ZWItcGxh
+dGZvcm0udGVzdIIWb3A2Ni53ZWItcGxhdGZvcm0udGVzdIIWb3A3Mi53ZWItcGxh
+dGZvcm0udGVzdIIWb3AyNC53ZWItcGxhdGZvcm0udGVzdIIWb3A0MS53ZWItcGxh
+dGZvcm0udGVzdIIWb3A3OS53ZWItcGxhdGZvcm0udGVzdIIWb3A5MS53ZWItcGxh
+dGZvcm0udGVzdIIWb3A1OS53ZWItcGxhdGZvcm0udGVzdIIWb3AzOS53ZWItcGxh
+dGZvcm0udGVzdIIWb3A2MC53ZWItcGxhdGZvcm0udGVzdIIWb3A1OC53ZWItcGxh
+dGZvcm0udGVzdIIWb3AyOC53ZWItcGxhdGZvcm0udGVzdIIWd3d3MS53ZWItcGxh
+dGZvcm0udGVzdIIWb3AxNC53ZWItcGxhdGZvcm0udGVzdIIWb3A2OS53ZWItcGxh
+dGZvcm0udGVzdIIWb3A0MC53ZWItcGxhdGZvcm0udGVzdIIWb3A3NC53ZWItcGxh
+dGZvcm0udGVzdIIWb3AzMS53ZWItcGxhdGZvcm0udGVzdIIWb3AxOC53ZWItcGxh
+dGZvcm0udGVzdIIWb3A3My53ZWItcGxhdGZvcm0udGVzdIIWb3A3Ny53ZWItcGxh
+dGZvcm0udGVzdIIWb3AxMi53ZWItcGxhdGZvcm0udGVzdIIWb3A1NC53ZWItcGxh
+dGZvcm0udGVzdIIWb3A2My53ZWItcGxhdGZvcm0udGVzdIIWb3A3MS53ZWItcGxh
+dGZvcm0udGVzdIIWb3A5NS53ZWItcGxhdGZvcm0udGVzdIIWb3AxNi53ZWItcGxh
+dGZvcm0udGVzdIIWb3AzNi53ZWItcGxhdGZvcm0udGVzdIIWb3AyNy53ZWItcGxh
+dGZvcm0udGVzdIIWb3AyOS53ZWItcGxhdGZvcm0udGVzdIIWb3A5NC53ZWItcGxh
+dGZvcm0udGVzdIIWb3A0NC53ZWItcGxhdGZvcm0udGVzdIIWb3AzMy53ZWItcGxh
+dGZvcm0udGVzdIIWb3A4NC53ZWItcGxhdGZvcm0udGVzdIIWb3AzMi53ZWItcGxh
+dGZvcm0udGVzdIIWb3A2MS53ZWItcGxhdGZvcm0udGVzdIIWb3A3MC53ZWItcGxh
+dGZvcm0udGVzdIIWd3d3Mi53ZWItcGxhdGZvcm0udGVzdIIWb3A0My53ZWItcGxh
+dGZvcm0udGVzdIIWb3A3OC53ZWItcGxhdGZvcm0udGVzdIIWb3AyNi53ZWItcGxh
+dGZvcm0udGVzdIIWb3A3Ni53ZWItcGxhdGZvcm0udGVzdIIWb3A1Mi53ZWItcGxh
+dGZvcm0udGVzdIIWb3A5OS53ZWItcGxhdGZvcm0udGVzdIIWb3A4Ni53ZWItcGxh
+dGZvcm0udGVzdIIWb3A0Ni53ZWItcGxhdGZvcm0udGVzdIIWb3AxNy53ZWItcGxh
+dGZvcm0udGVzdIIWb3A5MC53ZWItcGxhdGZvcm0udGVzdIIWb3A5My53ZWItcGxh
+dGZvcm0udGVzdIIWb3AxMC53ZWItcGxhdGZvcm0udGVzdIIWb3A1NS53ZWItcGxh
+dGZvcm0udGVzdIIWb3A0Ny53ZWItcGxhdGZvcm0udGVzdIIWb3A1MS53ZWItcGxh
+dGZvcm0udGVzdIIWb3A0NS53ZWItcGxhdGZvcm0udGVzdIIWb3A4MC53ZWItcGxh
+dGZvcm0udGVzdIIWb3A2OC53ZWItcGxhdGZvcm0udGVzdIIWb3A0OS53ZWItcGxh
+dGZvcm0udGVzdIIWb3A1Ny53ZWItcGxhdGZvcm0udGVzdIIWb3AzNS53ZWItcGxh
+dGZvcm0udGVzdIIWb3A2Ny53ZWItcGxhdGZvcm0udGVzdIIWb3A5Mi53ZWItcGxh
+dGZvcm0udGVzdIIWb3AxNS53ZWItcGxhdGZvcm0udGVzdIIWb3AxMy53ZWItcGxh
+dGZvcm0udGVzdIIWb3A3NS53ZWItcGxhdGZvcm0udGVzdIIWb3A2NC53ZWItcGxh
+dGZvcm0udGVzdIIWb3A5Ny53ZWItcGxhdGZvcm0udGVzdIIWb3AzNy53ZWItcGxh
+dGZvcm0udGVzdIIWb3A1Ni53ZWItcGxhdGZvcm0udGVzdIIWb3A2Mi53ZWItcGxh
+dGZvcm0udGVzdIIWb3A4Mi53ZWItcGxhdGZvcm0udGVzdIIWb3AyNS53ZWItcGxh
+dGZvcm0udGVzdIIWb3AxMS53ZWItcGxhdGZvcm0udGVzdIIWb3A1MC53ZWItcGxh
+dGZvcm0udGVzdIIWb3AzOC53ZWItcGxhdGZvcm0udGVzdIIWb3A4My53ZWItcGxh
+dGZvcm0udGVzdIIWb3A4MS53ZWItcGxhdGZvcm0udGVzdIIWb3AyMC53ZWItcGxh
+dGZvcm0udGVzdIIWb3AyMS53ZWItcGxhdGZvcm0udGVzdIIWb3AyMy53ZWItcGxh
+dGZvcm0udGVzdIIWb3A0Mi53ZWItcGxhdGZvcm0udGVzdIIWb3AyMi53ZWItcGxh
+dGZvcm0udGVzdIIWb3A2NS53ZWItcGxhdGZvcm0udGVzdIIWb3A5Ni53ZWItcGxh
+dGZvcm0udGVzdIIWb3A4Ny53ZWItcGxhdGZvcm0udGVzdIIWb3AxOS53ZWItcGxh
+dGZvcm0udGVzdIIWb3A1My53ZWItcGxhdGZvcm0udGVzdIIWb3AzMC53ZWItcGxh
+dGZvcm0udGVzdIIWb3A0OC53ZWItcGxhdGZvcm0udGVzdIIWb3AzNC53ZWItcGxh
+dGZvcm0udGVzdIIZb3A2Lm5vdC13ZWItcGxhdGZvcm0udGVzdIIZb3AzLm5vdC13
+ZWItcGxhdGZvcm0udGVzdIIZb3AyLm5vdC13ZWItcGxhdGZvcm0udGVzdIIZb3A1
+Lm5vdC13ZWItcGxhdGZvcm0udGVzdIIZd3d3Lm5vdC13ZWItcGxhdGZvcm0udGVz
+dIIZd3d3Lnd3dy53ZWItcGxhdGZvcm0udGVzdIIZb3A3Lm5vdC13ZWItcGxhdGZv
+cm0udGVzdIIZb3A0Lm5vdC13ZWItcGxhdGZvcm0udGVzdIIZb3A4Lm5vdC13ZWIt
+cGxhdGZvcm0udGVzdIIZb3A5Lm5vdC13ZWItcGxhdGZvcm0udGVzdIIZb3AxLm5v
+dC13ZWItcGxhdGZvcm0udGVzdIIab3AzNi5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SC
+Gm9wNTMubm90LXdlYi1wbGF0Zm9ybS50ZXN0ghpvcDUwLm5vdC13ZWItcGxhdGZv
+cm0udGVzdIIab3AyNC5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SCGm9wMzEubm90LXdl
+Yi1wbGF0Zm9ybS50ZXN0ghpvcDk1Lm5vdC13ZWItcGxhdGZvcm0udGVzdIIab3A4
+My5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SCGnd3dzIubm90LXdlYi1wbGF0Zm9ybS50
+ZXN0ghpvcDczLm5vdC13ZWItcGxhdGZvcm0udGVzdIIab3AxOS5ub3Qtd2ViLXBs
+YXRmb3JtLnRlc3SCGm9wMjEubm90LXdlYi1wbGF0Zm9ybS50ZXN0ghpvcDgxLm5v
+dC13ZWItcGxhdGZvcm0udGVzdIIab3A3MC5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SC
+Gm9wNzgubm90LXdlYi1wbGF0Zm9ybS50ZXN0ghpvcDQwLm5vdC13ZWItcGxhdGZv
+cm0udGVzdIIab3AyNS5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SCGm9wNjUubm90LXdl
+Yi1wbGF0Zm9ybS50ZXN0ghp3d3cud3d3Mi53ZWItcGxhdGZvcm0udGVzdIIab3A4
+MC5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SCGm9wNTIubm90LXdlYi1wbGF0Zm9ybS50
+ZXN0ghpvcDY4Lm5vdC13ZWItcGxhdGZvcm0udGVzdIIab3A0NS5ub3Qtd2ViLXBs
+YXRmb3JtLnRlc3SCGm9wNzEubm90LXdlYi1wbGF0Zm9ybS50ZXN0ghpvcDcyLm5v
+dC13ZWItcGxhdGZvcm0udGVzdIIab3A5MC5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SC
+Gm9wODkubm90LXdlYi1wbGF0Zm9ybS50ZXN0ghpvcDQ5Lm5vdC13ZWItcGxhdGZv
+cm0udGVzdIIab3A3Ny5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SCGm9wNzkubm90LXdl
+Yi1wbGF0Zm9ybS50ZXN0ghpvcDgyLm5vdC13ZWItcGxhdGZvcm0udGVzdIIad3d3
+Lnd3dzEud2ViLXBsYXRmb3JtLnRlc3SCGm9wMTIubm90LXdlYi1wbGF0Zm9ybS50
+ZXN0ghpvcDM5Lm5vdC13ZWItcGxhdGZvcm0udGVzdIIab3A0NC5ub3Qtd2ViLXBs
+YXRmb3JtLnRlc3SCGnd3dzEubm90LXdlYi1wbGF0Zm9ybS50ZXN0ghpvcDU4Lm5v
+dC13ZWItcGxhdGZvcm0udGVzdIIab3AxNC5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SC
+Gm9wMzAubm90LXdlYi1wbGF0Zm9ybS50ZXN0ghpvcDYyLm5vdC13ZWItcGxhdGZv
+cm0udGVzdIIab3A2MS5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SCGm9wOTIubm90LXdl
+Yi1wbGF0Zm9ybS50ZXN0ghpvcDI5Lm5vdC13ZWItcGxhdGZvcm0udGVzdIIab3A5
+OC5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SCGm9wNjQubm90LXdlYi1wbGF0Zm9ybS50
+ZXN0ghpvcDI2Lm5vdC13ZWItcGxhdGZvcm0udGVzdIIab3AyMi5ub3Qtd2ViLXBs
+YXRmb3JtLnRlc3SCGm9wOTQubm90LXdlYi1wbGF0Zm9ybS50ZXN0ghpvcDM4Lm5v
+dC13ZWItcGxhdGZvcm0udGVzdIIab3AzMy5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SC
+Gm9wMjMubm90LXdlYi1wbGF0Zm9ybS50ZXN0ghpvcDU3Lm5vdC13ZWItcGxhdGZv
+cm0udGVzdIIab3A1NC5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SCGm9wODUubm90LXdl
+Yi1wbGF0Zm9ybS50ZXN0ghpvcDQ2Lm5vdC13ZWItcGxhdGZvcm0udGVzdIIab3A5
+Ny5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SCGm9wMzIubm90LXdlYi1wbGF0Zm9ybS50
+ZXN0ghpvcDYwLm5vdC13ZWItcGxhdGZvcm0udGVzdIIab3A5Ni5ub3Qtd2ViLXBs
+YXRmb3JtLnRlc3SCGm9wNTEubm90LXdlYi1wbGF0Zm9ybS50ZXN0ghpvcDQxLm5v
+dC13ZWItcGxhdGZvcm0udGVzdIIab3AzNS5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SC
+Gm9wOTkubm90LXdlYi1wbGF0Zm9ybS50ZXN0ghpvcDQyLm5vdC13ZWItcGxhdGZv
+cm0udGVzdIIab3A2Ny5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SCGm9wMzcubm90LXdl
+Yi1wbGF0Zm9ybS50ZXN0ghpvcDQ4Lm5vdC13ZWItcGxhdGZvcm0udGVzdIIab3A1
+NS5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SCGm9wNTYubm90LXdlYi1wbGF0Zm9ybS50
+ZXN0ghpvcDg0Lm5vdC13ZWItcGxhdGZvcm0udGVzdIIab3AzNC5ub3Qtd2ViLXBs
+YXRmb3JtLnRlc3SCGm9wNjkubm90LXdlYi1wbGF0Zm9ybS50ZXN0ghpvcDExLm5v
+dC13ZWItcGxhdGZvcm0udGVzdIIab3A5My5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SC
+Gnd3dzEud3d3LndlYi1wbGF0Zm9ybS50ZXN0ghpvcDg2Lm5vdC13ZWItcGxhdGZv
+cm0udGVzdIIab3AxMy5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SCGm9wMjAubm90LXdl
+Yi1wbGF0Zm9ybS50ZXN0ghpvcDc2Lm5vdC13ZWItcGxhdGZvcm0udGVzdIIab3Ay
+Ny5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SCGm9wMTcubm90LXdlYi1wbGF0Zm9ybS50
+ZXN0ghpvcDc1Lm5vdC13ZWItcGxhdGZvcm0udGVzdIIab3AxNS5ub3Qtd2ViLXBs
+YXRmb3JtLnRlc3SCGm9wNDcubm90LXdlYi1wbGF0Zm9ybS50ZXN0ghpvcDE4Lm5v
+dC13ZWItcGxhdGZvcm0udGVzdIIab3A2My5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SC
+Gm9wMjgubm90LXdlYi1wbGF0Zm9ybS50ZXN0ghpvcDQzLm5vdC13ZWItcGxhdGZv
+cm0udGVzdIIab3A2Ni5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SCGnd3dzIud3d3Lndl
+Yi1wbGF0Zm9ybS50ZXN0ghpvcDkxLm5vdC13ZWItcGxhdGZvcm0udGVzdIIab3A3
+NC5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SCGm9wNTkubm90LXdlYi1wbGF0Zm9ybS50
+ZXN0ghpvcDg4Lm5vdC13ZWItcGxhdGZvcm0udGVzdIIab3A4Ny5ub3Qtd2ViLXBs
+YXRmb3JtLnRlc3SCGm9wMTAubm90LXdlYi1wbGF0Zm9ybS50ZXN0ghpvcDE2Lm5v
+dC13ZWItcGxhdGZvcm0udGVzdIIbd3d3MS53d3cyLndlYi1wbGF0Zm9ybS50ZXN0
+ght3d3cyLnd3dzIud2ViLXBsYXRmb3JtLnRlc3SCG3d3dzIud3d3MS53ZWItcGxh
+dGZvcm0udGVzdIIbd3d3MS53d3cxLndlYi1wbGF0Zm9ybS50ZXN0gh13d3cud3d3
+Lm5vdC13ZWItcGxhdGZvcm0udGVzdIIeeG4tLWx2ZS02bGFkLndlYi1wbGF0Zm9y
+bS50ZXN0gh53d3cxLnd3dy5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SCHnd3dy53d3cy
+Lm5vdC13ZWItcGxhdGZvcm0udGVzdIIed3d3Mi53d3cubm90LXdlYi1wbGF0Zm9y
+bS50ZXN0gh53d3cud3d3MS5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SCH3d3dzIud3d3
+Mi5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SCH3d3dzIud3d3MS5ub3Qtd2ViLXBsYXRm
+b3JtLnRlc3SCH3d3dzEud3d3MS5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SCH3d3dzEu
+d3d3Mi5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SCInhuLS1sdmUtNmxhZC53d3cud2Vi
+LXBsYXRmb3JtLnRlc3SCInhuLS1sdmUtNmxhZC5ub3Qtd2ViLXBsYXRmb3JtLnRl
+c3SCInd3dy54bi0tbHZlLTZsYWQud2ViLXBsYXRmb3JtLnRlc3SCI3d3dzIueG4t
+LWx2ZS02bGFkLndlYi1wbGF0Zm9ybS50ZXN0giN4bi0tbHZlLTZsYWQud3d3Mi53
+ZWItcGxhdGZvcm0udGVzdIIjeG4tLWx2ZS02bGFkLnd3dzEud2ViLXBsYXRmb3Jt
+LnRlc3SCI3d3dzEueG4tLWx2ZS02bGFkLndlYi1wbGF0Zm9ybS50ZXN0giZ4bi0t
+bHZlLTZsYWQud3d3Lm5vdC13ZWItcGxhdGZvcm0udGVzdIImd3d3LnhuLS1sdmUt
+NmxhZC5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SCJ3huLS1sdmUtNmxhZC53d3cxLm5v
+dC13ZWItcGxhdGZvcm0udGVzdIInd3d3Mi54bi0tbHZlLTZsYWQubm90LXdlYi1w
+bGF0Zm9ybS50ZXN0gid3d3cxLnhuLS1sdmUtNmxhZC5ub3Qtd2ViLXBsYXRmb3Jt
+LnRlc3SCJ3huLS1sdmUtNmxhZC53d3cyLm5vdC13ZWItcGxhdGZvcm0udGVzdIIp
+eG4tLW44ajZkczUzbHd3a3JxaHYyOGEud2ViLXBsYXRmb3JtLnRlc3SCK3huLS1s
+dmUtNmxhZC54bi0tbHZlLTZsYWQud2ViLXBsYXRmb3JtLnRlc3SCLXd3dy54bi0t
+bjhqNmRzNTNsd3drcnFodjI4YS53ZWItcGxhdGZvcm0udGVzdIIteG4tLW44ajZk
+czUzbHd3a3JxaHYyOGEubm90LXdlYi1wbGF0Zm9ybS50ZXN0gi14bi0tbjhqNmRz
+NTNsd3drcnFodjI4YS53d3cud2ViLXBsYXRmb3JtLnRlc3SCLnd3dzEueG4tLW44
+ajZkczUzbHd3a3JxaHYyOGEud2ViLXBsYXRmb3JtLnRlc3SCLnhuLS1uOGo2ZHM1
+M2x3d2tycWh2MjhhLnd3dzIud2ViLXBsYXRmb3JtLnRlc3SCLnhuLS1uOGo2ZHM1
+M2x3d2tycWh2MjhhLnd3dzEud2ViLXBsYXRmb3JtLnRlc3SCLnd3dzIueG4tLW44
+ajZkczUzbHd3a3JxaHYyOGEud2ViLXBsYXRmb3JtLnRlc3SCL3huLS1sdmUtNmxh
+ZC54bi0tbHZlLTZsYWQubm90LXdlYi1wbGF0Zm9ybS50ZXN0gjF3d3cueG4tLW44
+ajZkczUzbHd3a3JxaHYyOGEubm90LXdlYi1wbGF0Zm9ybS50ZXN0gjF4bi0tbjhq
+NmRzNTNsd3drcnFodjI4YS53d3cubm90LXdlYi1wbGF0Zm9ybS50ZXN0gjJ4bi0t
+bjhqNmRzNTNsd3drcnFodjI4YS53d3cyLm5vdC13ZWItcGxhdGZvcm0udGVzdIIy
+d3d3MS54bi0tbjhqNmRzNTNsd3drcnFodjI4YS5ub3Qtd2ViLXBsYXRmb3JtLnRl
+c3SCMnd3dzIueG4tLW44ajZkczUzbHd3a3JxaHYyOGEubm90LXdlYi1wbGF0Zm9y
+bS50ZXN0gjJ4bi0tbjhqNmRzNTNsd3drcnFodjI4YS53d3cxLm5vdC13ZWItcGxh
+dGZvcm0udGVzdII2eG4tLW44ajZkczUzbHd3a3JxaHYyOGEueG4tLWx2ZS02bGFk
+LndlYi1wbGF0Zm9ybS50ZXN0gjZ4bi0tbHZlLTZsYWQueG4tLW44ajZkczUzbHd3
+a3JxaHYyOGEud2ViLXBsYXRmb3JtLnRlc3SCOnhuLS1uOGo2ZHM1M2x3d2tycWh2
+MjhhLnhuLS1sdmUtNmxhZC5ub3Qtd2ViLXBsYXRmb3JtLnRlc3SCOnhuLS1sdmUt
+NmxhZC54bi0tbjhqNmRzNTNsd3drcnFodjI4YS5ub3Qtd2ViLXBsYXRmb3JtLnRl
+c3SCQXhuLS1uOGo2ZHM1M2x3d2tycWh2MjhhLnhuLS1uOGo2ZHM1M2x3d2tycWh2
+MjhhLndlYi1wbGF0Zm9ybS50ZXN0gkV4bi0tbjhqNmRzNTNsd3drcnFodjI4YS54
+bi0tbjhqNmRzNTNsd3drcnFodjI4YS5ub3Qtd2ViLXBsYXRmb3JtLnRlc3QwDQYJ
+KoZIhvcNAQELBQADggEBAAuLVWq0ZagqO8BiDoXV3JmfP9C3/8u2aOWu06JOyNSq
+NJXknkvkdOTE7BExZ+Wm2oVrD9dkt+fr/gm2iOF0HQwVoy4BA/xnnHy65vtSHVjN
+aKNVjJtXu4kyEEFxmaMa3CUt0Lkar+Z2QNf1I6nYWUWKq1NTh8wUs4feHQGZek9y
+R2mcmHOUeZAlcALsdbJ3ESiXzUM91Wg4c3H9qQui7S0cbuEF1DCmZAQQyujm4Hqp
+9CaUa6QoqgzrPuDv0r4LcOTdTPyihwwkg3Li5G8lqO7FEonZ3Y+CFMPWBq4du3s8
+/JsnVlC0KERggBeHHLHObXe00Ssy6NC6Bm9NBP9g/uM=
+-----END CERTIFICATE-----
diff --git a/tools/quic/certs/config.json b/tools/quic/certs/config.json
new file mode 100644
index 0000000..72a6d05
--- /dev/null
+++ b/tools/quic/certs/config.json
@@ -0,0 +1,17 @@
+{
+ "ports": {
+ "http": [],
+ "https": ["auto"],
+ "ws": [],
+ "wss": []
+ },
+ "check_subdomains": false,
+ "ssl": {
+ "type": "openssl",
+ "openssl": {
+ "duration": 3650,
+ "force_regenerate": false,
+ "base_path": "tools/certs"
+ }
+ }
+}
diff --git a/tools/quic/quic_transport_server.py b/tools/quic/quic_transport_server.py
new file mode 100644
index 0000000..10d8174
--- /dev/null
+++ b/tools/quic/quic_transport_server.py
@@ -0,0 +1,244 @@
+#!/usr/bin/env python3
+import argparse
+import asyncio
+import io
+import logging
+import os
+import re
+import struct
+import urllib.parse
+from typing import Dict, Optional
+
+from aioquic.asyncio import QuicConnectionProtocol, serve
+from aioquic.quic.configuration import QuicConfiguration
+from aioquic.quic.connection import END_STATES
+from aioquic.quic.events import StreamDataReceived, QuicEvent
+from aioquic.tls import SessionTicket
+
+SERVER_NAME = 'aioquic-transport'
+
+handlers_path = None
+
+
+class EventHandler:
+ def __init__(self, connection: QuicConnectionProtocol, global_dict: Dict):
+ self.connection = connection
+ self.global_dict = global_dict
+
+ def handle_client_indication(
+ self,
+ origin: str,
+ query: Dict[str, str]) -> None:
+ name = 'handle_client_indication'
+ if name in self.global_dict:
+ self.global_dict[name](self.connection, origin, query)
+
+ def handle_event(self, event: QuicEvent) -> None:
+ name = 'handle_event'
+ if name in self.global_dict:
+ self.global_dict[name](self.connection, event)
+
+
+class QuicTransportProtocol(QuicConnectionProtocol):
+ def __init__(self, *args, **kwargs) -> None:
+ super().__init__(*args, **kwargs)
+ self.streams = dict()
+ self.pending_events = []
+ self.client_indication_finished = False
+ self.client_indication_data = b''
+ self.handler = None
+
+ def quic_event_received(self, event: QuicEvent) -> None:
+ prefix = '!!'
+ logging.log(logging.INFO, 'QUIC event: %s' % type(event))
+ try:
+ if (not self.client_indication_finished and
+ isinstance(event, StreamDataReceived) and
+ event.stream_id == 2):
+ # client indication process
+ self.client_indication_data += event.data
+ if event.end_stream:
+ prefix = 'Client inditation error: '
+ self.process_client_indication()
+ if self.is_closing_or_closed():
+ return
+ prefix = 'Event handling Error: '
+ for e in self.pending_events:
+ self.handler.handle_event(e)
+ self.pending_events.clear()
+ elif not self.client_indication_finished:
+ self.pending_events.append(event)
+ elif self.handler is not None:
+ prefix = 'Event handling Error: '
+ self.handler.handle_event(event)
+ except Exception as e:
+ self.handler = None
+ logging.log(logging.WARN, prefix + str(e))
+ self.close()
+
+ def parse_client_indication(self, bs):
+ while True:
+ key_b = bs.read(2)
+ if len(key_b) == 0:
+ return
+ length_b = bs.read(2)
+ if len(key_b) != 2:
+ raise Exception('failed to get "Key" field')
+ if len(length_b) != 2:
+ raise Exception('failed to get "Length" field')
+ key = struct.unpack('!H', key_b)[0]
+ length = struct.unpack('!H', length_b)[0]
+ value = bs.read(length)
+ if len(value) != length:
+ raise Exception('truncated "Value" field')
+ yield (key, value)
+
+ def process_client_indication(self) -> None:
+ origin = None
+ origin_string = None
+ path = None
+ path_string = None
+ KEY_ORIGIN = 0
+ KEY_PATH = 1
+ for (key, value) in self.parse_client_indication(
+ io.BytesIO(self.client_indication_data)):
+ if key == KEY_ORIGIN:
+ origin_string = value.decode()
+ origin = urllib.parse.urlparse(origin_string)
+ elif key == KEY_PATH:
+ path_string = value.decode()
+ path = urllib.parse.urlparse(path_string)
+ else:
+ # We must ignore unrecognized fields.
+ pass
+ logging.log(logging.INFO,
+ 'origin = %s, path = %s' % (origin_string, path_string))
+ if origin is None:
+ raise Exception('No origin is given')
+ if path is None:
+ raise Exception('No path is given')
+ if origin.scheme != 'https' and origin.scheme != 'http':
+ raise Exception('Invalid origin: %s' % origin_string)
+ if origin.netloc == '':
+ raise Exception('Invalid origin: %s' % origin_string)
+
+ # To make the situation simple we accept only simple path strings.
+ m = re.compile('^/([a-zA-Z0-9\._\-]+)$').match(path.path)
+ if m is None:
+ raise Exception('Invalid path: %s' % path_string)
+
+ handler_name = m.group(1)
+ query = dict(urllib.parse.parse_qsl(path.query))
+ self.handler = self.create_event_handler(handler_name)
+ self.handler.handle_client_indication(origin_string, query)
+ if self.is_closing_or_closed():
+ return
+ self.client_indication_finished = True
+ logging.log(logging.INFO, 'Client indication finished')
+
+ def create_event_handler(self, handler_name: str) -> None:
+ global_dict = {}
+ with open(handlers_path + '/' + handler_name) as f:
+ exec(f.read(), global_dict)
+ return EventHandler(self, global_dict)
+
+ def is_closing_or_closed(self) -> bool:
+ if self._quic._close_pending:
+ return True
+ if self._quic._state in END_STATES:
+ return True
+ return False
+
+
+class SessionTicketStore:
+ '''
+ Simple in-memory store for session tickets.
+ '''
+
+ def __init__(self) -> None:
+ self.tickets: Dict[bytes, SessionTicket] = {}
+
+ def add(self, ticket: SessionTicket) -> None:
+ self.tickets[ticket.ticket] = ticket
+
+ def pop(self, label: bytes) -> Optional[SessionTicket]:
+ return self.tickets.pop(label, None)
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description='QUIC server')
+ parser.add_argument(
+ '-c',
+ '--certificate',
+ type=str,
+ required=True,
+ help='load the TLS certificate from the specified file',
+ )
+ parser.add_argument(
+ '--host',
+ type=str,
+ default='::',
+ help='listen on the specified address (defaults to ::)',
+ )
+ parser.add_argument(
+ '--port',
+ type=int,
+ default=4433,
+ help='listen on the specified port (defaults to 4433)',
+ )
+ parser.add_argument(
+ '-k',
+ '--private-key',
+ type=str,
+ required=True,
+ help='load the TLS private key from the specified file',
+ )
+ parser.add_argument(
+ '--handlers-path',
+ type=str,
+ required=True,
+ help='the directory path of QuicTransport event handlers',
+ )
+ parser.add_argument(
+ '-v',
+ '--verbose',
+ action='store_true',
+ help='increase logging verbosity'
+ )
+ args = parser.parse_args()
+
+ logging.basicConfig(
+ format='%(asctime)s %(levelname)s %(name)s %(message)s',
+ level=logging.DEBUG if args.verbose else logging.INFO,
+ )
+
+ configuration = QuicConfiguration(
+ alpn_protocols=['wq-vvv-01'] + ['siduck'],
+ is_client=False,
+ max_datagram_frame_size=65536,
+ )
+
+ handlers_path = os.path.abspath(os.path.expanduser(args.handlers_path))
+ logging.log(logging.INFO, 'port = %s' % args.port)
+ logging.log(logging.INFO, 'handlers path = %s' % handlers_path)
+
+ # load SSL certificate and key
+ configuration.load_cert_chain(args.certificate, args.private_key)
+
+ ticket_store = SessionTicketStore()
+
+ loop = asyncio.get_event_loop()
+ loop.run_until_complete(
+ serve(
+ args.host,
+ args.port,
+ configuration=configuration,
+ create_protocol=QuicTransportProtocol,
+ session_ticket_fetcher=ticket_store.pop,
+ session_ticket_handler=ticket_store.add,
+ )
+ )
+ try:
+ loop.run_forever()
+ except KeyboardInterrupt:
+ pass
diff --git a/tools/wptserve/wptserve/sslutils/openssl.py b/tools/wptserve/wptserve/sslutils/openssl.py
index aea1c73..64f6d5f 100644
--- a/tools/wptserve/wptserve/sslutils/openssl.py
+++ b/tools/wptserve/wptserve/sslutils/openssl.py
@@ -402,6 +402,8 @@
def _generate_host_cert(self, hosts):
host = hosts[0]
+ if not self.force_regenerate:
+ self._load_ca_cert()
if self._ca_key_path is None:
self._generate_ca(hosts)
ca_key_path = self._ca_key_path
diff --git a/webtransport/quic/client-indication.any.js b/webtransport/quic/client-indication.any.js
new file mode 100644
index 0000000..15baa5f
--- /dev/null
+++ b/webtransport/quic/client-indication.any.js
@@ -0,0 +1,32 @@
+// META: quic=true
+// META: script=/common/get-host-info.sub.js
+
+const PORT = 8983;
+const {ORIGINAL_HOST: HOST, ORIGIN} = get_host_info();
+const BASE = `quic-transport://${HOST}:${PORT}`;
+
+promise_test(async (test) => {
+ function onClosed() {
+ assert_unreached('The closed promise should be ' +
+ 'fulfilled or rejected after getting a PASS signal.');
+ }
+ const qt = new QuicTransport(
+ `${BASE}/client-indication.quic.py?origin=${ORIGIN}`);
+ qt.closed.then(test.step_func(onClosed), test.step_func(onClosed));
+
+ const streams = qt.receiveStreams();
+ const {done, value} = await streams.getReader().read();
+ assert_false(done, 'getting an incoming stream');
+
+ const readable = value.readable.pipeThrough(new TextDecoderStream());
+ const reader = readable.getReader();
+ let result = '';
+ while (true) {
+ const {done, value} = await reader.read();
+ if (done) {
+ break;
+ }
+ result += value;
+ }
+ assert_equals(result, 'PASS');
+}, 'Client indication');
diff --git a/webtransport/quic/handlers/README.md b/webtransport/quic/handlers/README.md
new file mode 100644
index 0000000..22073e7
--- /dev/null
+++ b/webtransport/quic/handlers/README.md
@@ -0,0 +1,2 @@
+This directory contains custom handlers for testing QuicTransport. Please see
+https://github.com/web-platform-tests/wpt/tools/quic.
\ No newline at end of file
diff --git a/webtransport/quic/handlers/client-indication.quic.py b/webtransport/quic/handlers/client-indication.quic.py
new file mode 100644
index 0000000..da0701c
--- /dev/null
+++ b/webtransport/quic/handlers/client-indication.quic.py
@@ -0,0 +1,28 @@
+import asyncio
+import logging
+
+from aioquic.asyncio import QuicConnectionProtocol
+from aioquic.quic.events import QuicEvent
+from typing import Dict
+
+
+async def notify_pass(connection: QuicConnectionProtocol):
+ _, writer = await connection.create_stream(is_unidirectional=True)
+ writer.write(b'PASS')
+ writer.write_eof()
+
+
+def handle_client_indication(connection: QuicConnectionProtocol,
+ origin: str, query: Dict[str, str]):
+ logging.log(logging.INFO, 'origin = %s, query = %s' % (origin, query))
+ if 'origin' not in query or query['origin'] != origin:
+ logging.log(logging.WARN, 'Client indication failure: invalid origin')
+ connection.close()
+ return
+
+ loop = asyncio.get_event_loop()
+ loop.create_task(notify_pass(connection))
+
+
+def handle_event(connection: QuicConnectionProtocol, event: QuicEvent) -> None:
+ pass