@push.rocks/smartproxy 19.3.2 → 19.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/forwarding/factory/forwarding-factory.js +29 -1
- package/dist_ts/http/index.d.ts +1 -3
- package/dist_ts/http/index.js +4 -10
- package/dist_ts/http/models/http-types.d.ts +4 -91
- package/dist_ts/http/models/http-types.js +5 -60
- package/dist_ts/http/router/proxy-router.d.ts +1 -1
- package/dist_ts/http/router/route-router.d.ts +1 -1
- package/dist_ts/index.d.ts +9 -7
- package/dist_ts/index.js +10 -7
- package/dist_ts/proxies/{network-proxy → http-proxy}/certificate-manager.d.ts +2 -2
- package/dist_ts/proxies/{network-proxy → http-proxy}/certificate-manager.js +1 -1
- package/dist_ts/proxies/{network-proxy → http-proxy}/connection-pool.d.ts +2 -2
- package/dist_ts/proxies/http-proxy/connection-pool.js +210 -0
- package/dist_ts/proxies/http-proxy/context-creator.js +108 -0
- package/dist_ts/proxies/{network-proxy → http-proxy}/function-cache.js +1 -1
- package/dist_ts/proxies/http-proxy/handlers/index.d.ts +5 -0
- package/dist_ts/proxies/http-proxy/handlers/index.js +6 -0
- package/dist_ts/proxies/http-proxy/handlers/redirect-handler.d.ts +18 -0
- package/dist_ts/proxies/http-proxy/handlers/redirect-handler.js +78 -0
- package/dist_ts/proxies/http-proxy/handlers/static-handler.d.ts +19 -0
- package/dist_ts/proxies/http-proxy/handlers/static-handler.js +203 -0
- package/dist_ts/proxies/{network-proxy/network-proxy.d.ts → http-proxy/http-proxy.d.ts} +10 -9
- package/dist_ts/proxies/{network-proxy/network-proxy.js → http-proxy/http-proxy.js} +13 -12
- package/dist_ts/proxies/{network-proxy → http-proxy}/http-request-handler.js +1 -1
- package/dist_ts/proxies/http-proxy/http2-request-handler.js +201 -0
- package/dist_ts/proxies/{network-proxy → http-proxy}/index.d.ts +2 -2
- package/dist_ts/proxies/http-proxy/index.js +12 -0
- package/dist_ts/proxies/http-proxy/models/http-types.d.ts +119 -0
- package/dist_ts/proxies/http-proxy/models/http-types.js +112 -0
- package/dist_ts/proxies/http-proxy/models/index.d.ts +5 -0
- package/dist_ts/proxies/http-proxy/models/index.js +6 -0
- package/dist_ts/proxies/{network-proxy → http-proxy}/models/types.d.ts +2 -2
- package/dist_ts/proxies/http-proxy/models/types.js +276 -0
- package/dist_ts/proxies/{network-proxy → http-proxy}/request-handler.d.ts +3 -3
- package/dist_ts/proxies/{network-proxy → http-proxy}/request-handler.js +2 -2
- package/dist_ts/proxies/http-proxy/security-manager.js +255 -0
- package/dist_ts/proxies/{network-proxy → http-proxy}/websocket-handler.d.ts +3 -3
- package/dist_ts/proxies/{network-proxy → http-proxy}/websocket-handler.js +2 -2
- package/dist_ts/proxies/index.d.ts +5 -5
- package/dist_ts/proxies/index.js +5 -5
- package/dist_ts/proxies/smart-proxy/certificate-manager.d.ts +4 -4
- package/dist_ts/proxies/smart-proxy/certificate-manager.js +11 -11
- package/dist_ts/proxies/smart-proxy/http-proxy-bridge.d.ts +41 -0
- package/dist_ts/proxies/smart-proxy/http-proxy-bridge.js +121 -0
- package/dist_ts/proxies/smart-proxy/index.d.ts +2 -1
- package/dist_ts/proxies/smart-proxy/index.js +4 -2
- package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +2 -2
- package/dist_ts/proxies/smart-proxy/port-manager.js +3 -3
- package/dist_ts/proxies/smart-proxy/route-connection-handler.d.ts +3 -3
- package/dist_ts/proxies/smart-proxy/route-connection-handler.js +24 -265
- package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +1 -1
- package/dist_ts/proxies/smart-proxy/smart-proxy.js +25 -25
- package/dist_ts/routing/index.d.ts +5 -0
- package/dist_ts/routing/index.js +8 -0
- package/dist_ts/routing/models/http-types.d.ts +6 -0
- package/dist_ts/routing/models/http-types.js +7 -0
- package/dist_ts/routing/router/index.d.ts +8 -0
- package/dist_ts/routing/router/index.js +7 -0
- package/dist_ts/{classes.router.d.ts → routing/router/proxy-router.d.ts} +14 -11
- package/dist_ts/{classes.router.js → routing/router/proxy-router.js} +2 -2
- package/dist_ts/routing/router/route-router.d.ts +108 -0
- package/dist_ts/routing/router/route-router.js +393 -0
- package/package.json +1 -1
- package/readme.md +18 -35
- package/readme.plan.md +173 -271
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/forwarding/factory/forwarding-factory.ts +28 -0
- package/ts/index.ts +13 -9
- package/ts/proxies/{network-proxy → http-proxy}/certificate-manager.ts +2 -2
- package/ts/proxies/{network-proxy → http-proxy}/connection-pool.ts +2 -2
- package/ts/proxies/http-proxy/handlers/index.ts +6 -0
- package/ts/proxies/http-proxy/handlers/redirect-handler.ts +105 -0
- package/ts/proxies/http-proxy/handlers/static-handler.ts +251 -0
- package/ts/proxies/{network-proxy/network-proxy.ts → http-proxy/http-proxy.ts} +15 -14
- package/ts/proxies/{network-proxy → http-proxy}/index.ts +3 -3
- package/ts/proxies/http-proxy/models/http-types.ts +165 -0
- package/ts/proxies/http-proxy/models/index.ts +5 -0
- package/ts/proxies/{network-proxy → http-proxy}/models/types.ts +2 -2
- package/ts/proxies/{network-proxy → http-proxy}/request-handler.ts +3 -3
- package/ts/proxies/{network-proxy → http-proxy}/websocket-handler.ts +3 -3
- package/ts/proxies/index.ts +7 -7
- package/ts/proxies/smart-proxy/certificate-manager.ts +10 -10
- package/ts/proxies/smart-proxy/{network-proxy-bridge.ts → http-proxy-bridge.ts} +44 -44
- package/ts/proxies/smart-proxy/index.ts +4 -1
- package/ts/proxies/smart-proxy/models/interfaces.ts +3 -3
- package/ts/proxies/smart-proxy/port-manager.ts +2 -2
- package/ts/proxies/smart-proxy/route-connection-handler.ts +23 -307
- package/ts/proxies/smart-proxy/smart-proxy.ts +25 -25
- package/ts/routing/index.ts +9 -0
- package/ts/routing/models/http-types.ts +6 -0
- package/ts/{http → routing}/router/proxy-router.ts +1 -1
- package/ts/{http → routing}/router/route-router.ts +1 -1
- package/dist_ts/certificate/acme/acme-factory.d.ts +0 -17
- package/dist_ts/certificate/acme/acme-factory.js +0 -40
- package/dist_ts/certificate/acme/challenge-handler.d.ts +0 -44
- package/dist_ts/certificate/acme/challenge-handler.js +0 -92
- package/dist_ts/certificate/acme/index.d.ts +0 -4
- package/dist_ts/certificate/acme/index.js +0 -5
- package/dist_ts/certificate/certificate-manager.d.ts +0 -150
- package/dist_ts/certificate/certificate-manager.js +0 -505
- package/dist_ts/certificate/events/certificate-events.d.ts +0 -33
- package/dist_ts/certificate/events/certificate-events.js +0 -38
- package/dist_ts/certificate/events/simplified-events.d.ts +0 -56
- package/dist_ts/certificate/events/simplified-events.js +0 -13
- package/dist_ts/certificate/index.d.ts +0 -30
- package/dist_ts/certificate/index.js +0 -37
- package/dist_ts/certificate/models/certificate-errors.d.ts +0 -69
- package/dist_ts/certificate/models/certificate-errors.js +0 -141
- package/dist_ts/certificate/models/certificate-strategy.d.ts +0 -60
- package/dist_ts/certificate/models/certificate-strategy.js +0 -73
- package/dist_ts/certificate/models/certificate-types.d.ts +0 -97
- package/dist_ts/certificate/models/certificate-types.js +0 -2
- package/dist_ts/certificate/providers/cert-provisioner.d.ts +0 -119
- package/dist_ts/certificate/providers/cert-provisioner.js +0 -422
- package/dist_ts/certificate/providers/index.d.ts +0 -4
- package/dist_ts/certificate/providers/index.js +0 -5
- package/dist_ts/certificate/simplified-certificate-manager.d.ts +0 -150
- package/dist_ts/certificate/simplified-certificate-manager.js +0 -501
- package/dist_ts/certificate/storage/file-storage.d.ts +0 -66
- package/dist_ts/certificate/storage/file-storage.js +0 -194
- package/dist_ts/certificate/storage/index.d.ts +0 -4
- package/dist_ts/certificate/storage/index.js +0 -5
- package/dist_ts/certificate/utils/certificate-helpers.d.ts +0 -17
- package/dist_ts/certificate/utils/certificate-helpers.js +0 -45
- package/dist_ts/classes.iptablesproxy.d.ts +0 -112
- package/dist_ts/classes.iptablesproxy.js +0 -765
- package/dist_ts/classes.networkproxy.d.ts +0 -243
- package/dist_ts/classes.networkproxy.js +0 -1424
- package/dist_ts/classes.nftablesproxy.d.ts +0 -219
- package/dist_ts/classes.nftablesproxy.js +0 -1542
- package/dist_ts/classes.port80handler.d.ts +0 -215
- package/dist_ts/classes.port80handler.js +0 -736
- package/dist_ts/classes.portproxy.d.ts +0 -171
- package/dist_ts/classes.portproxy.js +0 -1802
- package/dist_ts/classes.pp.acmemanager.d.ts +0 -34
- package/dist_ts/classes.pp.acmemanager.js +0 -123
- package/dist_ts/classes.pp.connectionhandler.d.ts +0 -39
- package/dist_ts/classes.pp.connectionhandler.js +0 -754
- package/dist_ts/classes.pp.connectionmanager.d.ts +0 -78
- package/dist_ts/classes.pp.connectionmanager.js +0 -378
- package/dist_ts/classes.pp.domainconfigmanager.d.ts +0 -55
- package/dist_ts/classes.pp.domainconfigmanager.js +0 -103
- package/dist_ts/classes.pp.interfaces.d.ts +0 -133
- package/dist_ts/classes.pp.interfaces.js +0 -2
- package/dist_ts/classes.pp.networkproxybridge.d.ts +0 -57
- package/dist_ts/classes.pp.networkproxybridge.js +0 -306
- package/dist_ts/classes.pp.portproxy.d.ts +0 -64
- package/dist_ts/classes.pp.portproxy.js +0 -567
- package/dist_ts/classes.pp.portrangemanager.d.ts +0 -56
- package/dist_ts/classes.pp.portrangemanager.js +0 -179
- package/dist_ts/classes.pp.securitymanager.d.ts +0 -47
- package/dist_ts/classes.pp.securitymanager.js +0 -126
- package/dist_ts/classes.pp.snihandler.d.ts +0 -153
- package/dist_ts/classes.pp.snihandler.js +0 -1053
- package/dist_ts/classes.pp.timeoutmanager.d.ts +0 -47
- package/dist_ts/classes.pp.timeoutmanager.js +0 -154
- package/dist_ts/classes.pp.tlsalert.d.ts +0 -149
- package/dist_ts/classes.pp.tlsalert.js +0 -225
- package/dist_ts/classes.pp.tlsmanager.d.ts +0 -57
- package/dist_ts/classes.pp.tlsmanager.js +0 -132
- package/dist_ts/classes.snihandler.d.ts +0 -198
- package/dist_ts/classes.snihandler.js +0 -1210
- package/dist_ts/classes.sslredirect.d.ts +0 -8
- package/dist_ts/classes.sslredirect.js +0 -28
- package/dist_ts/common/acmeFactory.d.ts +0 -9
- package/dist_ts/common/acmeFactory.js +0 -20
- package/dist_ts/common/port80-adapter.d.ts +0 -11
- package/dist_ts/common/port80-adapter.js +0 -87
- package/dist_ts/examples/forwarding-example.d.ts +0 -1
- package/dist_ts/examples/forwarding-example.js +0 -96
- package/dist_ts/forwarding/config/domain-config.d.ts +0 -12
- package/dist_ts/forwarding/config/domain-config.js +0 -12
- package/dist_ts/forwarding/config/domain-manager.d.ts +0 -86
- package/dist_ts/forwarding/config/domain-manager.js +0 -242
- package/dist_ts/helpers.certificates.d.ts +0 -5
- package/dist_ts/helpers.certificates.js +0 -23
- package/dist_ts/http/port80/acme-interfaces.d.ts +0 -108
- package/dist_ts/http/port80/acme-interfaces.js +0 -51
- package/dist_ts/http/port80/challenge-responder.d.ts +0 -53
- package/dist_ts/http/port80/challenge-responder.js +0 -203
- package/dist_ts/http/port80/index.d.ts +0 -6
- package/dist_ts/http/port80/index.js +0 -9
- package/dist_ts/http/port80/port80-handler.d.ts +0 -136
- package/dist_ts/http/port80/port80-handler.js +0 -592
- package/dist_ts/http/redirects/index.d.ts +0 -4
- package/dist_ts/http/redirects/index.js +0 -5
- package/dist_ts/networkproxy/classes.np.certificatemanager.d.ts +0 -77
- package/dist_ts/networkproxy/classes.np.certificatemanager.js +0 -372
- package/dist_ts/networkproxy/classes.np.connectionpool.d.ts +0 -47
- package/dist_ts/networkproxy/classes.np.connectionpool.js +0 -210
- package/dist_ts/networkproxy/classes.np.networkproxy.d.ts +0 -118
- package/dist_ts/networkproxy/classes.np.networkproxy.js +0 -387
- package/dist_ts/networkproxy/classes.np.requesthandler.d.ts +0 -56
- package/dist_ts/networkproxy/classes.np.requesthandler.js +0 -393
- package/dist_ts/networkproxy/classes.np.types.d.ts +0 -83
- package/dist_ts/networkproxy/classes.np.types.js +0 -35
- package/dist_ts/networkproxy/classes.np.websockethandler.d.ts +0 -38
- package/dist_ts/networkproxy/classes.np.websockethandler.js +0 -188
- package/dist_ts/networkproxy/index.d.ts +0 -1
- package/dist_ts/networkproxy/index.js +0 -4
- package/dist_ts/nfttablesproxy/classes.nftablesproxy.d.ts +0 -219
- package/dist_ts/nfttablesproxy/classes.nftablesproxy.js +0 -1542
- package/dist_ts/port80handler/classes.port80handler.d.ts +0 -10
- package/dist_ts/port80handler/classes.port80handler.js +0 -16
- package/dist_ts/proxies/network-proxy/connection-pool.js +0 -210
- package/dist_ts/proxies/network-proxy/context-creator.js +0 -108
- package/dist_ts/proxies/network-proxy/http2-request-handler.js +0 -201
- package/dist_ts/proxies/network-proxy/index.js +0 -12
- package/dist_ts/proxies/network-proxy/models/index.d.ts +0 -4
- package/dist_ts/proxies/network-proxy/models/index.js +0 -5
- package/dist_ts/proxies/network-proxy/models/types.js +0 -276
- package/dist_ts/proxies/network-proxy/security-manager.js +0 -255
- package/dist_ts/proxies/network-proxy/simplified-certificate-bridge.d.ts +0 -48
- package/dist_ts/proxies/network-proxy/simplified-certificate-bridge.js +0 -76
- package/dist_ts/proxies/smart-proxy/connection-handler.d.ts +0 -39
- package/dist_ts/proxies/smart-proxy/connection-handler.js +0 -894
- package/dist_ts/proxies/smart-proxy/domain-config-manager.d.ts +0 -110
- package/dist_ts/proxies/smart-proxy/domain-config-manager.js +0 -386
- package/dist_ts/proxies/smart-proxy/legacy-smart-proxy.d.ts +0 -168
- package/dist_ts/proxies/smart-proxy/legacy-smart-proxy.js +0 -642
- package/dist_ts/proxies/smart-proxy/models/simplified-smartproxy-config.d.ts +0 -65
- package/dist_ts/proxies/smart-proxy/models/simplified-smartproxy-config.js +0 -31
- package/dist_ts/proxies/smart-proxy/models/smartproxy-options.d.ts +0 -102
- package/dist_ts/proxies/smart-proxy/models/smartproxy-options.js +0 -73
- package/dist_ts/proxies/smart-proxy/network-proxy-bridge.d.ts +0 -41
- package/dist_ts/proxies/smart-proxy/network-proxy-bridge.js +0 -121
- package/dist_ts/proxies/smart-proxy/port-range-manager.d.ts +0 -56
- package/dist_ts/proxies/smart-proxy/port-range-manager.js +0 -176
- package/dist_ts/proxies/smart-proxy/route-helpers/index.d.ts +0 -9
- package/dist_ts/proxies/smart-proxy/route-helpers/index.js +0 -11
- package/dist_ts/proxies/smart-proxy/route-helpers.d.ts +0 -7
- package/dist_ts/proxies/smart-proxy/route-helpers.js +0 -9
- package/dist_ts/proxies/smart-proxy/simplified-smart-proxy.d.ts +0 -41
- package/dist_ts/proxies/smart-proxy/simplified-smart-proxy.js +0 -132
- package/dist_ts/proxies/smart-proxy/utils/route-migration-utils.d.ts +0 -51
- package/dist_ts/proxies/smart-proxy/utils/route-migration-utils.js +0 -124
- package/dist_ts/redirect/classes.redirect.d.ts +0 -96
- package/dist_ts/redirect/classes.redirect.js +0 -194
- package/dist_ts/smartproxy/classes.pp.certprovisioner.d.ts +0 -54
- package/dist_ts/smartproxy/classes.pp.certprovisioner.js +0 -179
- package/dist_ts/smartproxy/classes.pp.connectionhandler.d.ts +0 -39
- package/dist_ts/smartproxy/classes.pp.connectionhandler.js +0 -894
- package/dist_ts/smartproxy/classes.pp.connectionmanager.d.ts +0 -78
- package/dist_ts/smartproxy/classes.pp.connectionmanager.js +0 -378
- package/dist_ts/smartproxy/classes.pp.domainconfigmanager.d.ts +0 -94
- package/dist_ts/smartproxy/classes.pp.domainconfigmanager.js +0 -255
- package/dist_ts/smartproxy/classes.pp.interfaces.d.ts +0 -103
- package/dist_ts/smartproxy/classes.pp.interfaces.js +0 -2
- package/dist_ts/smartproxy/classes.pp.networkproxybridge.d.ts +0 -62
- package/dist_ts/smartproxy/classes.pp.networkproxybridge.js +0 -316
- package/dist_ts/smartproxy/classes.pp.portrangemanager.d.ts +0 -56
- package/dist_ts/smartproxy/classes.pp.portrangemanager.js +0 -176
- package/dist_ts/smartproxy/classes.pp.securitymanager.d.ts +0 -64
- package/dist_ts/smartproxy/classes.pp.securitymanager.js +0 -149
- package/dist_ts/smartproxy/classes.pp.snihandler.d.ts +0 -153
- package/dist_ts/smartproxy/classes.pp.snihandler.js +0 -1053
- package/dist_ts/smartproxy/classes.pp.timeoutmanager.d.ts +0 -47
- package/dist_ts/smartproxy/classes.pp.timeoutmanager.js +0 -154
- package/dist_ts/smartproxy/classes.pp.tlsalert.d.ts +0 -149
- package/dist_ts/smartproxy/classes.pp.tlsalert.js +0 -225
- package/dist_ts/smartproxy/classes.pp.tlsmanager.d.ts +0 -57
- package/dist_ts/smartproxy/classes.pp.tlsmanager.js +0 -132
- package/dist_ts/smartproxy/classes.smartproxy.d.ts +0 -63
- package/dist_ts/smartproxy/classes.smartproxy.js +0 -521
- package/dist_ts/smartproxy/forwarding/domain-config.d.ts +0 -12
- package/dist_ts/smartproxy/forwarding/domain-config.js +0 -12
- package/dist_ts/smartproxy/forwarding/domain-manager.d.ts +0 -86
- package/dist_ts/smartproxy/forwarding/domain-manager.js +0 -241
- package/dist_ts/smartproxy/forwarding/forwarding.factory.d.ts +0 -24
- package/dist_ts/smartproxy/forwarding/forwarding.factory.js +0 -137
- package/dist_ts/smartproxy/forwarding/forwarding.handler.d.ts +0 -55
- package/dist_ts/smartproxy/forwarding/forwarding.handler.js +0 -94
- package/dist_ts/smartproxy/forwarding/http.handler.d.ts +0 -25
- package/dist_ts/smartproxy/forwarding/http.handler.js +0 -123
- package/dist_ts/smartproxy/forwarding/https-passthrough.handler.d.ts +0 -24
- package/dist_ts/smartproxy/forwarding/https-passthrough.handler.js +0 -154
- package/dist_ts/smartproxy/forwarding/https-terminate-to-http.handler.d.ts +0 -36
- package/dist_ts/smartproxy/forwarding/https-terminate-to-http.handler.js +0 -229
- package/dist_ts/smartproxy/forwarding/https-terminate-to-https.handler.d.ts +0 -35
- package/dist_ts/smartproxy/forwarding/https-terminate-to-https.handler.js +0 -254
- package/dist_ts/smartproxy/forwarding/index.d.ts +0 -16
- package/dist_ts/smartproxy/forwarding/index.js +0 -23
- package/dist_ts/smartproxy/types/forwarding.types.d.ts +0 -104
- package/dist_ts/smartproxy/types/forwarding.types.js +0 -50
- package/dist_ts/smartproxy.classes.networkproxy.d.ts +0 -31
- package/dist_ts/smartproxy.classes.networkproxy.js +0 -305
- package/dist_ts/smartproxy.classes.router.d.ts +0 -13
- package/dist_ts/smartproxy.classes.router.js +0 -33
- package/dist_ts/smartproxy.classes.sslredirect.d.ts +0 -8
- package/dist_ts/smartproxy.classes.sslredirect.js +0 -28
- package/dist_ts/smartproxy.helpers.certificates.d.ts +0 -5
- package/dist_ts/smartproxy.helpers.certificates.js +0 -23
- package/dist_ts/smartproxy.plugins.d.ts +0 -18
- package/dist_ts/smartproxy.plugins.js +0 -23
- package/dist_ts/smartproxy.portproxy.d.ts +0 -26
- package/dist_ts/smartproxy.portproxy.js +0 -295
- package/ts/http/index.ts +0 -16
- package/ts/http/models/http-types.ts +0 -108
- package/ts/http/redirects/index.ts +0 -3
- package/ts/proxies/network-proxy/models/index.ts +0 -4
- package/ts/redirect/classes.redirect.ts +0 -295
- /package/dist_ts/proxies/{network-proxy → http-proxy}/context-creator.d.ts +0 -0
- /package/dist_ts/proxies/{network-proxy → http-proxy}/function-cache.d.ts +0 -0
- /package/dist_ts/proxies/{network-proxy → http-proxy}/http-request-handler.d.ts +0 -0
- /package/dist_ts/proxies/{network-proxy → http-proxy}/http2-request-handler.d.ts +0 -0
- /package/dist_ts/proxies/{network-proxy → http-proxy}/security-manager.d.ts +0 -0
- /package/ts/proxies/{network-proxy → http-proxy}/context-creator.ts +0 -0
- /package/ts/proxies/{network-proxy → http-proxy}/function-cache.ts +0 -0
- /package/ts/proxies/{network-proxy → http-proxy}/http-request-handler.ts +0 -0
- /package/ts/proxies/{network-proxy → http-proxy}/http2-request-handler.ts +0 -0
- /package/ts/proxies/{network-proxy → http-proxy}/security-manager.ts +0 -0
- /package/ts/{http → routing}/router/index.ts +0 -0
|
@@ -1,1053 +0,0 @@
|
|
|
1
|
-
import { Buffer } from 'buffer';
|
|
2
|
-
/**
|
|
3
|
-
* SNI (Server Name Indication) handler for TLS connections.
|
|
4
|
-
* Provides robust extraction of SNI values from TLS ClientHello messages
|
|
5
|
-
* with support for fragmented packets, TLS 1.3 resumption, Chrome-specific
|
|
6
|
-
* connection behaviors, and tab hibernation/reactivation scenarios.
|
|
7
|
-
*/
|
|
8
|
-
export class SniHandler {
|
|
9
|
-
// TLS record types and constants
|
|
10
|
-
static { this.TLS_HANDSHAKE_RECORD_TYPE = 22; }
|
|
11
|
-
static { this.TLS_APPLICATION_DATA_TYPE = 23; } // TLS Application Data record type
|
|
12
|
-
static { this.TLS_CLIENT_HELLO_HANDSHAKE_TYPE = 1; }
|
|
13
|
-
static { this.TLS_SNI_EXTENSION_TYPE = 0x0000; }
|
|
14
|
-
static { this.TLS_SESSION_TICKET_EXTENSION_TYPE = 0x0023; }
|
|
15
|
-
static { this.TLS_SNI_HOST_NAME_TYPE = 0; }
|
|
16
|
-
static { this.TLS_PSK_EXTENSION_TYPE = 0x0029; } // Pre-Shared Key extension type for TLS 1.3
|
|
17
|
-
static { this.TLS_PSK_KE_MODES_EXTENSION_TYPE = 0x002d; } // PSK Key Exchange Modes
|
|
18
|
-
static { this.TLS_EARLY_DATA_EXTENSION_TYPE = 0x002a; } // Early Data (0-RTT) extension
|
|
19
|
-
// Buffer for handling fragmented ClientHello messages
|
|
20
|
-
static { this.fragmentedBuffers = new Map(); }
|
|
21
|
-
static { this.fragmentTimeout = 1000; } // ms to wait for fragments before cleanup
|
|
22
|
-
/**
|
|
23
|
-
* Checks if a buffer contains a TLS handshake message (record type 22)
|
|
24
|
-
* @param buffer - The buffer to check
|
|
25
|
-
* @returns true if the buffer starts with a TLS handshake record type
|
|
26
|
-
*/
|
|
27
|
-
static isTlsHandshake(buffer) {
|
|
28
|
-
return buffer.length > 0 && buffer[0] === this.TLS_HANDSHAKE_RECORD_TYPE;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Checks if a buffer contains TLS application data (record type 23)
|
|
32
|
-
* @param buffer - The buffer to check
|
|
33
|
-
* @returns true if the buffer starts with a TLS application data record type
|
|
34
|
-
*/
|
|
35
|
-
static isTlsApplicationData(buffer) {
|
|
36
|
-
return buffer.length > 0 && buffer[0] === this.TLS_APPLICATION_DATA_TYPE;
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Creates a connection ID based on source/destination information
|
|
40
|
-
* Used to track fragmented ClientHello messages across multiple packets
|
|
41
|
-
*
|
|
42
|
-
* @param connectionInfo - Object containing connection identifiers (IP/port)
|
|
43
|
-
* @returns A string ID for the connection
|
|
44
|
-
*/
|
|
45
|
-
static createConnectionId(connectionInfo) {
|
|
46
|
-
const { sourceIp, sourcePort, destIp, destPort } = connectionInfo;
|
|
47
|
-
return `${sourceIp}:${sourcePort}-${destIp}:${destPort}`;
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Handles potential fragmented ClientHello messages by buffering and reassembling
|
|
51
|
-
* TLS record fragments that might span multiple TCP packets.
|
|
52
|
-
*
|
|
53
|
-
* @param buffer - The current buffer fragment
|
|
54
|
-
* @param connectionId - Unique identifier for the connection
|
|
55
|
-
* @param enableLogging - Whether to enable logging
|
|
56
|
-
* @returns A complete buffer if reassembly is successful, or undefined if more fragments are needed
|
|
57
|
-
*/
|
|
58
|
-
static handleFragmentedClientHello(buffer, connectionId, enableLogging = false) {
|
|
59
|
-
const log = (message) => {
|
|
60
|
-
if (enableLogging) {
|
|
61
|
-
console.log(`[SNI Fragment] ${message}`);
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
// Check if we've seen this connection before
|
|
65
|
-
if (!this.fragmentedBuffers.has(connectionId)) {
|
|
66
|
-
// New connection, start with this buffer
|
|
67
|
-
this.fragmentedBuffers.set(connectionId, buffer);
|
|
68
|
-
// Set timeout to clean up if we don't get a complete ClientHello
|
|
69
|
-
setTimeout(() => {
|
|
70
|
-
if (this.fragmentedBuffers.has(connectionId)) {
|
|
71
|
-
this.fragmentedBuffers.delete(connectionId);
|
|
72
|
-
log(`Connection ${connectionId} timed out waiting for complete ClientHello`);
|
|
73
|
-
}
|
|
74
|
-
}, this.fragmentTimeout);
|
|
75
|
-
// Evaluate if this buffer already contains a complete ClientHello
|
|
76
|
-
try {
|
|
77
|
-
if (buffer.length >= 5) {
|
|
78
|
-
// Get the record length from TLS header
|
|
79
|
-
const recordLength = (buffer[3] << 8) + buffer[4] + 5; // +5 for the TLS record header itself
|
|
80
|
-
log(`Initial buffer size: ${buffer.length}, expected record length: ${recordLength}`);
|
|
81
|
-
// Check if this buffer already contains a complete TLS record
|
|
82
|
-
if (buffer.length >= recordLength) {
|
|
83
|
-
log(`Initial buffer contains complete ClientHello, length: ${buffer.length}`);
|
|
84
|
-
return buffer;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
log(`Initial buffer too small (${buffer.length} bytes), needs at least 5 bytes for TLS header`);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
catch (e) {
|
|
92
|
-
log(`Error checking initial buffer completeness: ${e}`);
|
|
93
|
-
}
|
|
94
|
-
log(`Started buffering connection ${connectionId}, initial size: ${buffer.length}`);
|
|
95
|
-
return undefined; // Need more fragments
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
// Existing connection, append this buffer
|
|
99
|
-
const existingBuffer = this.fragmentedBuffers.get(connectionId);
|
|
100
|
-
const newBuffer = Buffer.concat([existingBuffer, buffer]);
|
|
101
|
-
this.fragmentedBuffers.set(connectionId, newBuffer);
|
|
102
|
-
log(`Appended to buffer for ${connectionId}, new size: ${newBuffer.length}`);
|
|
103
|
-
// Check if we now have a complete ClientHello
|
|
104
|
-
try {
|
|
105
|
-
if (newBuffer.length >= 5) {
|
|
106
|
-
// Get the record length from TLS header
|
|
107
|
-
const recordLength = (newBuffer[3] << 8) + newBuffer[4] + 5; // +5 for the TLS record header itself
|
|
108
|
-
log(`Reassembled buffer size: ${newBuffer.length}, expected record length: ${recordLength}`);
|
|
109
|
-
// Check if we have a complete TLS record now
|
|
110
|
-
if (newBuffer.length >= recordLength) {
|
|
111
|
-
log(`Assembled complete ClientHello, length: ${newBuffer.length}, needed: ${recordLength}`);
|
|
112
|
-
// Extract the complete TLS record (might be followed by more data)
|
|
113
|
-
const completeRecord = newBuffer.slice(0, recordLength);
|
|
114
|
-
// Check if this record is indeed a ClientHello (type 1) at position 5
|
|
115
|
-
if (completeRecord.length > 5 &&
|
|
116
|
-
completeRecord[5] === this.TLS_CLIENT_HELLO_HANDSHAKE_TYPE) {
|
|
117
|
-
log(`Verified record is a ClientHello handshake message`);
|
|
118
|
-
// Complete message received, remove from tracking
|
|
119
|
-
this.fragmentedBuffers.delete(connectionId);
|
|
120
|
-
return completeRecord;
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
log(`Record is complete but not a ClientHello handshake, continuing to buffer`);
|
|
124
|
-
// This might be another TLS record type preceding the ClientHello
|
|
125
|
-
// Try checking for a ClientHello starting at the end of this record
|
|
126
|
-
if (newBuffer.length > recordLength + 5) {
|
|
127
|
-
const nextRecordType = newBuffer[recordLength];
|
|
128
|
-
log(`Next record type: ${nextRecordType} (looking for ${this.TLS_HANDSHAKE_RECORD_TYPE})`);
|
|
129
|
-
if (nextRecordType === this.TLS_HANDSHAKE_RECORD_TYPE) {
|
|
130
|
-
const handshakeType = newBuffer[recordLength + 5];
|
|
131
|
-
log(`Next handshake type: ${handshakeType} (looking for ${this.TLS_CLIENT_HELLO_HANDSHAKE_TYPE})`);
|
|
132
|
-
if (handshakeType === this.TLS_CLIENT_HELLO_HANDSHAKE_TYPE) {
|
|
133
|
-
// Found a ClientHello in the next record, return the entire buffer
|
|
134
|
-
log(`Found ClientHello in subsequent record, returning full buffer`);
|
|
135
|
-
this.fragmentedBuffers.delete(connectionId);
|
|
136
|
-
return newBuffer;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
catch (e) {
|
|
145
|
-
log(`Error checking reassembled buffer completeness: ${e}`);
|
|
146
|
-
}
|
|
147
|
-
return undefined; // Still need more fragments
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* Checks if a buffer contains a TLS ClientHello message
|
|
152
|
-
* @param buffer - The buffer to check
|
|
153
|
-
* @returns true if the buffer appears to be a ClientHello message
|
|
154
|
-
*/
|
|
155
|
-
static isClientHello(buffer) {
|
|
156
|
-
// Minimum ClientHello size (TLS record header + handshake header)
|
|
157
|
-
if (buffer.length < 9) {
|
|
158
|
-
return false;
|
|
159
|
-
}
|
|
160
|
-
// Check record type (must be TLS_HANDSHAKE_RECORD_TYPE)
|
|
161
|
-
if (buffer[0] !== this.TLS_HANDSHAKE_RECORD_TYPE) {
|
|
162
|
-
return false;
|
|
163
|
-
}
|
|
164
|
-
// Skip version and length in TLS record header (5 bytes total)
|
|
165
|
-
// Check handshake type at byte 5 (must be CLIENT_HELLO)
|
|
166
|
-
return buffer[5] === this.TLS_CLIENT_HELLO_HANDSHAKE_TYPE;
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* Checks if a ClientHello message contains session resumption indicators
|
|
170
|
-
* such as session tickets or PSK (Pre-Shared Key) extensions.
|
|
171
|
-
*
|
|
172
|
-
* @param buffer - The buffer containing a ClientHello message
|
|
173
|
-
* @param enableLogging - Whether to enable logging
|
|
174
|
-
* @returns Object containing details about session resumption and SNI presence
|
|
175
|
-
*/
|
|
176
|
-
static hasSessionResumption(buffer, enableLogging = false) {
|
|
177
|
-
const log = (message) => {
|
|
178
|
-
if (enableLogging) {
|
|
179
|
-
console.log(`[Session Resumption] ${message}`);
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
if (!this.isClientHello(buffer)) {
|
|
183
|
-
return { isResumption: false, hasSNI: false };
|
|
184
|
-
}
|
|
185
|
-
try {
|
|
186
|
-
// Check for session ID presence first
|
|
187
|
-
let pos = 5 + 1 + 3 + 2; // Position after handshake type, length and client version
|
|
188
|
-
pos += 32; // Skip client random
|
|
189
|
-
if (pos + 1 > buffer.length)
|
|
190
|
-
return { isResumption: false, hasSNI: false };
|
|
191
|
-
const sessionIdLength = buffer[pos];
|
|
192
|
-
let hasNonEmptySessionId = sessionIdLength > 0;
|
|
193
|
-
if (hasNonEmptySessionId) {
|
|
194
|
-
log(`Detected non-empty session ID (length: ${sessionIdLength})`);
|
|
195
|
-
}
|
|
196
|
-
// Continue to check for extensions
|
|
197
|
-
pos += 1 + sessionIdLength;
|
|
198
|
-
// Skip cipher suites
|
|
199
|
-
if (pos + 2 > buffer.length)
|
|
200
|
-
return { isResumption: false, hasSNI: false };
|
|
201
|
-
const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
202
|
-
pos += 2 + cipherSuitesLength;
|
|
203
|
-
// Skip compression methods
|
|
204
|
-
if (pos + 1 > buffer.length)
|
|
205
|
-
return { isResumption: false, hasSNI: false };
|
|
206
|
-
const compressionMethodsLength = buffer[pos];
|
|
207
|
-
pos += 1 + compressionMethodsLength;
|
|
208
|
-
// Check for extensions
|
|
209
|
-
if (pos + 2 > buffer.length)
|
|
210
|
-
return { isResumption: false, hasSNI: false };
|
|
211
|
-
// Look for session resumption extensions
|
|
212
|
-
const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
213
|
-
pos += 2;
|
|
214
|
-
// Extensions end position
|
|
215
|
-
const extensionsEnd = pos + extensionsLength;
|
|
216
|
-
if (extensionsEnd > buffer.length)
|
|
217
|
-
return { isResumption: false, hasSNI: false };
|
|
218
|
-
// Track resumption indicators
|
|
219
|
-
let hasSessionTicket = false;
|
|
220
|
-
let hasPSK = false;
|
|
221
|
-
let hasEarlyData = false;
|
|
222
|
-
// Iterate through extensions
|
|
223
|
-
while (pos + 4 <= extensionsEnd) {
|
|
224
|
-
const extensionType = (buffer[pos] << 8) + buffer[pos + 1];
|
|
225
|
-
pos += 2;
|
|
226
|
-
const extensionLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
227
|
-
pos += 2;
|
|
228
|
-
if (extensionType === this.TLS_SESSION_TICKET_EXTENSION_TYPE) {
|
|
229
|
-
log('Found session ticket extension');
|
|
230
|
-
hasSessionTicket = true;
|
|
231
|
-
// Check if session ticket has non-zero length (active ticket)
|
|
232
|
-
if (extensionLength > 0) {
|
|
233
|
-
log(`Session ticket has length ${extensionLength} - active ticket present`);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
else if (extensionType === this.TLS_PSK_EXTENSION_TYPE) {
|
|
237
|
-
log('Found PSK extension (TLS 1.3 resumption mechanism)');
|
|
238
|
-
hasPSK = true;
|
|
239
|
-
}
|
|
240
|
-
else if (extensionType === this.TLS_EARLY_DATA_EXTENSION_TYPE) {
|
|
241
|
-
log('Found Early Data extension (TLS 1.3 0-RTT)');
|
|
242
|
-
hasEarlyData = true;
|
|
243
|
-
}
|
|
244
|
-
// Skip extension data
|
|
245
|
-
pos += extensionLength;
|
|
246
|
-
}
|
|
247
|
-
// Check if SNI is included
|
|
248
|
-
let hasSNI = false;
|
|
249
|
-
// Reset position and scan again for SNI extension
|
|
250
|
-
pos = 5 + 1 + 3 + 2; // Reset to after handshake type, length and client version
|
|
251
|
-
pos += 32; // Skip client random
|
|
252
|
-
if (pos + 1 <= buffer.length) {
|
|
253
|
-
const sessionIdLength = buffer[pos];
|
|
254
|
-
pos += 1 + sessionIdLength;
|
|
255
|
-
// Skip cipher suites
|
|
256
|
-
if (pos + 2 <= buffer.length) {
|
|
257
|
-
const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
258
|
-
pos += 2 + cipherSuitesLength;
|
|
259
|
-
// Skip compression methods
|
|
260
|
-
if (pos + 1 <= buffer.length) {
|
|
261
|
-
const compressionMethodsLength = buffer[pos];
|
|
262
|
-
pos += 1 + compressionMethodsLength;
|
|
263
|
-
// Check for extensions
|
|
264
|
-
if (pos + 2 <= buffer.length) {
|
|
265
|
-
const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
266
|
-
pos += 2;
|
|
267
|
-
// Extensions end position
|
|
268
|
-
const extensionsEnd = pos + extensionsLength;
|
|
269
|
-
if (extensionsEnd <= buffer.length) {
|
|
270
|
-
// Scan for SNI extension
|
|
271
|
-
while (pos + 4 <= extensionsEnd) {
|
|
272
|
-
const extensionType = (buffer[pos] << 8) + buffer[pos + 1];
|
|
273
|
-
pos += 2;
|
|
274
|
-
const extensionLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
275
|
-
pos += 2;
|
|
276
|
-
if (extensionType === this.TLS_SNI_EXTENSION_TYPE) {
|
|
277
|
-
// Check that the SNI extension actually has content
|
|
278
|
-
if (extensionLength > 0) {
|
|
279
|
-
hasSNI = true;
|
|
280
|
-
// Try to extract the actual SNI value for logging
|
|
281
|
-
try {
|
|
282
|
-
// Skip to server_name_list_length (2 bytes)
|
|
283
|
-
const tempPos = pos;
|
|
284
|
-
if (tempPos + 2 <= extensionsEnd) {
|
|
285
|
-
const nameListLength = (buffer[tempPos] << 8) + buffer[tempPos + 1];
|
|
286
|
-
// Skip server_name_list_length (2 bytes)
|
|
287
|
-
if (tempPos + 2 + 1 <= extensionsEnd) {
|
|
288
|
-
// Check name_type (should be 0 for hostname)
|
|
289
|
-
if (buffer[tempPos + 2] === 0) {
|
|
290
|
-
// Skip name_type (1 byte)
|
|
291
|
-
if (tempPos + 3 + 2 <= extensionsEnd) {
|
|
292
|
-
// Get name_length (2 bytes)
|
|
293
|
-
const nameLength = (buffer[tempPos + 3] << 8) + buffer[tempPos + 4];
|
|
294
|
-
// Extract the hostname
|
|
295
|
-
if (tempPos + 5 + nameLength <= extensionsEnd) {
|
|
296
|
-
const hostname = buffer
|
|
297
|
-
.slice(tempPos + 5, tempPos + 5 + nameLength)
|
|
298
|
-
.toString('utf8');
|
|
299
|
-
log(`Found SNI extension with server_name: ${hostname}`);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
catch (e) {
|
|
307
|
-
log(`Error extracting SNI value: ${e}`);
|
|
308
|
-
log('Found SNI extension with length: ' + extensionLength);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
else {
|
|
312
|
-
log('Found empty SNI extension, treating as no SNI');
|
|
313
|
-
}
|
|
314
|
-
break;
|
|
315
|
-
}
|
|
316
|
-
// Skip extension data
|
|
317
|
-
pos += extensionLength;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
// Consider it a resumption if any resumption mechanism is present
|
|
325
|
-
const isResumption = hasSessionTicket || hasPSK || hasEarlyData || (hasNonEmptySessionId && !hasPSK); // Legacy resumption
|
|
326
|
-
if (isResumption) {
|
|
327
|
-
log('Session resumption detected: ' +
|
|
328
|
-
(hasSessionTicket ? 'session ticket, ' : '') +
|
|
329
|
-
(hasPSK ? 'PSK, ' : '') +
|
|
330
|
-
(hasEarlyData ? 'early data, ' : '') +
|
|
331
|
-
(hasNonEmptySessionId ? 'session ID' : '') +
|
|
332
|
-
(hasSNI ? ', with SNI' : ', without SNI'));
|
|
333
|
-
}
|
|
334
|
-
// Return an object with both flags
|
|
335
|
-
// For clarity: connections should be blocked if they have session resumption without SNI
|
|
336
|
-
if (isResumption) {
|
|
337
|
-
log(`Resumption summary - hasSNI: ${hasSNI ? 'yes' : 'no'}, resumption type: ${hasSessionTicket ? 'session ticket, ' : ''}${hasPSK ? 'PSK, ' : ''}${hasEarlyData ? 'early data, ' : ''}${hasNonEmptySessionId ? 'session ID' : ''}`);
|
|
338
|
-
}
|
|
339
|
-
return {
|
|
340
|
-
isResumption,
|
|
341
|
-
hasSNI,
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
catch (error) {
|
|
345
|
-
log(`Error checking for session resumption: ${error}`);
|
|
346
|
-
return { isResumption: false, hasSNI: false };
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
/**
|
|
350
|
-
* Detects characteristics of a tab reactivation TLS handshake
|
|
351
|
-
* These often have specific patterns in Chrome and other browsers
|
|
352
|
-
*
|
|
353
|
-
* @param buffer - The buffer containing a ClientHello message
|
|
354
|
-
* @param enableLogging - Whether to enable logging
|
|
355
|
-
* @returns true if this appears to be a tab reactivation handshake
|
|
356
|
-
*/
|
|
357
|
-
static isTabReactivationHandshake(buffer, enableLogging = false) {
|
|
358
|
-
const log = (message) => {
|
|
359
|
-
if (enableLogging) {
|
|
360
|
-
console.log(`[Tab Reactivation] ${message}`);
|
|
361
|
-
}
|
|
362
|
-
};
|
|
363
|
-
if (!this.isClientHello(buffer)) {
|
|
364
|
-
return false;
|
|
365
|
-
}
|
|
366
|
-
try {
|
|
367
|
-
// Check for session ID presence (tab reactivation often has a session ID)
|
|
368
|
-
let pos = 5 + 1 + 3 + 2; // Position after handshake type, length and client version
|
|
369
|
-
pos += 32; // Skip client random
|
|
370
|
-
if (pos + 1 > buffer.length)
|
|
371
|
-
return false;
|
|
372
|
-
const sessionIdLength = buffer[pos];
|
|
373
|
-
// Non-empty session ID is a good indicator
|
|
374
|
-
if (sessionIdLength > 0) {
|
|
375
|
-
log(`Detected non-empty session ID (length: ${sessionIdLength})`);
|
|
376
|
-
// Skip to extensions
|
|
377
|
-
pos += 1 + sessionIdLength;
|
|
378
|
-
// Skip cipher suites
|
|
379
|
-
if (pos + 2 > buffer.length)
|
|
380
|
-
return false;
|
|
381
|
-
const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
382
|
-
pos += 2 + cipherSuitesLength;
|
|
383
|
-
// Skip compression methods
|
|
384
|
-
if (pos + 1 > buffer.length)
|
|
385
|
-
return false;
|
|
386
|
-
const compressionMethodsLength = buffer[pos];
|
|
387
|
-
pos += 1 + compressionMethodsLength;
|
|
388
|
-
// Check for extensions
|
|
389
|
-
if (pos + 2 > buffer.length)
|
|
390
|
-
return false;
|
|
391
|
-
// Look for specific extensions that indicate tab reactivation
|
|
392
|
-
const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
393
|
-
pos += 2;
|
|
394
|
-
// Extensions end position
|
|
395
|
-
const extensionsEnd = pos + extensionsLength;
|
|
396
|
-
if (extensionsEnd > buffer.length)
|
|
397
|
-
return false;
|
|
398
|
-
// Tab reactivation often has session tickets but no SNI
|
|
399
|
-
let hasSessionTicket = false;
|
|
400
|
-
let hasSNI = false;
|
|
401
|
-
let hasPSK = false;
|
|
402
|
-
// Iterate through extensions
|
|
403
|
-
while (pos + 4 <= extensionsEnd) {
|
|
404
|
-
const extensionType = (buffer[pos] << 8) + buffer[pos + 1];
|
|
405
|
-
pos += 2;
|
|
406
|
-
const extensionLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
407
|
-
pos += 2;
|
|
408
|
-
if (extensionType === this.TLS_SESSION_TICKET_EXTENSION_TYPE) {
|
|
409
|
-
hasSessionTicket = true;
|
|
410
|
-
}
|
|
411
|
-
else if (extensionType === this.TLS_SNI_EXTENSION_TYPE) {
|
|
412
|
-
hasSNI = true;
|
|
413
|
-
}
|
|
414
|
-
else if (extensionType === this.TLS_PSK_EXTENSION_TYPE) {
|
|
415
|
-
hasPSK = true;
|
|
416
|
-
}
|
|
417
|
-
// Skip extension data
|
|
418
|
-
pos += extensionLength;
|
|
419
|
-
}
|
|
420
|
-
// Pattern for tab reactivation: session identifier + (ticket or PSK) but no SNI
|
|
421
|
-
if ((hasSessionTicket || hasPSK) && !hasSNI) {
|
|
422
|
-
log('Detected tab reactivation pattern: session resumption without SNI');
|
|
423
|
-
return true;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
catch (error) {
|
|
428
|
-
log(`Error checking for tab reactivation: ${error}`);
|
|
429
|
-
}
|
|
430
|
-
return false;
|
|
431
|
-
}
|
|
432
|
-
/**
|
|
433
|
-
* Extracts the SNI (Server Name Indication) from a TLS ClientHello message.
|
|
434
|
-
* Implements robust parsing with support for session resumption edge cases.
|
|
435
|
-
*
|
|
436
|
-
* @param buffer - The buffer containing the TLS ClientHello message
|
|
437
|
-
* @param enableLogging - Whether to enable detailed debug logging
|
|
438
|
-
* @returns The extracted server name or undefined if not found
|
|
439
|
-
*/
|
|
440
|
-
static extractSNI(buffer, enableLogging = false) {
|
|
441
|
-
// Logging helper
|
|
442
|
-
const log = (message) => {
|
|
443
|
-
if (enableLogging) {
|
|
444
|
-
console.log(`[SNI Extraction] ${message}`);
|
|
445
|
-
}
|
|
446
|
-
};
|
|
447
|
-
try {
|
|
448
|
-
// Buffer must be at least 5 bytes (TLS record header)
|
|
449
|
-
if (buffer.length < 5) {
|
|
450
|
-
log('Buffer too small for TLS record header');
|
|
451
|
-
return undefined;
|
|
452
|
-
}
|
|
453
|
-
// Check record type (must be TLS_HANDSHAKE_RECORD_TYPE = 22)
|
|
454
|
-
if (buffer[0] !== this.TLS_HANDSHAKE_RECORD_TYPE) {
|
|
455
|
-
log(`Not a TLS handshake record: ${buffer[0]}`);
|
|
456
|
-
return undefined;
|
|
457
|
-
}
|
|
458
|
-
// Check TLS version
|
|
459
|
-
const majorVersion = buffer[1];
|
|
460
|
-
const minorVersion = buffer[2];
|
|
461
|
-
log(`TLS version: ${majorVersion}.${minorVersion}`);
|
|
462
|
-
// Parse record length (bytes 3-4, big-endian)
|
|
463
|
-
const recordLength = (buffer[3] << 8) + buffer[4];
|
|
464
|
-
log(`Record length: ${recordLength}`);
|
|
465
|
-
// Validate record length against buffer size
|
|
466
|
-
if (buffer.length < recordLength + 5) {
|
|
467
|
-
log('Buffer smaller than expected record length');
|
|
468
|
-
return undefined;
|
|
469
|
-
}
|
|
470
|
-
// Start of handshake message in the buffer
|
|
471
|
-
let pos = 5;
|
|
472
|
-
// Check handshake type (must be CLIENT_HELLO = 1)
|
|
473
|
-
if (buffer[pos] !== this.TLS_CLIENT_HELLO_HANDSHAKE_TYPE) {
|
|
474
|
-
log(`Not a ClientHello message: ${buffer[pos]}`);
|
|
475
|
-
return undefined;
|
|
476
|
-
}
|
|
477
|
-
// Skip handshake type (1 byte)
|
|
478
|
-
pos += 1;
|
|
479
|
-
// Parse handshake length (3 bytes, big-endian)
|
|
480
|
-
const handshakeLength = (buffer[pos] << 16) + (buffer[pos + 1] << 8) + buffer[pos + 2];
|
|
481
|
-
log(`Handshake length: ${handshakeLength}`);
|
|
482
|
-
// Skip handshake length (3 bytes)
|
|
483
|
-
pos += 3;
|
|
484
|
-
// Check client version (2 bytes)
|
|
485
|
-
const clientMajorVersion = buffer[pos];
|
|
486
|
-
const clientMinorVersion = buffer[pos + 1];
|
|
487
|
-
log(`Client version: ${clientMajorVersion}.${clientMinorVersion}`);
|
|
488
|
-
// Skip client version (2 bytes)
|
|
489
|
-
pos += 2;
|
|
490
|
-
// Skip client random (32 bytes)
|
|
491
|
-
pos += 32;
|
|
492
|
-
// Parse session ID
|
|
493
|
-
if (pos + 1 > buffer.length) {
|
|
494
|
-
log('Buffer too small for session ID length');
|
|
495
|
-
return undefined;
|
|
496
|
-
}
|
|
497
|
-
const sessionIdLength = buffer[pos];
|
|
498
|
-
log(`Session ID length: ${sessionIdLength}`);
|
|
499
|
-
// Skip session ID length (1 byte) and session ID
|
|
500
|
-
pos += 1 + sessionIdLength;
|
|
501
|
-
// Check if we have enough bytes left
|
|
502
|
-
if (pos + 2 > buffer.length) {
|
|
503
|
-
log('Buffer too small for cipher suites length');
|
|
504
|
-
return undefined;
|
|
505
|
-
}
|
|
506
|
-
// Parse cipher suites length (2 bytes, big-endian)
|
|
507
|
-
const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
508
|
-
log(`Cipher suites length: ${cipherSuitesLength}`);
|
|
509
|
-
// Skip cipher suites length (2 bytes) and cipher suites
|
|
510
|
-
pos += 2 + cipherSuitesLength;
|
|
511
|
-
// Check if we have enough bytes left
|
|
512
|
-
if (pos + 1 > buffer.length) {
|
|
513
|
-
log('Buffer too small for compression methods length');
|
|
514
|
-
return undefined;
|
|
515
|
-
}
|
|
516
|
-
// Parse compression methods length (1 byte)
|
|
517
|
-
const compressionMethodsLength = buffer[pos];
|
|
518
|
-
log(`Compression methods length: ${compressionMethodsLength}`);
|
|
519
|
-
// Skip compression methods length (1 byte) and compression methods
|
|
520
|
-
pos += 1 + compressionMethodsLength;
|
|
521
|
-
// Check if we have enough bytes for extensions length
|
|
522
|
-
if (pos + 2 > buffer.length) {
|
|
523
|
-
log('No extensions present or buffer too small');
|
|
524
|
-
return undefined;
|
|
525
|
-
}
|
|
526
|
-
// Parse extensions length (2 bytes, big-endian)
|
|
527
|
-
const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
528
|
-
log(`Extensions length: ${extensionsLength}`);
|
|
529
|
-
// Skip extensions length (2 bytes)
|
|
530
|
-
pos += 2;
|
|
531
|
-
// Extensions end position
|
|
532
|
-
const extensionsEnd = pos + extensionsLength;
|
|
533
|
-
// Check if extensions length is valid
|
|
534
|
-
if (extensionsEnd > buffer.length) {
|
|
535
|
-
log('Extensions length exceeds buffer size');
|
|
536
|
-
return undefined;
|
|
537
|
-
}
|
|
538
|
-
// Track if we found session tickets (for improved resumption handling)
|
|
539
|
-
let hasSessionTicket = false;
|
|
540
|
-
let hasPskExtension = false;
|
|
541
|
-
// Iterate through extensions
|
|
542
|
-
while (pos + 4 <= extensionsEnd) {
|
|
543
|
-
// Parse extension type (2 bytes, big-endian)
|
|
544
|
-
const extensionType = (buffer[pos] << 8) + buffer[pos + 1];
|
|
545
|
-
log(`Extension type: 0x${extensionType.toString(16).padStart(4, '0')}`);
|
|
546
|
-
// Skip extension type (2 bytes)
|
|
547
|
-
pos += 2;
|
|
548
|
-
// Parse extension length (2 bytes, big-endian)
|
|
549
|
-
const extensionLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
550
|
-
log(`Extension length: ${extensionLength}`);
|
|
551
|
-
// Skip extension length (2 bytes)
|
|
552
|
-
pos += 2;
|
|
553
|
-
// Check if this is the SNI extension
|
|
554
|
-
if (extensionType === this.TLS_SNI_EXTENSION_TYPE) {
|
|
555
|
-
log('Found SNI extension');
|
|
556
|
-
// Ensure we have enough bytes for the server name list
|
|
557
|
-
if (pos + 2 > extensionsEnd) {
|
|
558
|
-
log('Extension too small for server name list length');
|
|
559
|
-
pos += extensionLength; // Skip this extension
|
|
560
|
-
continue;
|
|
561
|
-
}
|
|
562
|
-
// Parse server name list length (2 bytes, big-endian)
|
|
563
|
-
const serverNameListLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
564
|
-
log(`Server name list length: ${serverNameListLength}`);
|
|
565
|
-
// Skip server name list length (2 bytes)
|
|
566
|
-
pos += 2;
|
|
567
|
-
// Ensure server name list length is valid
|
|
568
|
-
if (pos + serverNameListLength > extensionsEnd) {
|
|
569
|
-
log('Server name list length exceeds extension size');
|
|
570
|
-
break; // Exit the loop, extension parsing is broken
|
|
571
|
-
}
|
|
572
|
-
// End position of server name list
|
|
573
|
-
const serverNameListEnd = pos + serverNameListLength;
|
|
574
|
-
// Iterate through server names
|
|
575
|
-
while (pos + 3 <= serverNameListEnd) {
|
|
576
|
-
// Check name type (must be HOST_NAME_TYPE = 0 for hostname)
|
|
577
|
-
const nameType = buffer[pos];
|
|
578
|
-
log(`Name type: ${nameType}`);
|
|
579
|
-
if (nameType !== this.TLS_SNI_HOST_NAME_TYPE) {
|
|
580
|
-
log(`Unsupported name type: ${nameType}`);
|
|
581
|
-
pos += 1; // Skip name type (1 byte)
|
|
582
|
-
// Skip name length (2 bytes) and name data
|
|
583
|
-
if (pos + 2 <= serverNameListEnd) {
|
|
584
|
-
const nameLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
585
|
-
pos += 2 + nameLength;
|
|
586
|
-
}
|
|
587
|
-
else {
|
|
588
|
-
log('Invalid server name entry');
|
|
589
|
-
break;
|
|
590
|
-
}
|
|
591
|
-
continue;
|
|
592
|
-
}
|
|
593
|
-
// Skip name type (1 byte)
|
|
594
|
-
pos += 1;
|
|
595
|
-
// Ensure we have enough bytes for name length
|
|
596
|
-
if (pos + 2 > serverNameListEnd) {
|
|
597
|
-
log('Server name entry too small for name length');
|
|
598
|
-
break;
|
|
599
|
-
}
|
|
600
|
-
// Parse name length (2 bytes, big-endian)
|
|
601
|
-
const nameLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
602
|
-
log(`Name length: ${nameLength}`);
|
|
603
|
-
// Skip name length (2 bytes)
|
|
604
|
-
pos += 2;
|
|
605
|
-
// Ensure we have enough bytes for the name
|
|
606
|
-
if (pos + nameLength > serverNameListEnd) {
|
|
607
|
-
log('Name length exceeds server name list size');
|
|
608
|
-
break;
|
|
609
|
-
}
|
|
610
|
-
// Extract server name (hostname)
|
|
611
|
-
const serverName = buffer.slice(pos, pos + nameLength).toString('utf8');
|
|
612
|
-
log(`Extracted server name: ${serverName}`);
|
|
613
|
-
return serverName;
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
else if (extensionType === this.TLS_SESSION_TICKET_EXTENSION_TYPE) {
|
|
617
|
-
// If we encounter a session ticket extension, mark it for later
|
|
618
|
-
log('Found session ticket extension');
|
|
619
|
-
hasSessionTicket = true;
|
|
620
|
-
pos += extensionLength; // Skip this extension
|
|
621
|
-
}
|
|
622
|
-
else if (extensionType === this.TLS_PSK_EXTENSION_TYPE) {
|
|
623
|
-
// TLS 1.3 PSK extension - mark for resumption support
|
|
624
|
-
log('Found PSK extension (TLS 1.3 resumption indicator)');
|
|
625
|
-
hasPskExtension = true;
|
|
626
|
-
// We'll skip the extension here and process it separately if needed
|
|
627
|
-
pos += extensionLength;
|
|
628
|
-
}
|
|
629
|
-
else {
|
|
630
|
-
// Skip this extension
|
|
631
|
-
pos += extensionLength;
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
// Log if we found session resumption indicators but no SNI
|
|
635
|
-
if (hasSessionTicket || hasPskExtension) {
|
|
636
|
-
log('Session resumption indicators present but no SNI found');
|
|
637
|
-
}
|
|
638
|
-
log('No SNI extension found in ClientHello');
|
|
639
|
-
return undefined;
|
|
640
|
-
}
|
|
641
|
-
catch (error) {
|
|
642
|
-
log(`Error parsing SNI: ${error instanceof Error ? error.message : String(error)}`);
|
|
643
|
-
return undefined;
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
/**
|
|
647
|
-
* Attempts to extract SNI from the PSK extension in a TLS 1.3 ClientHello.
|
|
648
|
-
*
|
|
649
|
-
* In TLS 1.3, when a client attempts to resume a session, it may include
|
|
650
|
-
* the server name in the PSK identity hint rather than in the SNI extension.
|
|
651
|
-
*
|
|
652
|
-
* @param buffer - The buffer containing the TLS ClientHello message
|
|
653
|
-
* @param enableLogging - Whether to enable detailed debug logging
|
|
654
|
-
* @returns The extracted server name or undefined if not found
|
|
655
|
-
*/
|
|
656
|
-
static extractSNIFromPSKExtension(buffer, enableLogging = false) {
|
|
657
|
-
const log = (message) => {
|
|
658
|
-
if (enableLogging) {
|
|
659
|
-
console.log(`[PSK-SNI Extraction] ${message}`);
|
|
660
|
-
}
|
|
661
|
-
};
|
|
662
|
-
try {
|
|
663
|
-
// Ensure this is a ClientHello
|
|
664
|
-
if (!this.isClientHello(buffer)) {
|
|
665
|
-
log('Not a ClientHello message');
|
|
666
|
-
return undefined;
|
|
667
|
-
}
|
|
668
|
-
// Find the start position of extensions
|
|
669
|
-
let pos = 5; // Start after record header
|
|
670
|
-
// Skip handshake type (1 byte)
|
|
671
|
-
pos += 1;
|
|
672
|
-
// Skip handshake length (3 bytes)
|
|
673
|
-
pos += 3;
|
|
674
|
-
// Skip client version (2 bytes)
|
|
675
|
-
pos += 2;
|
|
676
|
-
// Skip client random (32 bytes)
|
|
677
|
-
pos += 32;
|
|
678
|
-
// Skip session ID
|
|
679
|
-
if (pos + 1 > buffer.length)
|
|
680
|
-
return undefined;
|
|
681
|
-
const sessionIdLength = buffer[pos];
|
|
682
|
-
pos += 1 + sessionIdLength;
|
|
683
|
-
// Skip cipher suites
|
|
684
|
-
if (pos + 2 > buffer.length)
|
|
685
|
-
return undefined;
|
|
686
|
-
const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
687
|
-
pos += 2 + cipherSuitesLength;
|
|
688
|
-
// Skip compression methods
|
|
689
|
-
if (pos + 1 > buffer.length)
|
|
690
|
-
return undefined;
|
|
691
|
-
const compressionMethodsLength = buffer[pos];
|
|
692
|
-
pos += 1 + compressionMethodsLength;
|
|
693
|
-
// Check if we have extensions
|
|
694
|
-
if (pos + 2 > buffer.length) {
|
|
695
|
-
log('No extensions present');
|
|
696
|
-
return undefined;
|
|
697
|
-
}
|
|
698
|
-
// Get extensions length
|
|
699
|
-
const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
700
|
-
pos += 2;
|
|
701
|
-
// Extensions end position
|
|
702
|
-
const extensionsEnd = pos + extensionsLength;
|
|
703
|
-
if (extensionsEnd > buffer.length)
|
|
704
|
-
return undefined;
|
|
705
|
-
// Look for PSK extension
|
|
706
|
-
while (pos + 4 <= extensionsEnd) {
|
|
707
|
-
const extensionType = (buffer[pos] << 8) + buffer[pos + 1];
|
|
708
|
-
pos += 2;
|
|
709
|
-
const extensionLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
710
|
-
pos += 2;
|
|
711
|
-
if (extensionType === this.TLS_PSK_EXTENSION_TYPE) {
|
|
712
|
-
log('Found PSK extension');
|
|
713
|
-
// PSK extension structure:
|
|
714
|
-
// 2 bytes: identities list length
|
|
715
|
-
if (pos + 2 > extensionsEnd)
|
|
716
|
-
break;
|
|
717
|
-
const identitiesLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
718
|
-
pos += 2;
|
|
719
|
-
// End of identities list
|
|
720
|
-
const identitiesEnd = pos + identitiesLength;
|
|
721
|
-
if (identitiesEnd > extensionsEnd)
|
|
722
|
-
break;
|
|
723
|
-
// Process each PSK identity
|
|
724
|
-
while (pos + 2 <= identitiesEnd) {
|
|
725
|
-
// Identity length (2 bytes)
|
|
726
|
-
if (pos + 2 > identitiesEnd)
|
|
727
|
-
break;
|
|
728
|
-
const identityLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
729
|
-
pos += 2;
|
|
730
|
-
if (pos + identityLength > identitiesEnd)
|
|
731
|
-
break;
|
|
732
|
-
// Try to extract hostname from identity
|
|
733
|
-
// Chrome often embeds the hostname in the PSK identity
|
|
734
|
-
// This is a heuristic as there's no standard format
|
|
735
|
-
if (identityLength > 0) {
|
|
736
|
-
const identity = buffer.slice(pos, pos + identityLength);
|
|
737
|
-
// Skip identity bytes
|
|
738
|
-
pos += identityLength;
|
|
739
|
-
// Skip obfuscated ticket age (4 bytes)
|
|
740
|
-
if (pos + 4 <= identitiesEnd) {
|
|
741
|
-
pos += 4;
|
|
742
|
-
}
|
|
743
|
-
else {
|
|
744
|
-
break;
|
|
745
|
-
}
|
|
746
|
-
// Try to parse the identity as UTF-8
|
|
747
|
-
try {
|
|
748
|
-
const identityStr = identity.toString('utf8');
|
|
749
|
-
log(`PSK identity: ${identityStr}`);
|
|
750
|
-
// Check if the identity contains hostname hints
|
|
751
|
-
// Chrome often embeds the hostname in a known format
|
|
752
|
-
// Try to extract using common patterns
|
|
753
|
-
// Pattern 1: Look for domain name pattern
|
|
754
|
-
const domainPattern = /([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?/i;
|
|
755
|
-
const domainMatch = identityStr.match(domainPattern);
|
|
756
|
-
if (domainMatch && domainMatch[0]) {
|
|
757
|
-
log(`Found domain in PSK identity: ${domainMatch[0]}`);
|
|
758
|
-
return domainMatch[0];
|
|
759
|
-
}
|
|
760
|
-
// Pattern 2: Chrome sometimes uses a specific format with delimiters
|
|
761
|
-
// This is a heuristic approach since the format isn't standardized
|
|
762
|
-
const parts = identityStr.split('|');
|
|
763
|
-
if (parts.length > 1) {
|
|
764
|
-
for (const part of parts) {
|
|
765
|
-
if (part.includes('.') && !part.includes('/')) {
|
|
766
|
-
const possibleDomain = part.trim();
|
|
767
|
-
if (/^[a-z0-9.-]+$/i.test(possibleDomain)) {
|
|
768
|
-
log(`Found possible domain in PSK delimiter format: ${possibleDomain}`);
|
|
769
|
-
return possibleDomain;
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
catch (e) {
|
|
776
|
-
log('Failed to parse PSK identity as UTF-8');
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
else {
|
|
782
|
-
// Skip this extension
|
|
783
|
-
pos += extensionLength;
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
log('No hostname found in PSK extension');
|
|
787
|
-
return undefined;
|
|
788
|
-
}
|
|
789
|
-
catch (error) {
|
|
790
|
-
log(`Error parsing PSK: ${error instanceof Error ? error.message : String(error)}`);
|
|
791
|
-
return undefined;
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
/**
|
|
795
|
-
* Checks if the buffer contains TLS 1.3 early data (0-RTT)
|
|
796
|
-
* @param buffer - The buffer to check
|
|
797
|
-
* @param enableLogging - Whether to enable logging
|
|
798
|
-
* @returns true if early data is detected
|
|
799
|
-
*/
|
|
800
|
-
static hasEarlyData(buffer, enableLogging = false) {
|
|
801
|
-
const log = (message) => {
|
|
802
|
-
if (enableLogging) {
|
|
803
|
-
console.log(`[Early Data] ${message}`);
|
|
804
|
-
}
|
|
805
|
-
};
|
|
806
|
-
try {
|
|
807
|
-
// Check if this is a valid ClientHello first
|
|
808
|
-
if (!this.isClientHello(buffer)) {
|
|
809
|
-
return false;
|
|
810
|
-
}
|
|
811
|
-
// Find the extensions section
|
|
812
|
-
let pos = 5; // Start after record header
|
|
813
|
-
// Skip handshake type (1 byte)
|
|
814
|
-
pos += 1;
|
|
815
|
-
// Skip handshake length (3 bytes)
|
|
816
|
-
pos += 3;
|
|
817
|
-
// Skip client version (2 bytes)
|
|
818
|
-
pos += 2;
|
|
819
|
-
// Skip client random (32 bytes)
|
|
820
|
-
pos += 32;
|
|
821
|
-
// Skip session ID
|
|
822
|
-
if (pos + 1 > buffer.length)
|
|
823
|
-
return false;
|
|
824
|
-
const sessionIdLength = buffer[pos];
|
|
825
|
-
pos += 1 + sessionIdLength;
|
|
826
|
-
// Skip cipher suites
|
|
827
|
-
if (pos + 2 > buffer.length)
|
|
828
|
-
return false;
|
|
829
|
-
const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
830
|
-
pos += 2 + cipherSuitesLength;
|
|
831
|
-
// Skip compression methods
|
|
832
|
-
if (pos + 1 > buffer.length)
|
|
833
|
-
return false;
|
|
834
|
-
const compressionMethodsLength = buffer[pos];
|
|
835
|
-
pos += 1 + compressionMethodsLength;
|
|
836
|
-
// Check if we have extensions
|
|
837
|
-
if (pos + 2 > buffer.length)
|
|
838
|
-
return false;
|
|
839
|
-
// Get extensions length
|
|
840
|
-
const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
841
|
-
pos += 2;
|
|
842
|
-
// Extensions end position
|
|
843
|
-
const extensionsEnd = pos + extensionsLength;
|
|
844
|
-
if (extensionsEnd > buffer.length)
|
|
845
|
-
return false;
|
|
846
|
-
// Look for early data extension
|
|
847
|
-
while (pos + 4 <= extensionsEnd) {
|
|
848
|
-
const extensionType = (buffer[pos] << 8) + buffer[pos + 1];
|
|
849
|
-
pos += 2;
|
|
850
|
-
const extensionLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
851
|
-
pos += 2;
|
|
852
|
-
if (extensionType === this.TLS_EARLY_DATA_EXTENSION_TYPE) {
|
|
853
|
-
log('Early Data (0-RTT) extension detected');
|
|
854
|
-
return true;
|
|
855
|
-
}
|
|
856
|
-
// Skip to next extension
|
|
857
|
-
pos += extensionLength;
|
|
858
|
-
}
|
|
859
|
-
return false;
|
|
860
|
-
}
|
|
861
|
-
catch (error) {
|
|
862
|
-
log(`Error checking for early data: ${error}`);
|
|
863
|
-
return false;
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
/**
|
|
867
|
-
* Attempts to extract SNI from an initial ClientHello packet and handles
|
|
868
|
-
* session resumption edge cases more robustly than the standard extraction.
|
|
869
|
-
*
|
|
870
|
-
* This method handles:
|
|
871
|
-
* 1. Standard SNI extraction
|
|
872
|
-
* 2. TLS 1.3 PSK-based resumption (Chrome, Firefox, etc.)
|
|
873
|
-
* 3. Session ticket-based resumption
|
|
874
|
-
* 4. Fragmented ClientHello messages
|
|
875
|
-
* 5. TLS 1.3 Early Data (0-RTT)
|
|
876
|
-
* 6. Chrome's connection racing behaviors
|
|
877
|
-
*
|
|
878
|
-
* @param buffer - The buffer containing the TLS ClientHello message
|
|
879
|
-
* @param connectionInfo - Optional connection information for fragment handling
|
|
880
|
-
* @param enableLogging - Whether to enable detailed debug logging
|
|
881
|
-
* @returns The extracted server name or undefined if not found
|
|
882
|
-
*/
|
|
883
|
-
static extractSNIWithResumptionSupport(buffer, connectionInfo, enableLogging = false) {
|
|
884
|
-
const log = (message) => {
|
|
885
|
-
if (enableLogging) {
|
|
886
|
-
console.log(`[SNI Extraction] ${message}`);
|
|
887
|
-
}
|
|
888
|
-
};
|
|
889
|
-
// Log buffer details for debugging
|
|
890
|
-
if (enableLogging) {
|
|
891
|
-
log(`Buffer size: ${buffer.length} bytes`);
|
|
892
|
-
log(`Buffer starts with: ${buffer.slice(0, Math.min(10, buffer.length)).toString('hex')}`);
|
|
893
|
-
if (buffer.length >= 5) {
|
|
894
|
-
const recordType = buffer[0];
|
|
895
|
-
const majorVersion = buffer[1];
|
|
896
|
-
const minorVersion = buffer[2];
|
|
897
|
-
const recordLength = (buffer[3] << 8) + buffer[4];
|
|
898
|
-
log(`TLS Record: type=${recordType}, version=${majorVersion}.${minorVersion}, length=${recordLength}`);
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
// Check if we need to handle fragmented packets
|
|
902
|
-
let processBuffer = buffer;
|
|
903
|
-
if (connectionInfo) {
|
|
904
|
-
const connectionId = this.createConnectionId(connectionInfo);
|
|
905
|
-
const reassembledBuffer = this.handleFragmentedClientHello(buffer, connectionId, enableLogging);
|
|
906
|
-
if (!reassembledBuffer) {
|
|
907
|
-
log(`Waiting for more fragments on connection ${connectionId}`);
|
|
908
|
-
return undefined; // Need more fragments to complete ClientHello
|
|
909
|
-
}
|
|
910
|
-
processBuffer = reassembledBuffer;
|
|
911
|
-
log(`Using reassembled buffer of length ${processBuffer.length}`);
|
|
912
|
-
}
|
|
913
|
-
// First try the standard SNI extraction
|
|
914
|
-
const standardSni = this.extractSNI(processBuffer, enableLogging);
|
|
915
|
-
if (standardSni) {
|
|
916
|
-
log(`Found standard SNI: ${standardSni}`);
|
|
917
|
-
return standardSni;
|
|
918
|
-
}
|
|
919
|
-
// Check for session resumption when standard SNI extraction fails
|
|
920
|
-
if (this.isClientHello(processBuffer)) {
|
|
921
|
-
const resumptionInfo = this.hasSessionResumption(processBuffer, enableLogging);
|
|
922
|
-
if (resumptionInfo.isResumption) {
|
|
923
|
-
log(`Detected session resumption in ClientHello without standard SNI`);
|
|
924
|
-
// Try to extract SNI from PSK extension
|
|
925
|
-
const pskSni = this.extractSNIFromPSKExtension(processBuffer, enableLogging);
|
|
926
|
-
if (pskSni) {
|
|
927
|
-
log(`Extracted SNI from PSK extension: ${pskSni}`);
|
|
928
|
-
return pskSni;
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
// Log detailed info about the ClientHello when SNI extraction fails
|
|
933
|
-
if (this.isClientHello(processBuffer) && enableLogging) {
|
|
934
|
-
log(`SNI extraction failed for ClientHello. Buffer details:`);
|
|
935
|
-
if (processBuffer.length >= 43) {
|
|
936
|
-
// ClientHello with at least client random
|
|
937
|
-
const clientRandom = processBuffer.slice(11, 11 + 32).toString('hex');
|
|
938
|
-
log(`Client Random: ${clientRandom}`);
|
|
939
|
-
// Log session ID length and presence
|
|
940
|
-
const sessionIdLength = processBuffer[43];
|
|
941
|
-
log(`Session ID length: ${sessionIdLength}`);
|
|
942
|
-
if (sessionIdLength > 0 && processBuffer.length >= 44 + sessionIdLength) {
|
|
943
|
-
const sessionId = processBuffer.slice(44, 44 + sessionIdLength).toString('hex');
|
|
944
|
-
log(`Session ID: ${sessionId}`);
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
return undefined;
|
|
949
|
-
}
|
|
950
|
-
/**
|
|
951
|
-
* Main entry point for SNI extraction that handles all edge cases.
|
|
952
|
-
* This should be called for each TLS packet received from a client.
|
|
953
|
-
*
|
|
954
|
-
* The method uses connection tracking to handle fragmented ClientHello
|
|
955
|
-
* messages and various TLS 1.3 behaviors, including Chrome's connection
|
|
956
|
-
* racing patterns and tab reactivation behaviors.
|
|
957
|
-
*
|
|
958
|
-
* @param buffer - The buffer containing TLS data
|
|
959
|
-
* @param connectionInfo - Connection metadata (IPs and ports)
|
|
960
|
-
* @param enableLogging - Whether to enable detailed debug logging
|
|
961
|
-
* @param cachedSni - Optional cached SNI from previous connections (for racing detection)
|
|
962
|
-
* @returns The extracted server name or undefined if not found or more data needed
|
|
963
|
-
*/
|
|
964
|
-
static processTlsPacket(buffer, connectionInfo, enableLogging = false, cachedSni) {
|
|
965
|
-
const log = (message) => {
|
|
966
|
-
if (enableLogging) {
|
|
967
|
-
console.log(`[TLS Packet] ${message}`);
|
|
968
|
-
}
|
|
969
|
-
};
|
|
970
|
-
// Add timestamp if not provided
|
|
971
|
-
if (!connectionInfo.timestamp) {
|
|
972
|
-
connectionInfo.timestamp = Date.now();
|
|
973
|
-
}
|
|
974
|
-
// Check if this is a TLS handshake or application data
|
|
975
|
-
if (!this.isTlsHandshake(buffer) && !this.isTlsApplicationData(buffer)) {
|
|
976
|
-
log('Not a TLS handshake or application data packet');
|
|
977
|
-
return undefined;
|
|
978
|
-
}
|
|
979
|
-
// Create connection ID for tracking
|
|
980
|
-
const connectionId = this.createConnectionId(connectionInfo);
|
|
981
|
-
log(`Processing TLS packet for connection ${connectionId}, buffer length: ${buffer.length}`);
|
|
982
|
-
// Handle application data with cached SNI (for connection racing)
|
|
983
|
-
if (this.isTlsApplicationData(buffer)) {
|
|
984
|
-
// If explicit cachedSni was provided, use it
|
|
985
|
-
if (cachedSni) {
|
|
986
|
-
log(`Using provided cached SNI for application data: ${cachedSni}`);
|
|
987
|
-
return cachedSni;
|
|
988
|
-
}
|
|
989
|
-
log('Application data packet without cached SNI, cannot determine hostname');
|
|
990
|
-
return undefined;
|
|
991
|
-
}
|
|
992
|
-
// Enhanced session resumption detection
|
|
993
|
-
if (this.isClientHello(buffer)) {
|
|
994
|
-
const resumptionInfo = this.hasSessionResumption(buffer, enableLogging);
|
|
995
|
-
if (resumptionInfo.isResumption) {
|
|
996
|
-
log(`Session resumption detected in TLS packet`);
|
|
997
|
-
// Always try standard SNI extraction first
|
|
998
|
-
const standardSni = this.extractSNI(buffer, enableLogging);
|
|
999
|
-
if (standardSni) {
|
|
1000
|
-
log(`Found standard SNI in session resumption: ${standardSni}`);
|
|
1001
|
-
return standardSni;
|
|
1002
|
-
}
|
|
1003
|
-
// Enhanced session resumption SNI extraction
|
|
1004
|
-
// Try extracting from PSK identity
|
|
1005
|
-
const pskSni = this.extractSNIFromPSKExtension(buffer, enableLogging);
|
|
1006
|
-
if (pskSni) {
|
|
1007
|
-
log(`Extracted SNI from PSK extension: ${pskSni}`);
|
|
1008
|
-
return pskSni;
|
|
1009
|
-
}
|
|
1010
|
-
// Additional check for SNI in session tickets
|
|
1011
|
-
if (enableLogging) {
|
|
1012
|
-
log(`Checking for session ticket information to extract server name...`);
|
|
1013
|
-
// Log more details for debugging
|
|
1014
|
-
try {
|
|
1015
|
-
// Look at the raw buffer for patterns
|
|
1016
|
-
log(`Buffer hexdump (first 100 bytes): ${buffer.slice(0, 100).toString('hex')}`);
|
|
1017
|
-
// Try to find hostname-like patterns in the buffer
|
|
1018
|
-
const bufferStr = buffer.toString('utf8', 0, buffer.length);
|
|
1019
|
-
const hostnamePattern = /([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?/gi;
|
|
1020
|
-
const hostMatches = bufferStr.match(hostnamePattern);
|
|
1021
|
-
if (hostMatches && hostMatches.length > 0) {
|
|
1022
|
-
log(`Possible hostnames found in buffer: ${hostMatches.join(', ')}`);
|
|
1023
|
-
// Check if any match looks like a valid domain
|
|
1024
|
-
for (const match of hostMatches) {
|
|
1025
|
-
if (match.includes('.') && match.length > 3) {
|
|
1026
|
-
log(`Potential SNI found in session data: ${match}`);
|
|
1027
|
-
// Don't automatically use this - just log for debugging
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
catch (e) {
|
|
1033
|
-
log(`Error scanning for patterns: ${e}`);
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
log(`Session resumption without extractable SNI`);
|
|
1037
|
-
// If allowSessionTicket=false, should be rejected by caller
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
// For handshake messages, try the full extraction process
|
|
1041
|
-
const sni = this.extractSNIWithResumptionSupport(buffer, connectionInfo, enableLogging);
|
|
1042
|
-
if (sni) {
|
|
1043
|
-
log(`Successfully extracted SNI: ${sni}`);
|
|
1044
|
-
return sni;
|
|
1045
|
-
}
|
|
1046
|
-
// If we couldn't extract an SNI, check if this is a valid ClientHello
|
|
1047
|
-
if (this.isClientHello(buffer)) {
|
|
1048
|
-
log('Valid ClientHello detected, but no SNI extracted - might need more data');
|
|
1049
|
-
}
|
|
1050
|
-
return undefined;
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5wcC5zbmloYW5kbGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvc21hcnRwcm94eS9jbGFzc2VzLnBwLnNuaWhhbmRsZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLFFBQVEsQ0FBQztBQUVoQzs7Ozs7R0FLRztBQUNILE1BQU0sT0FBTyxVQUFVO0lBQ3JCLGlDQUFpQzthQUNULDhCQUF5QixHQUFHLEVBQUUsQ0FBQzthQUMvQiw4QkFBeUIsR0FBRyxFQUFFLENBQUMsR0FBQyxtQ0FBbUM7YUFDbkUsb0NBQStCLEdBQUcsQ0FBQyxDQUFDO2FBQ3BDLDJCQUFzQixHQUFHLE1BQU0sQ0FBQzthQUNoQyxzQ0FBaUMsR0FBRyxNQUFNLENBQUM7YUFDM0MsMkJBQXNCLEdBQUcsQ0FBQyxDQUFDO2FBQzNCLDJCQUFzQixHQUFHLE1BQU0sQ0FBQyxHQUFDLDRDQUE0QzthQUM3RSxvQ0FBK0IsR0FBRyxNQUFNLENBQUMsR0FBQyx5QkFBeUI7YUFDbkUsa0NBQTZCLEdBQUcsTUFBTSxDQUFDLEdBQUMsK0JBQStCO0lBRS9GLHNEQUFzRDthQUN2QyxzQkFBaUIsR0FBd0IsSUFBSSxHQUFHLEVBQUUsQ0FBQzthQUNuRCxvQkFBZSxHQUFXLElBQUksQ0FBQyxHQUFDLDBDQUEwQztJQUV6Rjs7OztPQUlHO0lBQ0ksTUFBTSxDQUFDLGNBQWMsQ0FBQyxNQUFjO1FBQ3pDLE9BQU8sTUFBTSxDQUFDLE1BQU0sR0FBRyxDQUFDLElBQUksTUFBTSxDQUFDLENBQUMsQ0FBQyxLQUFLLElBQUksQ0FBQyx5QkFBeUIsQ0FBQztJQUMzRSxDQUFDO0lBRUQ7Ozs7T0FJRztJQUNJLE1BQU0sQ0FBQyxvQkFBb0IsQ0FBQyxNQUFjO1FBQy9DLE9BQU8sTUFBTSxDQUFDLE1BQU0sR0FBRyxDQUFDLElBQUksTUFBTSxDQUFDLENBQUMsQ0FBQyxLQUFLLElBQUksQ0FBQyx5QkFBeUIsQ0FBQztJQUMzRSxDQUFDO0lBRUQ7Ozs7OztPQU1HO0lBQ0ksTUFBTSxDQUFDLGtCQUFrQixDQUFDLGNBS2hDO1FBQ0MsTUFBTSxFQUFFLFFBQVEsRUFBRSxVQUFVLEVBQUUsTUFBTSxFQUFFLFFBQVEsRUFBRSxHQUFHLGNBQWMsQ0FBQztRQUNsRSxPQUFPLEdBQUcsUUFBUSxJQUFJLFVBQVUsSUFBSSxNQUFNLElBQUksUUFBUSxFQUFFLENBQUM7SUFDM0QsQ0FBQztJQUVEOzs7Ozs7OztPQVFHO0lBQ0ksTUFBTSxDQUFDLDJCQUEyQixDQUN2QyxNQUFjLEVBQ2QsWUFBb0IsRUFDcEIsZ0JBQXlCLEtBQUs7UUFFOUIsTUFBTSxHQUFHLEdBQUcsQ0FBQyxPQUFlLEVBQUUsRUFBRTtZQUM5QixJQUFJLGFBQWEsRUFBRSxDQUFDO2dCQUNsQixPQUFPLENBQUMsR0FBRyxDQUFDLGtCQUFrQixPQUFPLEVBQUUsQ0FBQyxDQUFDO1lBQzNDLENBQUM7UUFDSCxDQUFDLENBQUM7UUFFRiw2Q0FBNkM7UUFDN0MsSUFBSSxDQUFDLElBQUksQ0FBQyxpQkFBaUIsQ0FBQyxHQUFHLENBQUMsWUFBWSxDQUFDLEVBQUUsQ0FBQztZQUM5Qyx5Q0FBeUM7WUFDekMsSUFBSSxDQUFDLGlCQUFpQixDQUFDLEdBQUcsQ0FBQyxZQUFZLEVBQUUsTUFBTSxDQUFDLENBQUM7WUFFakQsaUVBQWlFO1lBQ2pFLFVBQVUsQ0FBQyxHQUFHLEVBQUU7Z0JBQ2QsSUFBSSxJQUFJLENBQUMsaUJBQWlCLENBQUMsR0FBRyxDQUFDLFlBQVksQ0FBQyxFQUFFLENBQUM7b0JBQzdDLElBQUksQ0FBQyxpQkFBaUIsQ0FBQyxNQUFNLENBQUMsWUFBWSxDQUFDLENBQUM7b0JBQzVDLEdBQUcsQ0FBQyxjQUFjLFlBQVksNkNBQTZDLENBQUMsQ0FBQztnQkFDL0UsQ0FBQztZQUNILENBQUMsRUFBRSxJQUFJLENBQUMsZUFBZSxDQUFDLENBQUM7WUFFekIsa0VBQWtFO1lBQ2xFLElBQUksQ0FBQztnQkFDSCxJQUFJLE1BQU0sQ0FBQyxNQUFNLElBQUksQ0FBQyxFQUFFLENBQUM7b0JBQ3ZCLHdDQUF3QztvQkFDeEMsTUFBTSxZQUFZLEdBQUcsQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLEdBQUcsTUFBTSxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLHNDQUFzQztvQkFDN0YsR0FBRyxDQUFDLHdCQUF3QixNQUFNLENBQUMsTUFBTSw2QkFBNkIsWUFBWSxFQUFFLENBQUMsQ0FBQztvQkFFdEYsOERBQThEO29CQUM5RCxJQUFJLE1BQU0sQ0FBQyxNQUFNLElBQUksWUFBWSxFQUFFLENBQUM7d0JBQ2xDLEdBQUcsQ0FBQyx5REFBeUQsTUFBTSxDQUFDLE1BQU0sRUFBRSxDQUFDLENBQUM7d0JBQzlFLE9BQU8sTUFBTSxDQUFDO29CQUNoQixDQUFDO2dCQUNILENBQUM7cUJBQU0sQ0FBQztvQkFDTixHQUFHLENBQ0QsNkJBQTZCLE1BQU0sQ0FBQyxNQUFNLGdEQUFnRCxDQUMzRixDQUFDO2dCQUNKLENBQUM7WUFDSCxDQUFDO1lBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQztnQkFDWCxHQUFHLENBQUMsK0NBQStDLENBQUMsRUFBRSxDQUFDLENBQUM7WUFDMUQsQ0FBQztZQUVELEdBQUcsQ0FBQyxnQ0FBZ0MsWUFBWSxtQkFBbUIsTUFBTSxDQUFDLE1BQU0sRUFBRSxDQUFDLENBQUM7WUFDcEYsT0FBTyxTQUFTLENBQUMsQ0FBQyxzQkFBc0I7UUFDMUMsQ0FBQzthQUFNLENBQUM7WUFDTiwwQ0FBMEM7WUFDMUMsTUFBTSxjQUFjLEdBQUcsSUFBSSxDQUFDLGlCQUFpQixDQUFDLEdBQUcsQ0FBQyxZQUFZLENBQUUsQ0FBQztZQUNqRSxNQUFNLFNBQVMsR0FBRyxNQUFNLENBQUMsTUFBTSxDQUFDLENBQUMsY0FBYyxFQUFFLE1BQU0sQ0FBQyxDQUFDLENBQUM7WUFDMUQsSUFBSSxDQUFDLGlCQUFpQixDQUFDLEdBQUcsQ0FBQyxZQUFZLEVBQUUsU0FBUyxDQUFDLENBQUM7WUFFcEQsR0FBRyxDQUFDLDBCQUEwQixZQUFZLGVBQWUsU0FBUyxDQUFDLE1BQU0sRUFBRSxDQUFDLENBQUM7WUFFN0UsOENBQThDO1lBQzlDLElBQUksQ0FBQztnQkFDSCxJQUFJLFNBQVMsQ0FBQyxNQUFNLElBQUksQ0FBQyxFQUFFLENBQUM7b0JBQzFCLHdDQUF3QztvQkFDeEMsTUFBTSxZQUFZLEdBQUcsQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLEdBQUcsU0FBUyxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLHNDQUFzQztvQkFDbkcsR0FBRyxDQUNELDRCQUE0QixTQUFTLENBQUMsTUFBTSw2QkFBNkIsWUFBWSxFQUFFLENBQ3hGLENBQUM7b0JBRUYsNkNBQTZDO29CQUM3QyxJQUFJLFNBQVMsQ0FBQyxNQUFNLElBQUksWUFBWSxFQUFFLENBQUM7d0JBQ3JDLEdBQUcsQ0FDRCwyQ0FBMkMsU0FBUyxDQUFDLE1BQU0sYUFBYSxZQUFZLEVBQUUsQ0FDdkYsQ0FBQzt3QkFFRixtRUFBbUU7d0JBQ25FLE1BQU0sY0FBYyxHQUFHLFNBQVMsQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLFlBQVksQ0FBQyxDQUFDO3dCQUV4RCxzRUFBc0U7d0JBQ3RFLElBQ0UsY0FBYyxDQUFDLE1BQU0sR0FBRyxDQUFDOzRCQUN6QixjQUFjLENBQUMsQ0FBQyxDQUFDLEtBQUssSUFBSSxDQUFDLCtCQUErQixFQUMxRCxDQUFDOzRCQUNELEdBQUcsQ0FBQyxvREFBb0QsQ0FBQyxDQUFDOzRCQUUxRCxrREFBa0Q7NEJBQ2xELElBQUksQ0FBQyxpQkFBaUIsQ0FBQyxNQUFNLENBQUMsWUFBWSxDQUFDLENBQUM7NEJBQzVDLE9BQU8sY0FBYyxDQUFDO3dCQUN4QixDQUFDOzZCQUFNLENBQUM7NEJBQ04sR0FBRyxDQUFDLDBFQUEwRSxDQUFDLENBQUM7NEJBQ2hGLGtFQUFrRTs0QkFFbEUsb0VBQW9FOzRCQUNwRSxJQUFJLFNBQVMsQ0FBQyxNQUFNLEdBQUcsWUFBWSxHQUFHLENBQUMsRUFBRSxDQUFDO2dDQUN4QyxNQUFNLGNBQWMsR0FBRyxTQUFTLENBQUMsWUFBWSxDQUFDLENBQUM7Z0NBQy9DLEdBQUcsQ0FDRCxxQkFBcUIsY0FBYyxpQkFBaUIsSUFBSSxDQUFDLHlCQUF5QixHQUFHLENBQ3RGLENBQUM7Z0NBRUYsSUFBSSxjQUFjLEtBQUssSUFBSSxDQUFDLHlCQUF5QixFQUFFLENBQUM7b0NBQ3RELE1BQU0sYUFBYSxHQUFHLFNBQVMsQ0FBQyxZQUFZLEdBQUcsQ0FBQyxDQUFDLENBQUM7b0NBQ2xELEdBQUcsQ0FDRCx3QkFBd0IsYUFBYSxpQkFBaUIsSUFBSSxDQUFDLCtCQUErQixHQUFHLENBQzlGLENBQUM7b0NBRUYsSUFBSSxhQUFhLEtBQUssSUFBSSxDQUFDLCtCQUErQixFQUFFLENBQUM7d0NBQzNELG1FQUFtRTt3Q0FDbkUsR0FBRyxDQUFDLCtEQUErRCxDQUFDLENBQUM7d0NBQ3JFLElBQUksQ0FBQyxpQkFBaUIsQ0FBQyxNQUFNLENBQUMsWUFBWSxDQUFDLENBQUM7d0NBQzVDLE9BQU8sU0FBUyxDQUFDO29DQUNuQixDQUFDO2dDQUNILENBQUM7NEJBQ0gsQ0FBQzt3QkFDSCxDQUFDO29CQUNILENBQUM7Z0JBQ0gsQ0FBQztZQUNILENBQUM7WUFBQyxPQUFPLENBQUMsRUFBRSxDQUFDO2dCQUNYLEdBQUcsQ0FBQyxtREFBbUQsQ0FBQyxFQUFFLENBQUMsQ0FBQztZQUM5RCxDQUFDO1lBRUQsT0FBTyxTQUFTLENBQUMsQ0FBQyw0QkFBNEI7UUFDaEQsQ0FBQztJQUNILENBQUM7SUFFRDs7OztPQUlHO0lBQ0ksTUFBTSxDQUFDLGFBQWEsQ0FBQyxNQUFjO1FBQ3hDLGtFQUFrRTtRQUNsRSxJQUFJLE1BQU0sQ0FBQyxNQUFNLEdBQUcsQ0FBQyxFQUFFLENBQUM7WUFDdEIsT0FBTyxLQUFLLENBQUM7UUFDZixDQUFDO1FBRUQsd0RBQXdEO1FBQ3hELElBQUksTUFBTSxDQUFDLENBQUMsQ0FBQyxLQUFLLElBQUksQ0FBQyx5QkFBeUIsRUFBRSxDQUFDO1lBQ2pELE9BQU8sS0FBSyxDQUFDO1FBQ2YsQ0FBQztRQUVELCtEQUErRDtRQUMvRCx3REFBd0Q7UUFDeEQsT0FBTyxNQUFNLENBQUMsQ0FBQyxDQUFDLEtBQUssSUFBSSxDQUFDLCtCQUErQixDQUFDO0lBQzVELENBQUM7SUFFRDs7Ozs7OztPQU9HO0lBQ0ksTUFBTSxDQUFDLG9CQUFvQixDQUNoQyxNQUFjLEVBQ2QsZ0JBQXlCLEtBQUs7UUFFOUIsTUFBTSxHQUFHLEdBQUcsQ0FBQyxPQUFlLEVBQUUsRUFBRTtZQUM5QixJQUFJLGFBQWEsRUFBRSxDQUFDO2dCQUNsQixPQUFPLENBQUMsR0FBRyxDQUFDLHdCQUF3QixPQUFPLEVBQUUsQ0FBQyxDQUFDO1lBQ2pELENBQUM7UUFDSCxDQUFDLENBQUM7UUFFRixJQUFJLENBQUMsSUFBSSxDQUFDLGFBQWEsQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDO1lBQ2hDLE9BQU8sRUFBRSxZQUFZLEVBQUUsS0FBSyxFQUFFLE1BQU0sRUFBRSxLQUFLLEVBQUUsQ0FBQztRQUNoRCxDQUFDO1FBRUQsSUFBSSxDQUFDO1lBQ0gsc0NBQXNDO1lBQ3RDLElBQUksR0FBRyxHQUFHLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLDJEQUEyRDtZQUNwRixHQUFHLElBQUksRUFBRSxDQUFDLENBQUMscUJBQXFCO1lBRWhDLElBQUksR0FBRyxHQUFHLENBQUMsR0FBRyxNQUFNLENBQUMsTUFBTTtnQkFBRSxPQUFPLEVBQUUsWUFBWSxFQUFFLEtBQUssRUFBRSxNQUFNLEVBQUUsS0FBSyxFQUFFLENBQUM7WUFFM0UsTUFBTSxlQUFlLEdBQUcsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDO1lBQ3BDLElBQUksb0JBQW9CLEdBQUcsZUFBZSxHQUFHLENBQUMsQ0FBQztZQUUvQyxJQUFJLG9CQUFvQixFQUFFLENBQUM7Z0JBQ3pCLEdBQUcsQ0FBQywwQ0FBMEMsZUFBZSxHQUFHLENBQUMsQ0FBQztZQUNwRSxDQUFDO1lBRUQsbUNBQW1DO1lBQ25DLEdBQUcsSUFBSSxDQUFDLEdBQUcsZUFBZSxDQUFDO1lBRTNCLHFCQUFxQjtZQUNyQixJQUFJLEdBQUcsR0FBRyxDQUFDLEdBQUcsTUFBTSxDQUFDLE1BQU07Z0JBQUUsT0FBTyxFQUFFLFlBQVksRUFBRSxLQUFLLEVBQUUsTUFBTSxFQUFFLEtBQUssRUFBRSxDQUFDO1lBQzNFLE1BQU0sa0JBQWtCLEdBQUcsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxDQUFDLEdBQUcsTUFBTSxDQUFDLEdBQUcsR0FBRyxDQUFDLENBQUMsQ0FBQztZQUNoRSxHQUFHLElBQUksQ0FBQyxHQUFHLGtCQUFrQixDQUFDO1lBRTlCLDJCQUEyQjtZQUMzQixJQUFJLEdBQUcsR0FBRyxDQUFDLEdBQUcsTUFBTSxDQUFDLE1BQU07Z0JBQUUsT0FBTyxFQUFFLFlBQVksRUFBRSxLQUFLLEVBQUUsTUFBTSxFQUFFLEtBQUssRUFBRSxDQUFDO1lBQzNFLE1BQU0sd0JBQXdCLEdBQUcsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDO1lBQzdDLEdBQUcsSUFBSSxDQUFDLEdBQUcsd0JBQXdCLENBQUM7WUFFcEMsdUJBQXVCO1lBQ3ZCLElBQUksR0FBRyxHQUFHLENBQUMsR0FBRyxNQUFNLENBQUMsTUFBTTtnQkFBRSxPQUFPLEVBQUUsWUFBWSxFQUFFLEtBQUssRUFBRSxNQUFNLEVBQUUsS0FBSyxFQUFFLENBQUM7WUFFM0UseUNBQXlDO1lBQ3pDLE1BQU0sZ0JBQWdCLEdBQUcsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxDQUFDLEdBQUcsTUFBTSxDQUFDLEdBQUcsR0FBRyxDQUFDLENBQUMsQ0FBQztZQUM5RCxHQUFHLElBQUksQ0FBQyxDQUFDO1lBRVQsMEJBQTBCO1lBQzFCLE1BQU0sYUFBYSxHQUFHLEdBQUcsR0FBRyxnQkFBZ0IsQ0FBQztZQUM3QyxJQUFJLGFBQWEsR0FBRyxNQUFNLENBQUMsTUFBTTtnQkFBRSxPQUFPLEVBQUUsWUFBWSxFQUFFLEtBQUssRUFBRSxNQUFNLEVBQUUsS0FBSyxFQUFFLENBQUM7WUFFakYsOEJBQThCO1lBQzlCLElBQUksZ0JBQWdCLEdBQUcsS0FBSyxDQUFDO1lBQzdCLElBQUksTUFBTSxHQUFHLEtBQUssQ0FBQztZQUNuQixJQUFJLFlBQVksR0FBRyxLQUFLLENBQUM7WUFFekIsNkJBQTZCO1lBQzdCLE9BQU8sR0FBRyxHQUFHLENBQUMsSUFBSSxhQUFhLEVBQUUsQ0FBQztnQkFDaEMsTUFBTSxhQUFhLEdBQUcsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxDQUFDLEdBQUcsTUFBTSxDQUFDLEdBQUcsR0FBRyxDQUFDLENBQUMsQ0FBQztnQkFDM0QsR0FBRyxJQUFJLENBQUMsQ0FBQztnQkFFVCxNQUFNLGVBQWUsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLENBQUMsR0FBRyxNQUFNLENBQUMsR0FBRyxHQUFHLENBQUMsQ0FBQyxDQUFDO2dCQUM3RCxHQUFHLElBQUksQ0FBQyxDQUFDO2dCQUVULElBQUksYUFBYSxLQUFLLElBQUksQ0FBQyxpQ0FBaUMsRUFBRSxDQUFDO29CQUM3RCxHQUFHLENBQUMsZ0NBQWdDLENBQUMsQ0FBQztvQkFDdEMsZ0JBQWdCLEdBQUcsSUFBSSxDQUFDO29CQUV4Qiw4REFBOEQ7b0JBQzlELElBQUksZUFBZSxHQUFHLENBQUMsRUFBRSxDQUFDO3dCQUN4QixHQUFHLENBQUMsNkJBQTZCLGVBQWUsMEJBQTBCLENBQUMsQ0FBQztvQkFDOUUsQ0FBQztnQkFDSCxDQUFDO3FCQUFNLElBQUksYUFBYSxLQUFLLElBQUksQ0FBQyxzQkFBc0IsRUFBRSxDQUFDO29CQUN6RCxHQUFHLENBQUMsb0RBQW9ELENBQUMsQ0FBQztvQkFDMUQsTUFBTSxHQUFHLElBQUksQ0FBQztnQkFDaEIsQ0FBQztxQkFBTSxJQUFJLGFBQWEsS0FBSyxJQUFJLENBQUMsNkJBQTZCLEVBQUUsQ0FBQztvQkFDaEUsR0FBRyxDQUFDLDRDQUE0QyxDQUFDLENBQUM7b0JBQ2xELFlBQVksR0FBRyxJQUFJLENBQUM7Z0JBQ3RCLENBQUM7Z0JBRUQsc0JBQXNCO2dCQUN0QixHQUFHLElBQUksZUFBZSxDQUFDO1lBQ3pCLENBQUM7WUFFRCwyQkFBMkI7WUFDM0IsSUFBSSxNQUFNLEdBQUcsS0FBSyxDQUFDO1lBRW5CLGtEQUFrRDtZQUNsRCxHQUFHLEdBQUcsQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsMkRBQTJEO1lBQ2hGLEdBQUcsSUFBSSxFQUFFLENBQUMsQ0FBQyxxQkFBcUI7WUFFaEMsSUFBSSxHQUFHLEdBQUcsQ0FBQyxJQUFJLE1BQU0sQ0FBQyxNQUFNLEVBQUUsQ0FBQztnQkFDN0IsTUFBTSxlQUFlLEdBQUcsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDO2dCQUNwQyxHQUFHLElBQUksQ0FBQyxHQUFHLGVBQWUsQ0FBQztnQkFFM0IscUJBQXFCO2dCQUNyQixJQUFJLEdBQUcsR0FBRyxDQUFDLElBQUksTUFBTSxDQUFDLE1BQU0sRUFBRSxDQUFDO29CQUM3QixNQUFNLGtCQUFrQixHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsQ0FBQyxHQUFHLE1BQU0sQ0FBQyxHQUFHLEdBQUcsQ0FBQyxDQUFDLENBQUM7b0JBQ2hFLEdBQUcsSUFBSSxDQUFDLEdBQUcsa0JBQWtCLENBQUM7b0JBRTlCLDJCQUEyQjtvQkFDM0IsSUFBSSxHQUFHLEdBQUcsQ0FBQyxJQUFJLE1BQU0sQ0FBQyxNQUFNLEVBQUUsQ0FBQzt3QkFDN0IsTUFBTSx3QkFBd0IsR0FBRyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUM7d0JBQzdDLEdBQUcsSUFBSSxDQUFDLEdBQUcsd0JBQXdCLENBQUM7d0JBRXBDLHVCQUF1Qjt3QkFDdkIsSUFBSSxHQUFHLEdBQUcsQ0FBQyxJQUFJLE1BQU0sQ0FBQyxNQUFNLEVBQUUsQ0FBQzs0QkFDN0IsTUFBTSxnQkFBZ0IsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLENBQUMsR0FBRyxNQUFNLENBQUMsR0FBRyxHQUFHLENBQUMsQ0FBQyxDQUFDOzRCQUM5RCxHQUFHLElBQUksQ0FBQyxDQUFDOzRCQUVULDBCQUEwQjs0QkFDMUIsTUFBTSxhQUFhLEdBQUcsR0FBRyxHQUFHLGdCQUFnQixDQUFDOzRCQUM3QyxJQUFJLGFBQWEsSUFBSSxNQUFNLENBQUMsTUFBTSxFQUFFLENBQUM7Z0NBQ25DLHlCQUF5QjtnQ0FDekIsT0FBTyxHQUFHLEdBQUcsQ0FBQyxJQUFJLGFBQWEsRUFBRSxDQUFDO29DQUNoQyxNQUFNLGFBQWEsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLENBQUMsR0FBRyxNQUFNLENBQUMsR0FBRyxHQUFHLENBQUMsQ0FBQyxDQUFDO29DQUMzRCxHQUFHLElBQUksQ0FBQyxDQUFDO29DQUVULE1BQU0sZUFBZSxHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsQ0FBQyxHQUFHLE1BQU0sQ0FBQyxHQUFHLEdBQUcsQ0FBQyxDQUFDLENBQUM7b0NBQzdELEdBQUcsSUFBSSxDQUFDLENBQUM7b0NBRVQsSUFBSSxhQUFhLEtBQUssSUFBSSxDQUFDLHNCQUFzQixFQUFFLENBQUM7d0NBQ2xELG9EQUFvRDt3Q0FDcEQsSUFBSSxlQUFlLEdBQUcsQ0FBQyxFQUFFLENBQUM7NENBQ3hCLE1BQU0sR0FBRyxJQUFJLENBQUM7NENBRWQsa0RBQWtEOzRDQUNsRCxJQUFJLENBQUM7Z0RBQ0gsNENBQTRDO2dEQUM1QyxNQUFNLE9BQU8sR0FBRyxHQUFHLENBQUM7Z0RBQ3BCLElBQUksT0FBTyxHQUFHLENBQUMsSUFBSSxhQUFhLEVBQUUsQ0FBQztvREFDakMsTUFBTSxjQUFjLEdBQUcsQ0FBQyxNQUFNLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDLEdBQUcsTUFBTSxDQUFDLE9BQU8sR0FBRyxDQUFDLENBQUMsQ0FBQztvREFFcEUseUNBQXlDO29EQUN6QyxJQUFJLE9BQU8sR0FBRyxDQUFDLEdBQUcsQ0FBQyxJQUFJLGFBQWEsRUFBRSxDQUFDO3dEQUNyQyw2Q0FBNkM7d0RBQzdDLElBQUksTUFBTSxDQUFDLE9BQU8sR0FBRyxDQUFDLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQzs0REFDOUIsMEJBQTBCOzREQUMxQixJQUFJLE9BQU8sR0FBRyxDQUFDLEdBQUcsQ0FBQyxJQUFJLGFBQWEsRUFBRSxDQUFDO2dFQUNyQyw0QkFBNEI7Z0VBQzVCLE1BQU0sVUFBVSxHQUFHLENBQUMsTUFBTSxDQUFDLE9BQU8sR0FBRyxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsR0FBRyxNQUFNLENBQUMsT0FBTyxHQUFHLENBQUMsQ0FBQyxDQUFDO2dFQUVwRSx1QkFBdUI7Z0VBQ3ZCLElBQUksT0FBTyxHQUFHLENBQUMsR0FBRyxVQUFVLElBQUksYUFBYSxFQUFFLENBQUM7b0VBQzlDLE1BQU0sUUFBUSxHQUFHLE1BQU07eUVBQ3BCLEtBQUssQ0FBQyxPQUFPLEdBQUcsQ0FBQyxFQUFFLE9BQU8sR0FBRyxDQUFDLEdBQUcsVUFBVSxDQUFDO3lFQUM1QyxRQUFRLENBQUMsTUFBTSxDQUFDLENBQUM7b0VBQ3BCLEdBQUcsQ0FBQyx5Q0FBeUMsUUFBUSxFQUFFLENBQUMsQ0FBQztnRUFDM0QsQ0FBQzs0REFDSCxDQUFDO3dEQUNILENBQUM7b0RBQ0gsQ0FBQztnREFDSCxDQUFDOzRDQUNILENBQUM7NENBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQztnREFDWCxHQUFHLENBQUMsK0JBQStCLENBQUMsRUFBRSxDQUFDLENBQUM7Z0RBQ3hDLEdBQUcsQ0FBQyxtQ0FBbUMsR0FBRyxlQUFlLENBQUMsQ0FBQzs0Q0FDN0QsQ0FBQzt3Q0FDSCxDQUFDOzZDQUFNLENBQUM7NENBQ04sR0FBRyxDQUFDLCtDQUErQyxDQUFDLENBQUM7d0NBQ3ZELENBQUM7d0NBQ0QsTUFBTTtvQ0FDUixDQUFDO29DQUVELHNCQUFzQjtvQ0FDdEIsR0FBRyxJQUFJLGVBQWUsQ0FBQztnQ0FDekIsQ0FBQzs0QkFDSCxDQUFDO3dCQUNILENBQUM7b0JBQ0gsQ0FBQztnQkFDSCxDQUFDO1lBQ0gsQ0FBQztZQUVELGtFQUFrRTtZQUNsRSxNQUFNLFlBQVksR0FDaEIsZ0JBQWdCLElBQUksTUFBTSxJQUFJLFlBQVksSUFBSSxDQUFDLG9CQUFvQixJQUFJLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQyxvQkFBb0I7WUFFdkcsSUFBSSxZQUFZLEVBQUUsQ0FBQztnQkFDakIsR0FBRyxDQUNELCtCQUErQjtvQkFDN0IsQ0FBQyxnQkFBZ0IsQ0FBQyxDQUFDLENBQUMsa0JBQWtCLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztvQkFDNUMsQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO29CQUN2QixDQUFDLFlBQVksQ0FBQyxDQUFDLENBQUMsY0FBYyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUM7b0JBQ3BDLENBQUMsb0JBQW9CLENBQUMsQ0FBQyxDQUFDLFlBQVksQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO29CQUMxQyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsWUFBWSxDQUFDLENBQUMsQ0FBQyxlQUFlLENBQUMsQ0FDNUMsQ0FBQztZQUNKLENBQUM7WUFFRCxtQ0FBbUM7WUFDbkMseUZBQXlGO1lBQ3pGLElBQUksWUFBWSxFQUFFLENBQUM7Z0JBQ2pCLEdBQUcsQ0FDRCxnQ0FBZ0MsTUFBTSxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLElBQUksc0JBQ25ELGdCQUFnQixDQUFDLENBQUMsQ0FBQyxrQkFBa0IsQ0FBQyxDQUFDLENBQUMsRUFDMUMsR0FBRyxNQUFNLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsRUFBRSxHQUFHLFlBQVksQ0FBQyxDQUFDLENBQUMsY0FBYyxDQUFDLENBQUMsQ0FBQyxFQUFFLEdBQzNELG9CQUFvQixDQUFDLENBQUMsQ0FBQyxZQUFZLENBQUMsQ0FBQyxDQUFDLEVBQ3hDLEVBQUUsQ0FDSCxDQUFDO1lBQ0osQ0FBQztZQUVELE9BQU87Z0JBQ0wsWUFBWTtnQkFDWixNQUFNO2FBQ1AsQ0FBQztRQUNKLENBQUM7UUFBQyxPQUFPLEtBQUssRUFBRSxDQUFDO1lBQ2YsR0FBRyxDQUFDLDBDQUEwQyxLQUFLLEVBQUUsQ0FBQyxDQUFDO1lBQ3ZELE9BQU8sRUFBRSxZQUFZLEVBQUUsS0FBSyxFQUFFLE1BQU0sRUFBRSxLQUFLLEVBQUUsQ0FBQztRQUNoRCxDQUFDO0lBQ0gsQ0FBQztJQUVEOzs7Ozs7O09BT0c7SUFDSSxNQUFNLENBQUMsMEJBQTBCLENBQ3RDLE1BQWMsRUFDZCxnQkFBeUIsS0FBSztRQUU5QixNQUFNLEdBQUcsR0FBRyxDQUFDLE9BQWUsRUFBRSxFQUFFO1lBQzlCLElBQUksYUFBYSxFQUFFLENBQUM7Z0JBQ2xCLE9BQU8sQ0FBQyxHQUFHLENBQUMsc0JBQXNCLE9BQU8sRUFBRSxDQUFDLENBQUM7WUFDL0MsQ0FBQztRQUNILENBQUMsQ0FBQztRQUVGLElBQUksQ0FBQyxJQUFJLENBQUMsYUFBYSxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUM7WUFDaEMsT0FBTyxLQUFLLENBQUM7UUFDZixDQUFDO1FBRUQsSUFBSSxDQUFDO1lBQ0gsMEVBQTBFO1lBQzFFLElBQUksR0FBRyxHQUFHLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLDJEQUEyRDtZQUNwRixHQUFHLElBQUksRUFBRSxDQUFDLENBQUMscUJBQXFCO1lBRWhDLElBQUksR0FBRyxHQUFHLENBQUMsR0FBRyxNQUFNLENBQUMsTUFBTTtnQkFBRSxPQUFPLEtBQUssQ0FBQztZQUUxQyxNQUFNLGVBQWUsR0FBRyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUM7WUFFcEMsMkNBQTJDO1lBQzNDLElBQUksZUFBZSxHQUFHLENBQUMsRUFBRSxDQUFDO2dCQUN4QixHQUFHLENBQUMsMENBQTBDLGVBQWUsR0FBRyxDQUFDLENBQUM7Z0JBRWxFLHFCQUFxQjtnQkFDckIsR0FBRyxJQUFJLENBQUMsR0FBRyxlQUFlLENBQUM7Z0JBRTNCLHFCQUFxQjtnQkFDckIsSUFBSSxHQUFHLEdBQUcsQ0FBQyxHQUFHLE1BQU0sQ0FBQyxNQUFNO29CQUFFLE9BQU8sS0FBSyxDQUFDO2dCQUMxQyxNQUFNLGtCQUFrQixHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsQ0FBQyxHQUFHLE1BQU0sQ0FBQyxHQUFHLEdBQUcsQ0FBQyxDQUFDLENBQUM7Z0JBQ2hFLEdBQUcsSUFBSSxDQUFDLEdBQUcsa0JBQWtCLENBQUM7Z0JBRTlCLDJCQUEyQjtnQkFDM0IsSUFBSSxHQUFHLEdBQUcsQ0FBQyxHQUFHLE1BQU0sQ0FBQyxNQUFNO29CQUFFLE9BQU8sS0FBSyxDQUFDO2dCQUMxQyxNQUFNLHdCQUF3QixHQUFHLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQztnQkFDN0MsR0FBRyxJQUFJLENBQUMsR0FBRyx3QkFBd0IsQ0FBQztnQkFFcEMsdUJBQXVCO2dCQUN2QixJQUFJLEdBQUcsR0FBRyxDQUFDLEdBQUcsTUFBTSxDQUFDLE1BQU07b0JBQUUsT0FBTyxLQUFLLENBQUM7Z0JBRTFDLDhEQUE4RDtnQkFDOUQsTUFBTSxnQkFBZ0IsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLENBQUMsR0FBRyxNQUFNLENBQUMsR0FBRyxHQUFHLENBQUMsQ0FBQyxDQUFDO2dCQUM5RCxHQUFHLElBQUksQ0FBQyxDQUFDO2dCQUVULDBCQUEwQjtnQkFDMUIsTUFBTSxhQUFhLEdBQUcsR0FBRyxHQUFHLGdCQUFnQixDQUFDO2dCQUM3QyxJQUFJLGFBQWEsR0FBRyxNQUFNLENBQUMsTUFBTTtvQkFBRSxPQUFPLEtBQUssQ0FBQztnQkFFaEQsd0RBQXdEO2dCQUN4RCxJQUFJLGdCQUFnQixHQUFHLEtBQUssQ0FBQztnQkFDN0IsSUFBSSxNQUFNLEdBQUcsS0FBSyxDQUFDO2dCQUNuQixJQUFJLE1BQU0sR0FBRyxLQUFLLENBQUM7Z0JBRW5CLDZCQUE2QjtnQkFDN0IsT0FBTyxHQUFHLEdBQUcsQ0FBQyxJQUFJLGFBQWEsRUFBRSxDQUFDO29CQUNoQyxNQUFNLGFBQWEsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLENBQUMsR0FBRyxNQUFNLENBQUMsR0FBRyxHQUFHLENBQUMsQ0FBQyxDQUFDO29CQUMzRCxHQUFHLElBQUksQ0FBQyxDQUFDO29CQUVULE1BQU0sZUFBZSxHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsQ0FBQyxHQUFHLE1BQU0sQ0FBQyxHQUFHLEdBQUcsQ0FBQyxDQUFDLENBQUM7b0JBQzdELEdBQUcsSUFBSSxDQUFDLENBQUM7b0JBRVQsSUFBSSxhQUFhLEtBQUssSUFBSSxDQUFDLGlDQUFpQyxFQUFFLENBQUM7d0JBQzdELGdCQUFnQixHQUFHLElBQUksQ0FBQztvQkFDMUIsQ0FBQzt5QkFBTSxJQUFJLGFBQWEsS0FBSyxJQUFJLENBQUMsc0JBQXNCLEVBQUUsQ0FBQzt3QkFDekQsTUFBTSxHQUFHLElBQUksQ0FBQztvQkFDaEIsQ0FBQzt5QkFBTSxJQUFJLGFBQWEsS0FBSyxJQUFJLENBQUMsc0JBQXNCLEVBQUUsQ0FBQzt3QkFDekQsTUFBTSxHQUFHLElBQUksQ0FBQztvQkFDaEIsQ0FBQztvQkFFRCxzQkFBc0I7b0JBQ3RCLEdBQUcsSUFBSSxlQUFlLENBQUM7Z0JBQ3pCLENBQUM7Z0JBRUQsZ0ZBQWdGO2dCQUNoRixJQUFJLENBQUMsZ0JBQWdCLElBQUksTUFBTSxDQUFDLElBQUksQ0FBQyxNQUFNLEVBQUUsQ0FBQztvQkFDNUMsR0FBRyxDQUFDLG1FQUFtRSxDQUFDLENBQUM7b0JBQ3pFLE9BQU8sSUFBSSxDQUFDO2dCQUNkLENBQUM7WUFDSCxDQUFDO1FBQ0gsQ0FBQztRQUFDLE9BQU8sS0FBSyxFQUFFLENBQUM7WUFDZixHQUFHLENBQUMsd0NBQXdDLEtBQUssRUFBRSxDQUFDLENBQUM7UUFDdkQsQ0FBQztRQUVELE9BQU8sS0FBSyxDQUFDO0lBQ2YsQ0FBQztJQUVEOzs7Ozs7O09BT0c7SUFDSSxNQUFNLENBQUMsVUFBVSxDQUFDLE1BQWMsRUFBRSxnQkFBeUIsS0FBSztRQUNyRSxpQkFBaUI7UUFDakIsTUFBTSxHQUFHLEdBQUcsQ0FBQyxPQUFlLEVBQUUsRUFBRTtZQUM5QixJQUFJLGFBQWEsRUFBRSxDQUFDO2dCQUNsQixPQUFPLENBQUMsR0FBRyxDQUFDLG9CQUFvQixPQUFPLEVBQUUsQ0FBQyxDQUFDO1lBQzdDLENBQUM7UUFDSCxDQUFDLENBQUM7UUFFRixJQUFJLENBQUM7WUFDSCxzREFBc0Q7WUFDdEQsSUFBSSxNQUFNLENBQUMsTUFBTSxHQUFHLENBQUMsRUFBRSxDQUFDO2dCQUN0QixHQUFHLENBQUMsd0NBQXdDLENBQUMsQ0FBQztnQkFDOUMsT0FBTyxTQUFTLENBQUM7WUFDbkIsQ0FBQztZQUVELDZEQUE2RDtZQUM3RCxJQUFJLE1BQU0sQ0FBQyxDQUFDLENBQUMsS0FBSyxJQUFJLENBQUMseUJBQXlCLEVBQUUsQ0FBQztnQkFDakQsR0FBRyxDQUFDLCtCQUErQixNQUFNLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDO2dCQUNoRCxPQUFPLFNBQVMsQ0FBQztZQUNuQixDQUFDO1lBRUQsb0JBQW9CO1lBQ3BCLE1BQU0sWUFBWSxHQUFHLE1BQU0sQ0FBQyxDQUFDLENBQUMsQ0FBQztZQUMvQixNQUFNLFlBQVksR0FBRyxNQUFNLENBQUMsQ0FBQyxDQUFDLENBQUM7WUFDL0IsR0FBRyxDQUFDLGdCQUFnQixZQUFZLElBQUksWUFBWSxFQUFFLENBQUMsQ0FBQztZQUVwRCw4Q0FBOEM7WUFDOUMsTUFBTSxZQUFZLEdBQUcsQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLEdBQUcsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDO1lBQ2xELEdBQUcsQ0FBQyxrQkFBa0IsWUFBWSxFQUFFLENBQUMsQ0FBQztZQUV0Qyw2Q0FBNkM7WUFDN0MsSUFBSSxNQUFNLENBQUMsTUFBTSxHQUFHLFlBQVksR0FBRyxDQUFDLEVBQUUsQ0FBQztnQkFDckMsR0FBRyxDQUFDLDRDQUE0QyxDQUFDLENBQUM7Z0JBQ2xELE9BQU8sU0FBUyxDQUFDO1lBQ25CLENBQUM7WUFFRCwyQ0FBMkM7WUFDM0MsSUFBSSxHQUFHLEdBQUcsQ0FBQyxDQUFDO1lBRVosa0RBQWtEO1lBQ2xELElBQUksTUFBTSxDQUFDLEdBQUcsQ0FBQyxLQUFLLElBQUksQ0FBQywrQkFBK0IsRUFBRSxDQUFDO2dCQUN6RCxHQUFHLENBQUMsOEJBQThCLE1BQU0sQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDLENBQUM7Z0JBQ2pELE9BQU8sU0FBUyxDQUFDO1lBQ25CLENBQUM7WUFFRCwrQkFBK0I7WUFDL0IsR0FBRyxJQUFJLENBQUMsQ0FBQztZQUVULCtDQUErQztZQUMvQyxNQUFNLGVBQWUsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsSUFBSSxFQUFFLENBQUMsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLEdBQUcsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLEdBQUcsTUFBTSxDQUFDLEdBQUcsR0FBRyxDQUFDLENBQUMsQ0FBQztZQUN2RixHQUFHLENBQUMscUJBQXFCLGVBQWUsRUFBRSxDQUFDLENBQUM7WUFFNUMsa0NBQWtDO1lBQ2xDLEdBQUcsSUFBSSxDQUFDLENBQUM7WUFFVCxpQ0FBaUM7WUFDakMsTUFBTSxrQkFBa0IsR0FBRyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUM7WUFDdkMsTUFBTSxrQkFBa0IsR0FBRyxNQUFNLENBQUMsR0FBRyxHQUFHLENBQUMsQ0FBQyxDQUFDO1lBQzNDLEdBQUcsQ0FBQyxtQkFBbUIsa0JBQWtCLElBQUksa0JBQWtCLEVBQUUsQ0FBQyxDQUFDO1lBRW5FLGdDQUFnQztZQUNoQyxHQUFHLElBQUksQ0FBQyxDQUFDO1lBRVQsZ0NBQWdDO1lBQ2hDLEdBQUcsSUFBSSxFQUFFLENBQUM7WUFFVixtQkFBbUI7WUFDbkIsSUFBSSxHQUFHLEdBQUcsQ0FBQyxHQUFHLE1BQU0sQ0FBQyxNQUFNLEVBQUUsQ0FBQztnQkFDNUIsR0FBRyxDQUFDLHdDQUF3QyxDQUFDLENBQUM7Z0JBQzlDLE9BQU8sU0FBUyxDQUFDO1lBQ25CLENBQUM7WUFFRCxNQUFNLGVBQWUsR0FBRyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUM7WUFDcEMsR0FBRyxDQUFDLHNCQUFzQixlQUFlLEVBQUUsQ0FBQyxDQUFDO1lBRTdDLGlEQUFpRDtZQUNqRCxHQUFHLElBQUksQ0FBQyxHQUFHLGVBQWUsQ0FBQztZQUUzQixxQ0FBcUM7WUFDckMsSUFBSSxHQUFHLEdBQUcsQ0FBQyxHQUFHLE1BQU0sQ0FBQyxNQUFNLEVBQUUsQ0FBQztnQkFDNUIsR0FBRyxDQUFDLDJDQUEyQyxDQUFDLENBQUM7Z0JBQ2pELE9BQU8sU0FBUyxDQUFDO1lBQ25CLENBQUM7WUFFRCxtREFBbUQ7WUFDbkQsTUFBTSxrQkFBa0IsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLENBQUMsR0FBRyxNQUFNLENBQUMsR0FBRyxHQUFHLENBQUMsQ0FBQyxDQUFDO1lBQ2hFLEdBQUcsQ0FBQyx5QkFBeUIsa0JBQWtCLEVBQUUsQ0FBQyxDQUFDO1lBRW5ELHdEQUF3RDtZQUN4RCxHQUFHLElBQUksQ0FBQyxHQUFHLGtCQUFrQixDQUFDO1lBRTlCLHFDQUFxQztZQUNyQyxJQUFJLEdBQUcsR0FBRyxDQUFDLEdBQUcsTUFBTSxDQUFDLE1BQU0sRUFBRSxDQUFDO2dCQUM1QixHQUFHLENBQUMsaURBQWlELENBQUMsQ0FBQztnQkFDdkQsT0FBTyxTQUFTLENBQUM7WUFDbkIsQ0FBQztZQUVELDRDQUE0QztZQUM1QyxNQUFNLHdCQUF3QixHQUFHLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQztZQUM3QyxHQUFHLENBQUMsK0JBQStCLHdCQUF3QixFQUFFLENBQUMsQ0FBQztZQUUvRCxtRUFBbUU7WUFDbkUsR0FBRyxJQUFJLENBQUMsR0FBRyx3QkFBd0IsQ0FBQztZQUVwQyxzREFBc0Q7WUFDdEQsSUFBSSxHQUFHLEdBQUcsQ0FBQyxHQUFHLE1BQU0sQ0FBQyxNQUFNLEVBQUUsQ0FBQztnQkFDNUIsR0FBRyxDQUFDLDJDQUEyQyxDQUFDLENBQUM7Z0JBQ2pELE9BQU8sU0FBUyxDQUFDO1lBQ25CLENBQUM7WUFFRCxnREFBZ0Q7WUFDaEQsTUFBTSxnQkFBZ0IsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLENBQUMsR0FBRyxNQUFNLENBQUMsR0FBRyxHQUFHLENBQUMsQ0FBQyxDQUFDO1lBQzlELEdBQUcsQ0FBQyxzQkFBc0IsZ0JBQWdCLEVBQUUsQ0FBQyxDQUFDO1lBRTlDLG1DQUFtQztZQUNuQyxHQUFHLElBQUksQ0FBQyxDQUFDO1lBRVQsMEJBQTBCO1lBQzFCLE1BQU0sYUFBYSxHQUFHLEdBQUcsR0FBRyxnQkFBZ0IsQ0FBQztZQUU3QyxzQ0FBc0M7WUFDdEMsSUFBSSxhQUFhLEdBQUcsTUFBTSxDQUFDLE1BQU0sRUFBRSxDQUFDO2dCQUNsQyxHQUFHLENBQUMsdUNBQXVDLENBQUMsQ0FBQztnQkFDN0MsT0FBTyxTQUFTLENBQUM7WUFDbkIsQ0FBQztZQUVELHVFQUF1RTtZQUN2RSxJQUFJLGdCQUFnQixHQUFHLEtBQUssQ0FBQztZQUM3QixJQUFJLGVBQWUsR0FBRyxLQUFLLENBQUM7WUFFNUIsNkJBQTZCO1lBQzdCLE9BQU8sR0FBRyxHQUFHLENBQUMsSUFBSSxhQUFhLEVBQUUsQ0FBQztnQkFDaEMsNkNBQTZDO2dCQUM3QyxNQUFNLGFBQWEsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLENBQUMsR0FBRyxNQUFNLENBQUMsR0FBRyxHQUFHLENBQUMsQ0FBQyxDQUFDO2dCQUMzRCxHQUFHLENBQUMscUJBQXFCLGFBQWEsQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUMsRUFBRSxHQUFHLENBQUMsRUFBRSxDQUFDLENBQUM7Z0JBRXhFLGdDQUFnQztnQkFDaEMsR0FBRyxJQUFJLENBQUMsQ0FBQztnQkFFVCwrQ0FBK0M7Z0JBQy9DLE1BQU0sZUFBZSxHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsQ0FBQyxHQUFHLE1BQU0sQ0FBQyxHQUFHLEdBQUcsQ0FBQyxDQUFDLENBQUM7Z0JBQzdELEdBQUcsQ0FBQyxxQkFBcUIsZUFBZSxFQUFFLENBQUMsQ0FBQztnQkFFNUMsa0NBQWtDO2dCQUNsQyxHQUFHLElBQUksQ0FBQyxDQUFDO2dCQUVULHFDQUFxQztnQkFDckMsSUFBSSxhQUFhLEtBQUssSUFBSSxDQUFDLHNCQUFzQixFQUFFLENBQUM7b0JBQ2xELEdBQUcsQ0FBQyxxQkFBcUIsQ0FBQyxDQUFDO29CQUUzQix1REFBdUQ7b0JBQ3ZELElBQUksR0FBRyxHQUFHLENBQUMsR0FBRyxhQUFhLEVBQUUsQ0FBQzt3QkFDNUIsR0FBRyxDQUFDLGlEQUFpRCxDQUFDLENBQUM7d0JBQ3ZELEdBQUcsSUFBSSxlQUFlLENBQUMsQ0FBQyxzQkFBc0I7d0JBQzlDLFNBQVM7b0JBQ1gsQ0FBQztvQkFFRCxzREFBc0Q7b0JBQ3RELE1BQU0sb0JBQW9CLEdBQUcsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxDQUFDLEdBQUcsTUFBTSxDQUFDLEdBQUcsR0FBRyxDQUFDLENBQUMsQ0FBQztvQkFDbEUsR0FBRyxDQUFDLDRCQUE0QixvQkFBb0IsRUFBRSxDQUFDLENBQUM7b0JBRXhELHlDQUF5QztvQkFDekMsR0FBRyxJQUFJLENBQUMsQ0FBQztvQkFFVCwwQ0FBMEM7b0JBQzFDLElBQUksR0FBRyxHQUFHLG9CQUFvQixHQUFHLGFBQWEsRUFBRSxDQUFDO3dCQUMvQyxHQUFHLENBQUMsZ0RBQWdELENBQUMsQ0FBQzt3QkFDdEQsTUFBTSxDQUFDLDZDQUE2QztvQkFDdEQsQ0FBQztvQkFFRCxtQ0FBbUM7b0JBQ25DLE1BQU0saUJBQWlCLEdBQUcsR0FBRyxHQUFHLG9CQUFvQixDQUFDO29CQUVyRCwrQkFBK0I7b0JBQy9CLE9BQU8sR0FBRyxHQUFHLENBQUMsSUFBSSxpQkFBaUIsRUFBRSxDQUFDO3dCQUNwQyw0REFBNEQ7d0JBQzVELE1BQU0sUUFBUSxHQUFHLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQzt3QkFDN0IsR0FBRyxDQUFDLGNBQWMsUUFBUSxFQUFFLENBQUMsQ0FBQzt3QkFFOUIsSUFBSSxRQUFRLEtBQUssSUFBSSxDQUFDLHNCQUFzQixFQUFFLENBQUM7NEJBQzdDLEdBQUcsQ0FBQywwQkFBMEIsUUFBUSxFQUFFLENBQUMsQ0FBQzs0QkFDMUMsR0FBRyxJQUFJLENBQUMsQ0FBQyxDQUFDLDBCQUEwQjs0QkFFcEMsMkNBQTJDOzRCQUMzQyxJQUFJLEdBQUcsR0FBRyxDQUFDLElBQUksaUJBQWlCLEVBQUUsQ0FBQztnQ0FDakMsTUFBTSxVQUFVLEdBQUcsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxDQUFDLEdBQUcsTUFBTSxDQUFDLEdBQUcsR0FBRyxDQUFDLENBQUMsQ0FBQztnQ0FDeEQsR0FBRyxJQUFJLENBQUMsR0FBRyxVQUFVLENBQUM7NEJBQ3hCLENBQUM7aUNBQU0sQ0FBQztnQ0FDTixHQUFHLENBQUMsMkJBQTJCLENBQUMsQ0FBQztnQ0FDakMsTUFBTTs0QkFDUixDQUFDOzRCQUNELFNBQVM7d0JBQ1gsQ0FBQzt3QkFFRCwwQkFBMEI7d0JBQzFCLEdBQUcsSUFBSSxDQUFDLENBQUM7d0JBRVQsOENBQThDO3dCQUM5QyxJQUFJLEdBQUcsR0FBRyxDQUFDLEdBQUcsaUJBQWlCLEVBQUUsQ0FBQzs0QkFDaEMsR0FBRyxDQUFDLDZDQUE2QyxDQUFDLENBQUM7NEJBQ25ELE1BQU07d0JBQ1IsQ0FBQzt3QkFFRCwwQ0FBMEM7d0JBQzFDLE1BQU0sVUFBVSxHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsQ0FBQyxHQUFHLE1BQU0sQ0FBQyxHQUFHLEdBQUcsQ0FBQyxDQUFDLENBQUM7d0JBQ3hELEdBQUcsQ0FBQyxnQkFBZ0IsVUFBVSxFQUFFLENBQUMsQ0FBQzt3QkFFbEMsNkJBQTZCO3dCQUM3QixHQUFHLElBQUksQ0FBQyxDQUFDO3dCQUVULDJDQUEyQzt3QkFDM0MsSUFBSSxHQUFHLEdBQUcsVUFBVSxHQUFHLGlCQUFpQixFQUFFLENBQUM7NEJBQ3pDLEdBQUcsQ0FBQywyQ0FBMkMsQ0FBQyxDQUFDOzRCQUNqRCxNQUFNO3dCQUNSLENBQUM7d0JBRUQsaUNBQWlDO3dCQUNqQyxNQUFNLFVBQVUsR0FBRyxNQUFNLENBQUMsS0FBSyxDQUFDLEdBQUcsRUFBRSxHQUFHLEdBQUcsVUFBVSxDQUFDLENBQUMsUUFBUSxDQUFDLE1BQU0sQ0FBQyxDQUFDO3dCQUN4RSxHQUFHLENBQUMsMEJBQTBCLFVBQVUsRUFBRSxDQUFDLENBQUM7d0JBQzVDLE9BQU8sVUFBVSxDQUFDO29CQUNwQixDQUFDO2dCQUNILENBQUM7cUJBQU0sSUFBSSxhQUFhLEtBQUssSUFBSSxDQUFDLGlDQUFpQyxFQUFFLENBQUM7b0JBQ3BFLGdFQUFnRTtvQkFDaEUsR0FBRyxDQUFDLGdDQUFnQyxDQUFDLENBQUM7b0JBQ3RDLGdCQUFnQixHQUFHLElBQUksQ0FBQztvQkFDeEIsR0FBRyxJQUFJLGVBQWUsQ0FBQyxDQUFDLHNCQUFzQjtnQkFDaEQsQ0FBQztxQkFBTSxJQUFJLGFBQWEsS0FBSyxJQUFJLENBQUMsc0JBQXNCLEVBQUUsQ0FBQztvQkFDekQsc0RBQXNEO29CQUN0RCxHQUFHLENBQUMsb0RBQW9ELENBQUMsQ0FBQztvQkFDMUQsZUFBZSxHQUFHLElBQUksQ0FBQztvQkFDdkIsb0VBQW9FO29CQUNwRSxHQUFHLElBQUksZUFBZSxDQUFDO2dCQUN6QixDQUFDO3FCQUFNLENBQUM7b0JBQ04sc0JBQXNCO29CQUN0QixHQUFHLElBQUksZUFBZSxDQUFDO2dCQUN6QixDQUFDO1lBQ0gsQ0FBQztZQUVELDJEQUEyRDtZQUMzRCxJQUFJLGdCQUFnQixJQUFJLGVBQWUsRUFBRSxDQUFDO2dCQUN4QyxHQUFHLENBQUMsd0RBQXdELENBQUMsQ0FBQztZQUNoRSxDQUFDO1lBRUQsR0FBRyxDQUFDLHVDQUF1QyxDQUFDLENBQUM7WUFDN0MsT0FBTyxTQUFTLENBQUM7UUFDbkIsQ0FBQztRQUFDLE9BQU8sS0FBSyxFQUFFLENBQUM7WUFDZixHQUFHLENBQUMsc0JBQXNCLEtBQUssWUFBWSxLQUFLLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDLENBQUM7WUFDcEYsT0FBTyxTQUFTLENBQUM7UUFDbkIsQ0FBQztJQUNILENBQUM7SUFFRDs7Ozs7Ozs7O09BU0c7SUFDSSxNQUFNLENBQUMsMEJBQTBCLENBQ3RDLE1BQWMsRUFDZCxnQkFBeUIsS0FBSztRQUU5QixNQUFNLEdBQUcsR0FBRyxDQUFDLE9BQWUsRUFBRSxFQUFFO1lBQzlCLElBQUksYUFBYSxFQUFFLENBQUM7Z0JBQ2xCLE9BQU8sQ0FBQyxHQUFHLENBQUMsd0JBQXdCLE9BQU8sRUFBRSxDQUFDLENBQUM7WUFDakQsQ0FBQztRQUNILENBQUMsQ0FBQztRQUVGLElBQUksQ0FBQztZQUNILCtCQUErQjtZQUMvQixJQUFJLENBQUMsSUFBSSxDQUFDLGFBQWEsQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDO2dCQUNoQyxHQUFHLENBQUMsMkJBQTJCLENBQUMsQ0FBQztnQkFDakMsT0FBTyxTQUFTLENBQUM7WUFDbkIsQ0FBQztZQUVELHdDQUF3QztZQUN4QyxJQUFJLEdBQUcsR0FBRyxDQUFDLENBQUMsQ0FBQyw0QkFBNEI7WUFFekMsK0JBQStCO1lBQy9CLEdBQUcsSUFBSSxDQUFDLENBQUM7WUFFVCxrQ0FBa0M7WUFDbEMsR0FBRyxJQUFJLENBQUMsQ0FBQztZQUVULGdDQUFnQztZQUNoQyxHQUFHLElBQUksQ0FBQyxDQUFDO1lBRVQsZ0NBQWdDO1lBQ2hDLEdBQUcsSUFBSSxFQUFFLENBQUM7WUFFVixrQkFBa0I7WUFDbEIsSUFBSSxHQUFHLEdBQUcsQ0FBQyxHQUFHLE1BQU0sQ0FBQyxNQUFNO2dCQUFFLE9BQU8sU0FBUyxDQUFDO1lBQzlDLE1BQU0sZUFBZSxHQUFHLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQztZQUNwQyxHQUFHLElBQUksQ0FBQyxHQUFHLGVBQWUsQ0FBQztZQUUzQixxQkFBcUI7WUFDckIsSUFBSSxHQUFHLEdBQUcsQ0FBQyxHQUFHLE1BQU0sQ0FBQyxNQUFNO2dCQUFFLE9BQU8sU0FBUyxDQUFDO1lBQzlDLE1BQU0sa0JBQWtCLEdBQUcsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxDQUFDLEdBQUcsTUFBTSxDQUFDLEdBQUcsR0FBRyxDQUFDLENBQUMsQ0FBQztZQUNoRSxHQUFHLElBQUksQ0FBQyxHQUFHLGtCQUFrQixDQUFDO1lBRTlCLDJCQUEyQjtZQUMzQixJQUFJLEdBQUcsR0FBRyxDQUFDLEdBQUcsTUFBTSxDQUFDLE1BQU07Z0JBQUUsT0FBTyxTQUFTLENBQUM7WUFDOUMsTUFBTSx3QkFBd0IsR0FBRyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUM7WUFDN0MsR0FBRyxJQUFJLENBQUMsR0FBRyx3QkFBd0IsQ0FBQztZQUVwQyw4QkFBOEI7WUFDOUIsSUFBSSxHQUFHLEdBQUcsQ0FBQyxHQUFHLE1BQU0sQ0FBQyxNQUFNLEVBQUUsQ0FBQztnQkFDNUIsR0FBRyxDQUFDLHVCQUF1QixDQUFDLENBQUM7Z0JBQzdCLE9BQU8sU0FBUyxDQUFDO1lBQ25CLENBQUM7WUFFRCx3QkFBd0I7WUFDeEIsTUFBTSxnQkFBZ0IsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLENBQUMsR0FBRyxNQUFNLENBQUMsR0FBRyxHQUFHLENBQUMsQ0FBQyxDQUFDO1lBQzlELEdBQUcsSUFBSSxDQUFDLENBQUM7WUFFVCwwQkFBMEI7WUFDMUIsTUFBTSxhQUFhLEdBQUcsR0FBRyxHQUFHLGdCQUFnQixDQUFDO1lBQzdDLElBQUksYUFBYSxHQUFHLE1BQU0sQ0FBQyxNQUFNO2dCQUFFLE9BQU8sU0FBUyxDQUFDO1lBRXBELHlCQUF5QjtZQUN6QixPQUFPLEdBQUcsR0FBRyxDQUFDLElBQUksYUFBYSxFQUFFLENBQUM7Z0JBQ2hDLE1BQU0sYUFBYSxHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsQ0FBQyxHQUFHLE1BQU0sQ0FBQyxHQUFHLEdBQUcsQ0FBQyxDQUFDLENBQUM7Z0JBQzNELEdBQUcsSUFBSSxDQUFDLENBQUM7Z0JBRVQsTUFBTSxlQUFlLEdBQUcsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxDQUFDLEdBQUcsTUFBTSxDQUFDLEdBQUcsR0FBRyxDQUFDLENBQUMsQ0FBQztnQkFDN0QsR0FBRyxJQUFJLENBQUMsQ0FBQztnQkFFVCxJQUFJLGFBQWEsS0FBSyxJQUFJLENBQUMsc0JBQXNCLEVBQUUsQ0FBQztvQkFDbEQsR0FBRyxDQUFDLHFCQUFxQixDQUFDLENBQUM7b0JBRTNCLDJCQUEyQjtvQkFDM0Isa0NBQWtDO29CQUNsQyxJQUFJLEdBQUcsR0FBRyxDQUFDLEdBQUcsYUFBYTt3QkFBRSxNQUFNO29CQUNuQyxNQUFNLGdCQUFnQixHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsQ0FBQyxHQUFHLE1BQU0sQ0FBQyxHQUFHLEdBQUcsQ0FBQyxDQUFDLENBQUM7b0JBQzlELEdBQUcsSUFBSSxDQUFDLENBQUM7b0JBRVQseUJBQXlCO29CQUN6QixNQUFNLGFBQWEsR0FBRyxHQUFHLEdBQUcsZ0JBQWdCLENBQUM7b0JBQzdDLElBQUksYUFBYSxHQUFHLGFBQWE7d0JBQUUsTUFBTTtvQkFFekMsNEJBQTRCO29CQUM1QixPQUFPLEdBQUcsR0FBRyxDQUFDLElBQUksYUFBYSxFQUFFLENBQUM7d0JBQ2hDLDRCQUE0Qjt3QkFDNUIsSUFBSSxHQUFHLEdBQUcsQ0FBQyxHQUFHLGFBQWE7NEJBQUUsTUFBTTt3QkFDbkMsTUFBTSxjQUFjLEdBQUcsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxDQUFDLEdBQUcsTUFBTSxDQUFDLEdBQUcsR0FBRyxDQUFDLENBQUMsQ0FBQzt3QkFDNUQsR0FBRyxJQUFJLENBQUMsQ0FBQzt3QkFFVCxJQUFJLEdBQUcsR0FBRyxjQUFjLEdBQUcsYUFBYTs0QkFBRSxNQUFNO3dCQUVoRCx3Q0FBd0M7d0JBQ3hDLHVEQUF1RDt3QkFDdkQsb0RBQW9EO3dCQUNwRCxJQUFJLGNBQWMsR0FBRyxDQUFDLEVBQUUsQ0FBQzs0QkFDdkIsTUFBTSxRQUFRLEdBQUcsTUFBTSxDQUFDLEtBQUssQ0FBQyxHQUFHLEVBQUUsR0FBRyxHQUFHLGNBQWMsQ0FBQyxDQUFDOzRCQUV6RCxzQkFBc0I7NEJBQ3RCLEdBQUcsSUFBSSxjQUFjLENBQUM7NEJBRXRCLHVDQUF1Qzs0QkFDdkMsSUFBSSxHQUFHLEdBQUcsQ0FBQyxJQUFJLGFBQWEsRUFBRSxDQUFDO2dDQUM3QixHQUFHLElBQUksQ0FBQyxDQUFDOzRCQUNYLENBQUM7aUNBQU0sQ0FBQztnQ0FDTixNQUFNOzRCQUNSLENBQUM7NEJBRUQscUNBQXFDOzRCQUNyQyxJQUFJLENBQUM7Z0NBQ0gsTUFBTSxXQUFXLEdBQUcsUUFBUSxDQUFDLFFBQVEsQ0FBQyxNQUFNLENBQUMsQ0FBQztnQ0FDOUMsR0FBRyxDQUFDLGlCQUFpQixXQUFXLEVBQUUsQ0FBQyxDQUFDO2dDQUVwQyxnREFBZ0Q7Z0NBQ2hELHFEQUFxRDtnQ0FDckQsdUNBQXVDO2dDQUV2QywwQ0FBMEM7Z0NBQzFDLE1BQU0sYUFBYSxHQUNqQiw0RUFBNEUsQ0FBQztnQ0FDL0UsTUFBTSxXQUFXLEdBQUcsV0FBVyxDQUFDLEtBQUssQ0FBQyxhQUFhLENBQUMsQ0FBQztnQ0FDckQsSUFBSSxXQUFXLElBQUksV0FBVyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUM7b0NBQ2xDLEdBQUcsQ0FBQyxpQ0FBaUMsV0FBVyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQztvQ0FDdkQsT0FBTyxXQUFXLENBQUMsQ0FBQyxDQUFDLENBQUM7Z0NBQ3hCLENBQUM7Z0NBRUQscUVBQXFFO2dDQUNyRSxtRUFBbUU7Z0NBQ25FLE1BQU0sS0FBSyxHQUFHLFdBQVcsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUM7Z0NBQ3JDLElBQUksS0FBSyxDQUFDLE1BQU0sR0FBRyxDQUFDLEVBQUUsQ0FBQztvQ0FDckIsS0FBSyxNQUFNLElBQUksSUFBSSxLQUFLLEVBQUUsQ0FBQzt3Q0FDekIsSUFBSSxJQUFJLENBQUMsUUFBUSxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDOzRDQUM5QyxNQUFNLGNBQWMsR0FBRyxJQUFJLENBQUMsSUFBSSxFQUFFLENBQUM7NENBQ25DLElBQUksZ0JBQWdCLENBQUMsSUFBSSxDQUFDLGNBQWMsQ0FBQyxFQUFFLENBQUM7Z0RBQzFDLEdBQUcsQ0FBQyxrREFBa0QsY0FBYyxFQUFFLENBQUMsQ0FBQztnREFDeEUsT0FBTyxjQUFjLENBQUM7NENBQ3hCLENBQUM7d0NBQ0gsQ0FBQztvQ0FDSCxDQUFDO2dDQUNILENBQUM7NEJBQ0gsQ0FBQzs0QkFBQyxPQUFPLENBQUMsRUFBRSxDQUFDO2dDQUNYLEdBQUcsQ0FBQyx1Q0FBdUMsQ0FBQyxDQUFDOzRCQUMvQyxDQUFDO3dCQUNILENBQUM7b0JBQ0gsQ0FBQztnQkFDSCxDQUFDO3FCQUFNLENBQUM7b0JBQ04sc0JBQXNCO29CQUN0QixHQUFHLElBQUksZUFBZSxDQUFDO2dCQUN6QixDQUFDO1lBQ0gsQ0FBQztZQUVELEdBQUcsQ0FBQyxvQ0FBb0MsQ0FBQyxDQUFDO1lBQzFDLE9BQU8sU0FBUyxDQUFDO1FBQ25CLENBQUM7UUFBQyxPQUFPLEtBQUssRUFBRSxDQUFDO1lBQ2YsR0FBRyxDQUFDLHNCQUFzQixLQUFLLFlBQVksS0FBSyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQyxDQUFDO1lBQ3BGLE9BQU8sU0FBUyxDQUFDO1FBQ25CLENBQUM7SUFDSCxDQUFDO0lBRUQ7Ozs7O09BS0c7SUFDSSxNQUFNLENBQUMsWUFBWSxDQUFDLE1BQWMsRUFBRSxnQkFBeUIsS0FBSztRQUN2RSxNQUFNLEdBQUcsR0FBRyxDQUFDLE9BQWUsRUFBRSxFQUFFO1lBQzlCLElBQUksYUFBYSxFQUFFLENBQUM7Z0JBQ2xCLE9BQU8sQ0FBQyxHQUFHLENBQUMsZ0JBQWdCLE9BQU8sRUFBRSxDQUFDLENBQUM7WUFDekMsQ0FBQztRQUNILENBQUMsQ0FBQztRQUVGLElBQUksQ0FBQztZQUNILDZDQUE2QztZQUM3QyxJQUFJLENBQUMsSUFBSSxDQUFDLGFBQWEsQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDO2dCQUNoQyxPQUFPLEtBQUssQ0FBQztZQUNmLENBQUM7WUFFRCw4QkFBOEI7WUFDOUIsSUFBSSxHQUFHLEdBQUcsQ0FBQyxDQUFDLENBQUMsNEJBQTRCO1lBRXpDLCtCQUErQjtZQUMvQixHQUFHLElBQUksQ0FBQyxDQUFDO1lBRVQsa0NBQWtDO1lBQ2xDLEdBQUcsSUFBSSxDQUFDLENBQUM7WUFFVCxnQ0FBZ0M7WUFDaEMsR0FBRyxJQUFJLENBQUMsQ0FBQztZQUVULGdDQUFnQztZQUNoQyxHQUFHLElBQUksRUFBRSxDQUFDO1lBRVYsa0JBQWtCO1lBQ2xCLElBQUksR0FBRyxHQUFHLENBQUMsR0FBRyxNQUFNLENBQUMsTUFBTTtnQkFBRSxPQUFPLEtBQUssQ0FBQztZQUMxQyxNQUFNLGVBQWUsR0FBRyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUM7WUFDcEMsR0FBRyxJQUFJLENBQUMsR0FBRyxlQUFlLENBQUM7WUFFM0IscUJBQXFCO1lBQ3JCLElBQUksR0FBRyxHQUFHLENBQUMsR0FBRyxNQUFNLENBQUMsTUFBTTtnQkFBRSxPQUFPLEtBQUssQ0FBQztZQUMxQyxNQUFNLGtCQUFrQixHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsQ0FBQyxHQUFHLE1BQU0sQ0FBQyxHQUFHLEdBQUcsQ0FBQyxDQUFDLENBQUM7WUFDaEUsR0FBRyxJQUFJLENBQUMsR0FBRyxrQkFBa0IsQ0FBQztZQUU5QiwyQkFBMkI7WUFDM0IsSUFBSSxHQUFHLEdBQUcsQ0FBQyxHQUFHLE1BQU0sQ0FBQyxNQUFNO2dCQUFFLE9BQU8sS0FBSyxDQUFDO1lBQzFDLE1BQU0sd0JBQXdCLEdBQUcsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDO1lBQzdDLEdBQUcsSUFBSSxDQUFDLEdBQUcsd0JBQXdCLENBQUM7WUFFcEMsOEJBQThCO1lBQzlCLElBQUksR0FBRyxHQUFHLENBQUMsR0FBRyxNQUFNLENBQUMsTUFBTTtnQkFBRSxPQUFPLEtBQUssQ0FBQztZQUUxQyx3QkFBd0I7WUFDeEIsTUFBTSxnQkFBZ0IsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLENBQUMsR0FBRyxNQUFNLENBQUMsR0FBRyxHQUFHLENBQUMsQ0FBQyxDQUFDO1lBQzlELEdBQUcsSUFBSSxDQUFDLENBQUM7WUFFVCwwQkFBMEI7WUFDMUIsTUFBTSxhQUFhLEdBQUcsR0FBRyxHQUFHLGdCQUFnQixDQUFDO1lBQzdDLElBQUksYUFBYSxHQUFHLE1BQU0sQ0FBQyxNQUFNO2dCQUFFLE9BQU8sS0FBSyxDQUFDO1lBRWhELGdDQUFnQztZQUNoQyxPQUFPLEdBQUcsR0FBRyxDQUFDLElBQUksYUFBYSxFQUFFLENBQUM7Z0JBQ2hDLE1BQU0sYUFBYSxHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsQ0FBQyxHQUFHLE1BQU0sQ0FBQyxHQUFHLEdBQUcsQ0FBQyxDQUFDLENBQUM7Z0JBQzNELEdBQUcsSUFBSSxDQUFDLENBQUM7Z0JBRVQsTUFBTSxlQUFlLEdBQUcsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxDQUFDLEdBQUcsTUFBTSxDQUFDLEdBQUcsR0FBRyxDQUFDLENBQUMsQ0FBQztnQkFDN0QsR0FBRyxJQUFJLENBQUMsQ0FBQztnQkFFVCxJQUFJLGFBQWEsS0FBSyxJQUFJLENBQUMsNkJBQTZCLEVBQUUsQ0FBQztvQkFDekQsR0FBRyxDQUFDLHVDQUF1QyxDQUFDLENBQUM7b0JBQzdDLE9BQU8sSUFBSSxDQUFDO2dCQUNkLENBQUM7Z0JBRUQseUJBQXlCO2dCQUN6QixHQUFHLElBQUksZUFBZSxDQUFDO1lBQ3pCLENBQUM7WUFFRCxPQUFPLEtBQUssQ0FBQztRQUNmLENBQUM7UUFBQyxPQUFPLEtBQUssRUFBRSxDQUFDO1lBQ2YsR0FBRyxDQUFDLGtDQUFrQyxLQUFLLEVBQUUsQ0FBQyxDQUFDO1lBQy9DLE9BQU8sS0FBSyxDQUFDO1FBQ2YsQ0FBQztJQUNILENBQUM7SUFFRDs7Ozs7Ozs7Ozs7Ozs7OztPQWdCRztJQUNJLE1BQU0sQ0FBQywrQkFBK0IsQ0FDM0MsTUFBYyxFQUNkLGNBS0MsRUFDRCxnQkFBeUIsS0FBSztRQUU5QixNQUFNLEdBQUcsR0FBRyxDQUFDLE9BQWUsRUFBRSxFQUFFO1lBQzlCLElBQUksYUFBYSxFQUFFLENBQUM7Z0JBQ2xCLE9BQU8sQ0FBQyxHQUFHLENBQUMsb0JBQW9CLE9BQU8sRUFBRSxDQUFDLENBQUM7WUFDN0MsQ0FBQztRQUNILENBQUMsQ0FBQztRQUVGLG1DQUFtQztRQUNuQyxJQUFJLGFBQWEsRUFBRSxDQUFDO1lBQ2xCLEdBQUcsQ0FBQyxnQkFBZ0IsTUFBTSxDQUFDLE1BQU0sUUFBUSxDQUFDLENBQUM7WUFDM0MsR0FBRyxDQUFDLHVCQUF1QixNQUFNLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSxJQUFJLENBQUMsR0FBRyxDQUFDLEVBQUUsRUFBRSxNQUFNLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQyxDQUFDO1lBRTNGLElBQUksTUFBTSxDQUFDLE1BQU0sSUFBSSxDQUFDLEVBQUUsQ0FBQztnQkFDdkIsTUFBTSxVQUFVLEdBQUcsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDO2dCQUM3QixNQUFNLFlBQVksR0FBRyxNQUFNLENBQUMsQ0FBQyxDQUFDLENBQUM7Z0JBQy9CLE1BQU0sWUFBWSxHQUFHLE1BQU0sQ0FBQyxDQUFDLENBQUMsQ0FBQztnQkFDL0IsTUFBTSxZQUFZLEdBQUcsQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLEdBQUcsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDO2dCQUVsRCxHQUFHLENBQ0Qsb0JBQW9CLFVBQVUsYUFBYSxZQUFZLElBQUksWUFBWSxZQUFZLFlBQVksRUFBRSxDQUNsRyxDQUFDO1lBQ0osQ0FBQztRQUNILENBQUM7UUFFRCxnREFBZ0Q7UUFDaEQsSUFBSSxhQUFhLEdBQUcsTUFBTSxDQUFDO1FBQzNCLElBQUksY0FBYyxFQUFFLENBQUM7WUFDbkIsTUFBTSxZQUFZLEdBQUcsSUFBSSxDQUFDLGtCQUFrQixDQUFDLGNBQWMsQ0FBQyxDQUFDO1lBQzdELE1BQU0saUJBQWlCLEdBQUcsSUFBSSxDQUFDLDJCQUEyQixDQUN4RCxNQUFNLEVBQ04sWUFBWSxFQUNaLGFBQWEsQ0FDZCxDQUFDO1lBRUYsSUFBSSxDQUFDLGlCQUFpQixFQUFFLENBQUM7Z0JBQ3ZCLEdBQUcsQ0FBQyw0Q0FBNEMsWUFBWSxFQUFFLENBQUMsQ0FBQztnQkFDaEUsT0FBTyxTQUFTLENBQUMsQ0FBQyw4Q0FBOEM7WUFDbEUsQ0FBQztZQUVELGFBQWEsR0FBRyxpQkFBaUIsQ0FBQztZQUNsQyxHQUFHLENBQUMsc0NBQXNDLGFBQWEsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxDQUFDO1FBQ3BFLENBQUM7UUFFRCx3Q0FBd0M7UUFDeEMsTUFBTSxXQUFXLEdBQUcsSUFBSSxDQUFDLFVBQVUsQ0FBQyxhQUFhLEVBQUUsYUFBYSxDQUFDLENBQUM7UUFDbEUsSUFBSSxXQUFXLEVBQUUsQ0FBQztZQUNoQixHQUFHLENBQUMsdUJBQXVCLFdBQVcsRUFBRSxDQUFDLENBQUM7WUFDMUMsT0FBTyxXQUFXLENBQUM7UUFDckIsQ0FBQztRQUVELGtFQUFrRTtRQUNsRSxJQUFJLElBQUksQ0FBQyxhQUFhLENBQUMsYUFBYSxDQUFDLEVBQUUsQ0FBQztZQUN0QyxNQUFNLGNBQWMsR0FBRyxJQUFJLENBQUMsb0JBQW9CLENBQUMsYUFBYSxFQUFFLGFBQWEsQ0FBQyxDQUFDO1lBRS9FLElBQUksY0FBYyxDQUFDLFlBQVksRUFBRSxDQUFDO2dCQUNoQyxHQUFHLENBQUMsaUVBQWlFLENBQUMsQ0FBQztnQkFFdkUsd0NBQXdDO2dCQUN4QyxNQUFNLE1BQU0sR0FBRyxJQUFJLENBQUMsMEJBQTBCLENBQUMsYUFBYSxFQUFFLGFBQWEsQ0FBQyxDQUFDO2dCQUM3RSxJQUFJLE1BQU0sRUFBRSxDQUFDO29CQUNYLEdBQUcsQ0FBQyxxQ0FBcUMsTUFBTSxFQUFFLENBQUMsQ0FBQztvQkFDbkQsT0FBTyxNQUFNLENBQUM7Z0JBQ2hCLENBQUM7WUFDSCxDQUFDO1FBQ0gsQ0FBQztRQUVELG9FQUFvRTtRQUNwRSxJQUFJLElBQUksQ0FBQyxhQUFhLENBQUMsYUFBYSxDQUFDLElBQUksYUFBYSxFQUFFLENBQUM7WUFDdkQsR0FBRyxDQUFDLHdEQUF3RCxDQUFDLENBQUM7WUFFOUQsSUFBSSxhQUFhLENBQUMsTUFBTSxJQUFJLEVBQUUsRUFBRSxDQUFDO2dCQUMvQiwwQ0FBMEM7Z0JBQzFDLE1BQU0sWUFBWSxHQUFHLGFBQWEsQ0FBQyxLQUFLLENBQUMsRUFBRSxFQUFFLEVBQUUsR0FBRyxFQUFFLENBQUMsQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDLENBQUM7Z0JBQ3RFLEdBQUcsQ0FBQyxrQkFBa0IsWUFBWSxFQUFFLENBQUMsQ0FBQztnQkFFdEMscUNBQXFDO2dCQUNyQyxNQUFNLGVBQWUsR0FBRyxhQUFhLENBQUMsRUFBRSxDQUFDLENBQUM7Z0JBQzFDLEdBQUcsQ0FBQyxzQkFBc0IsZUFBZSxFQUFFLENBQUMsQ0FBQztnQkFFN0MsSUFBSSxlQUFlLEdBQUcsQ0FBQyxJQUFJLGFBQWEsQ0FBQyxNQUFNLElBQUksRUFBRSxHQUFHLGVBQWUsRUFBRSxDQUFDO29CQUN4RSxNQUFNLFNBQVMsR0FBRyxhQUFhLENBQUMsS0FBSyxDQUFDLEVBQUUsRUFBRSxFQUFFLEdBQUcsZUFBZSxDQUFDLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxDQUFDO29CQUNoRixHQUFHLENBQUMsZUFBZSxTQUFTLEVBQUUsQ0FBQyxDQUFDO2dCQUNsQyxDQUFDO1lBQ0gsQ0FBQztRQUNILENBQUM7UUFFRCxPQUFPLFNBQVMsQ0FBQztJQUNuQixDQUFDO0lBRUQ7Ozs7Ozs7Ozs7Ozs7T0FhRztJQUNJLE1BQU0sQ0FBQyxnQkFBZ0IsQ0FDNUIsTUFBYyxFQUNkLGNBTUMsRUFDRCxnQkFBeUIsS0FBSyxFQUM5QixTQUFrQjtRQUVsQixNQUFNLEdBQUcsR0FBRyxDQUFDLE9BQWUsRUFBRSxFQUFFO1lBQzlCLElBQUksYUFBYSxFQUFFLENBQUM7Z0JBQ2xCLE9BQU8sQ0FBQyxHQUFHLENBQUMsZ0JBQWdCLE9BQU8sRUFBRSxDQUFDLENBQUM7WUFDekMsQ0FBQztRQUNILENBQUMsQ0FBQztRQUVGLGdDQUFnQztRQUNoQyxJQUFJLENBQUMsY0FBYyxDQUFDLFNBQVMsRUFBRSxDQUFDO1lBQzlCLGNBQWMsQ0FBQyxTQUFTLEdBQUcsSUFBSSxDQUFDLEdBQUcsRUFBRSxDQUFDO1FBQ3hDLENBQUM7UUFFRCx1REFBdUQ7UUFDdkQsSUFBSSxDQUFDLElBQUksQ0FBQyxjQUFjLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsb0JBQW9CLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQztZQUN2RSxHQUFHLENBQUMsZ0RBQWdELENBQUMsQ0FBQztZQUN0RCxPQUFPLFNBQVMsQ0FBQztRQUNuQixDQUFDO1FBRUQsb0NBQW9DO1FBQ3BDLE1BQU0sWUFBWSxHQUFHLElBQUksQ0FBQyxrQkFBa0IsQ0FBQyxjQUFjLENBQUMsQ0FBQztRQUM3RCxHQUFHLENBQUMsd0NBQXdDLFlBQVksb0JBQW9CLE1BQU0sQ0FBQyxNQUFNLEVBQUUsQ0FBQyxDQUFDO1FBRTdGLGtFQUFrRTtRQUNsRSxJQUFJLElBQUksQ0FBQyxvQkFBb0IsQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDO1lBQ3RDLDZDQUE2QztZQUM3QyxJQUFJLFNBQVMsRUFBRSxDQUFDO2dCQUNkLEdBQUcsQ0FBQyxtREFBbUQsU0FBUyxFQUFFLENBQUMsQ0FBQztnQkFDcEUsT0FBTyxTQUFTLENBQUM7WUFDbkIsQ0FBQztZQUVELEdBQUcsQ0FBQyx1RUFBdUUsQ0FBQyxDQUFDO1lBQzdFLE9BQU8sU0FBUyxDQUFDO1FBQ25CLENBQUM7UUFFRCx3Q0FBd0M7UUFDeEMsSUFBSSxJQUFJLENBQUMsYUFBYSxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUM7WUFDL0IsTUFBTSxjQUFjLEdBQUcsSUFBSSxDQUFDLG9CQUFvQixDQUFDLE1BQU0sRUFBRSxhQUFhLENBQUMsQ0FBQztZQUV4RSxJQUFJLGNBQWMsQ0FBQyxZQUFZLEVBQUUsQ0FBQztnQkFDaEMsR0FBRyxDQUFDLDJDQUEyQyxDQUFDLENBQUM7Z0JBRWpELDJDQUEyQztnQkFDM0MsTUFBTSxXQUFXLEdBQUcsSUFBSSxDQUFDLFVBQVUsQ0FBQyxNQUFNLEVBQUUsYUFBYSxDQUFDLENBQUM7Z0JBQzNELElBQUksV0FBVyxFQUFFLENBQUM7b0JBQ2hCLEdBQUcsQ0FBQyw2Q0FBNkMsV0FBVyxFQUFFLENBQUMsQ0FBQztvQkFDaEUsT0FBTyxXQUFXLENBQUM7Z0JBQ3JCLENBQUM7Z0JBRUQsNkNBQTZDO2dCQUM3QyxtQ0FBbUM7Z0JBQ25DLE1BQU0sTUFBTSxHQUFHLElBQUksQ0FBQywwQkFBMEIsQ0FBQyxNQUFNLEVBQUUsYUFBYSxDQUFDLENBQUM7Z0JBQ3RFLElBQUksTUFBTSxFQUFFLENBQUM7b0JBQ1gsR0FBRyxDQUFDLHFDQUFxQyxNQUFNLEVBQUUsQ0FBQyxDQUFDO29CQUNuRCxPQUFPLE1BQU0sQ0FBQztnQkFDaEIsQ0FBQztnQkFFRCw4Q0FBOEM7Z0JBQzlDLElBQUksYUFBYSxFQUFFLENBQUM7b0JBQ2xCLEdBQUcsQ0FBQyxtRUFBbUUsQ0FBQyxDQUFDO29CQUN6RSxpQ0FBaUM7b0JBQ2pDLElBQUksQ0FBQzt3QkFDSCxzQ0FBc0M7d0JBQ3RDLEdBQUcsQ0FBQyxxQ0FBcUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLEVBQUUsR0FBRyxDQUFDLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUMsQ0FBQzt3QkFFakYsbURBQW1EO3dCQUNuRCxNQUFNLFNBQVMsR0FBRyxNQUFNLENBQUMsUUFBUSxDQUFDLE1BQU0sRUFBRSxDQUFDLEVBQUUsTUFBTSxDQUFDLE1BQU0sQ0FBQyxDQUFDO3dCQUM1RCxNQUFNLGVBQWUsR0FDbkIsNkVBQTZFLENBQUM7d0JBQ2hGLE1BQU0sV0FBVyxHQUFHLFNBQVMsQ0FBQyxLQUFLLENBQUMsZUFBZSxDQUFDLENBQUM7d0JBRXJELElBQUksV0FBVyxJQUFJLFdBQVcsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxFQUFFLENBQUM7NEJBQzFDLEdBQUcsQ0FBQyx1Q0FBdUMsV0FBVyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLENBQUM7NEJBRXJFLCtDQUErQzs0QkFDL0MsS0FBSyxNQUFNLEtBQUssSUFBSSxXQUFXLEVBQUUsQ0FBQztnQ0FDaEMsSUFBSSxLQUFLLENBQUMsUUFBUSxDQUFDLEdBQUcsQ0FBQyxJQUFJLEtBQUssQ0FBQyxNQUFNLEdBQUcsQ0FBQyxFQUFFLENBQUM7b0NBQzVDLEdBQUcsQ0FBQyx3Q0FBd0MsS0FBSyxFQUFFLENBQUMsQ0FBQztvQ0FDckQsd0RBQXdEO2dDQUMxRCxDQUFDOzRCQUNILENBQUM7d0JBQ0gsQ0FBQztvQkFDSCxDQUFDO29CQUFDLE9BQU8sQ0FBQyxFQUFFLENBQUM7d0JBQ1gsR0FBRyxDQUFDLGdDQUFnQyxDQUFDLEVBQUUsQ0FBQyxDQUFDO29CQUMzQyxDQUFDO2dCQUNILENBQUM7Z0JBRUQsR0FBRyxDQUFDLDRDQUE0QyxDQUFDLENBQUM7Z0JBQ2xELDREQUE0RDtZQUM5RCxDQUFDO1FBQ0gsQ0FBQztRQUVELDBEQUEwRDtRQUMxRCxNQUFNLEdBQUcsR0FBRyxJQUFJLENBQUMsK0JBQStCLENBQUMsTUFBTSxFQUFFLGNBQWMsRUFBRSxhQUFhLENBQUMsQ0FBQztRQUV4RixJQUFJLEdBQUcsRUFBRSxDQUFDO1lBQ1IsR0FBRyxDQUFDLCtCQUErQixHQUFHLEVBQUUsQ0FBQyxDQUFDO1lBQzFDLE9BQU8sR0FBRyxDQUFDO1FBQ2IsQ0FBQztRQUVELHNFQUFzRTtRQUN0RSxJQUFJLElBQUksQ0FBQyxhQUFhLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQztZQUMvQixHQUFHLENBQUMseUVBQXlFLENBQUMsQ0FBQztRQUNqRixDQUFDO1FBRUQsT0FBTyxTQUFTLENBQUM7SUFDbkIsQ0FBQyJ9
|