From 512cf2462e561c4657e83ca2f6dc2f9fb3ca133e Mon Sep 17 00:00:00 2001 From: Yutaka Hirano Date: Wed, 13 May 2020 06:13:18 +0900 Subject: [PATCH] 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 --- tools/quic/README.md | 30 +++ tools/quic/certs/README.md | 12 + tools/quic/certs/cert.key | 28 ++ tools/quic/certs/cert.pem | 240 +++++++++++++++++ tools/quic/certs/config.json | 17 ++ tools/quic/quic_transport_server.py | 244 ++++++++++++++++++ tools/wptserve/wptserve/sslutils/openssl.py | 2 + webtransport/quic/client-indication.any.js | 32 +++ webtransport/quic/handlers/README.md | 2 + .../quic/handlers/client-indication.quic.py | 28 ++ 10 files changed, 635 insertions(+) create mode 100644 tools/quic/README.md create mode 100644 tools/quic/certs/README.md create mode 100644 tools/quic/certs/cert.key create mode 100644 tools/quic/certs/cert.pem create mode 100644 tools/quic/certs/config.json create mode 100644 tools/quic/quic_transport_server.py create mode 100644 webtransport/quic/client-indication.any.js create mode 100644 webtransport/quic/handlers/README.md create mode 100644 webtransport/quic/handlers/client-indication.quic.py diff --git a/tools/quic/README.md b/tools/quic/README.md new file mode 100644 index 00000000000000..5a64b701cfb859 --- /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 00000000000000..216cf13fffc1bb --- /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 00000000000000..993cdfa543728b --- /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 00000000000000..7e70f1b082acb3 --- /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 00000000000000..72a6d0529fb944 --- /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 00000000000000..10d81743693b43 --- /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 aea1c7380b179c..64f6d5fb2db279 100644 --- a/tools/wptserve/wptserve/sslutils/openssl.py +++ b/tools/wptserve/wptserve/sslutils/openssl.py @@ -402,6 +402,8 @@ def _load_host_cert(self, hosts): 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 00000000000000..15baa5f8542cb2 --- /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 00000000000000..22073e783a2097 --- /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 00000000000000..da0701cc04b17b --- /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