From 91612c4d5eb44c79510e1c47399054432295d2fa Mon Sep 17 00:00:00 2001 From: Bartlomiej Obecny Date: Tue, 8 Dec 2020 14:27:02 +0100 Subject: [PATCH 1/4] chore: moving plugin from api to core (#1715) --- packages/opentelemetry-api/src/index.ts | 1 - packages/opentelemetry-core/src/index.ts | 1 + .../src/platform/BaseAbstractPlugin.ts | 10 ++-------- .../src/platform/browser/BasePlugin.ts | 8 ++------ .../opentelemetry-core/src/platform/node/BasePlugin.ts | 5 ++--- .../src/trace}/Plugin.ts | 3 +-- .../opentelemetry-grpc-utils/test/grpcUtils.test.ts | 8 ++++++-- .../src/instrumentation/PluginLoader.ts | 8 ++------ packages/opentelemetry-plugin-fetch/src/fetch.ts | 2 +- packages/opentelemetry-plugin-grpc-js/src/types.ts | 2 +- packages/opentelemetry-plugin-grpc/src/types.ts | 2 +- packages/opentelemetry-plugin-http/src/types.ts | 3 ++- 12 files changed, 21 insertions(+), 32 deletions(-) rename packages/{opentelemetry-api/src/trace/instrumentation => opentelemetry-core/src/trace}/Plugin.ts (96%) diff --git a/packages/opentelemetry-api/src/index.ts b/packages/opentelemetry-api/src/index.ts index e290b8a6c7..09fc2f2b24 100644 --- a/packages/opentelemetry-api/src/index.ts +++ b/packages/opentelemetry-api/src/index.ts @@ -33,7 +33,6 @@ export * from './metrics/Observation'; export * from './metrics/ObserverResult'; export * from './trace/attributes'; export * from './trace/Event'; -export * from './trace/instrumentation/Plugin'; export * from './trace/link_context'; export * from './trace/link'; export * from './trace/NoopLogger'; diff --git a/packages/opentelemetry-core/src/index.ts b/packages/opentelemetry-core/src/index.ts index e3a836e1b2..0835399ec1 100644 --- a/packages/opentelemetry-core/src/index.ts +++ b/packages/opentelemetry-core/src/index.ts @@ -30,6 +30,7 @@ export * from './correlation-context/correlation-context'; export * from './correlation-context/propagation/HttpCorrelationContext'; export * from './platform'; export * from './trace/NoRecordingSpan'; +export * from './trace/Plugin'; export * from './trace/sampler/AlwaysOffSampler'; export * from './trace/sampler/AlwaysOnSampler'; export * from './trace/sampler/ParentBasedSampler'; diff --git a/packages/opentelemetry-core/src/platform/BaseAbstractPlugin.ts b/packages/opentelemetry-core/src/platform/BaseAbstractPlugin.ts index 7a334310fd..d6dcda1e47 100644 --- a/packages/opentelemetry-core/src/platform/BaseAbstractPlugin.ts +++ b/packages/opentelemetry-core/src/platform/BaseAbstractPlugin.ts @@ -14,14 +14,8 @@ * limitations under the License. */ -import { - Tracer, - Plugin, - Logger, - PluginConfig, - TracerProvider, - PluginInternalFiles, -} from '@opentelemetry/api'; +import { Tracer, Logger, TracerProvider } from '@opentelemetry/api'; +import { Plugin, PluginConfig, PluginInternalFiles } from '../trace/Plugin'; /** This class represent the base to patch plugin. */ export abstract class BaseAbstractPlugin implements Plugin { diff --git a/packages/opentelemetry-core/src/platform/browser/BasePlugin.ts b/packages/opentelemetry-core/src/platform/browser/BasePlugin.ts index bf6e227d8a..124495b0a3 100644 --- a/packages/opentelemetry-core/src/platform/browser/BasePlugin.ts +++ b/packages/opentelemetry-core/src/platform/browser/BasePlugin.ts @@ -14,12 +14,8 @@ * limitations under the License. */ -import { - Logger, - Plugin, - PluginConfig, - TracerProvider, -} from '@opentelemetry/api'; +import { Logger, TracerProvider } from '@opentelemetry/api'; +import { Plugin, PluginConfig } from '../../trace/Plugin'; import { BaseAbstractPlugin } from '../BaseAbstractPlugin'; /** This class represent the base to patch plugin. */ diff --git a/packages/opentelemetry-core/src/platform/node/BasePlugin.ts b/packages/opentelemetry-core/src/platform/node/BasePlugin.ts index 3581cee96a..28a4c3e322 100644 --- a/packages/opentelemetry-core/src/platform/node/BasePlugin.ts +++ b/packages/opentelemetry-core/src/platform/node/BasePlugin.ts @@ -14,14 +14,13 @@ * limitations under the License. */ +import { Logger, TracerProvider } from '@opentelemetry/api'; import { Plugin, - Logger, PluginConfig, PluginInternalFiles, PluginInternalFilesVersion, - TracerProvider, -} from '@opentelemetry/api'; +} from '../../trace/Plugin'; import * as semver from 'semver'; import * as path from 'path'; import { BaseAbstractPlugin } from '../BaseAbstractPlugin'; diff --git a/packages/opentelemetry-api/src/trace/instrumentation/Plugin.ts b/packages/opentelemetry-core/src/trace/Plugin.ts similarity index 96% rename from packages/opentelemetry-api/src/trace/instrumentation/Plugin.ts rename to packages/opentelemetry-core/src/trace/Plugin.ts index 11d96fed2b..6f7efdec2b 100644 --- a/packages/opentelemetry-api/src/trace/instrumentation/Plugin.ts +++ b/packages/opentelemetry-core/src/trace/Plugin.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { Logger } from '../../common/Logger'; -import { TracerProvider } from '../tracer_provider'; +import { Logger, TracerProvider } from '@opentelemetry/api'; /** Interface Plugin to apply patch. */ export interface Plugin { diff --git a/packages/opentelemetry-grpc-utils/test/grpcUtils.test.ts b/packages/opentelemetry-grpc-utils/test/grpcUtils.test.ts index 5fd6c25ad9..c2f877455a 100644 --- a/packages/opentelemetry-grpc-utils/test/grpcUtils.test.ts +++ b/packages/opentelemetry-grpc-utils/test/grpcUtils.test.ts @@ -19,9 +19,13 @@ import { NoopTracerProvider, SpanKind, propagation, - PluginConfig, } from '@opentelemetry/api'; -import { NoopLogger, HttpTraceContext, BasePlugin } from '@opentelemetry/core'; +import { + NoopLogger, + HttpTraceContext, + BasePlugin, + PluginConfig, +} from '@opentelemetry/core'; import { NodeTracerProvider } from '@opentelemetry/node'; import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; import { ContextManager } from '@opentelemetry/context-base'; diff --git a/packages/opentelemetry-node/src/instrumentation/PluginLoader.ts b/packages/opentelemetry-node/src/instrumentation/PluginLoader.ts index 145cafe88a..e8ea58d879 100644 --- a/packages/opentelemetry-node/src/instrumentation/PluginLoader.ts +++ b/packages/opentelemetry-node/src/instrumentation/PluginLoader.ts @@ -14,12 +14,8 @@ * limitations under the License. */ -import { - Logger, - Plugin, - PluginConfig, - TracerProvider, -} from '@opentelemetry/api'; +import { Logger, TracerProvider } from '@opentelemetry/api'; +import { Plugin, PluginConfig } from '@opentelemetry/core'; import * as hook from 'require-in-the-middle'; import * as utils from './utils'; diff --git a/packages/opentelemetry-plugin-fetch/src/fetch.ts b/packages/opentelemetry-plugin-fetch/src/fetch.ts index 20b28ea833..6860b83c01 100644 --- a/packages/opentelemetry-plugin-fetch/src/fetch.ts +++ b/packages/opentelemetry-plugin-fetch/src/fetch.ts @@ -32,7 +32,7 @@ const OBSERVER_WAIT_TIME_MS = 300; /** * FetchPlugin Config */ -export interface FetchPluginConfig extends api.PluginConfig { +export interface FetchPluginConfig extends core.PluginConfig { // the number of timing resources is limited, after the limit // (chrome 250, safari 150) the information is not collected anymore // the only way to prevent that is to regularly clean the resources diff --git a/packages/opentelemetry-plugin-grpc-js/src/types.ts b/packages/opentelemetry-plugin-grpc-js/src/types.ts index 0e26b59ddf..c9104eb08d 100644 --- a/packages/opentelemetry-plugin-grpc-js/src/types.ts +++ b/packages/opentelemetry-plugin-grpc-js/src/types.ts @@ -17,7 +17,7 @@ import type * as grpcJs from '@grpc/grpc-js'; import type { EventEmitter } from 'events'; import type { CALL_SPAN_ENDED } from './utils'; -import { PluginConfig } from '@opentelemetry/api'; +import { PluginConfig } from '@opentelemetry/core'; export type IgnoreMatcher = string | RegExp | ((str: string) => boolean); diff --git a/packages/opentelemetry-plugin-grpc/src/types.ts b/packages/opentelemetry-plugin-grpc/src/types.ts index e630fa9d95..d85baab944 100644 --- a/packages/opentelemetry-plugin-grpc/src/types.ts +++ b/packages/opentelemetry-plugin-grpc/src/types.ts @@ -16,7 +16,7 @@ import * as grpcModule from 'grpc'; import * as events from 'events'; -import { PluginConfig } from '@opentelemetry/api'; +import { PluginConfig } from '@opentelemetry/core'; export type grpc = typeof grpcModule; diff --git a/packages/opentelemetry-plugin-http/src/types.ts b/packages/opentelemetry-plugin-http/src/types.ts index cc41781754..5f675cbd6f 100644 --- a/packages/opentelemetry-plugin-http/src/types.ts +++ b/packages/opentelemetry-plugin-http/src/types.ts @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { PluginConfig, Span } from '@opentelemetry/api'; +import { Span } from '@opentelemetry/api'; +import { PluginConfig } from '@opentelemetry/core'; import type * as http from 'http'; import { ClientRequest, From e941f55bc4bdd4a98df8cdcc8356cc7c6164bfa4 Mon Sep 17 00:00:00 2001 From: Brad Frost Date: Wed, 9 Dec 2020 10:46:51 -0500 Subject: [PATCH 2/4] fix(xhr): check for resource timing support (#1720) --- .../src/xhr.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/opentelemetry-instrumentation-xml-http-request/src/xhr.ts b/packages/opentelemetry-instrumentation-xml-http-request/src/xhr.ts index 70fa9cf5d8..2ee1d0afe8 100644 --- a/packages/opentelemetry-instrumentation-xml-http-request/src/xhr.ts +++ b/packages/opentelemetry-instrumentation-xml-http-request/src/xhr.ts @@ -168,7 +168,11 @@ export class XMLHttpRequestInstrumentation extends InstrumentationBase Date: Wed, 9 Dec 2020 20:33:33 +0100 Subject: [PATCH 3/4] feat: migrate http and https plugin to instrumentation #1658 (#1671) Co-authored-by: Daniel Dyla --- .circleci/config.yml | 1 + .../.eslintignore | 1 + .../.eslintrc.js | 7 + .../.npmignore | 4 + .../LICENSE | 201 +++++ .../README.md | 85 ++ .../package.json | 80 ++ .../src/http.ts | 628 +++++++++++++ .../src/index.ts | 19 + .../src/types.ts | 98 +++ .../src/utils.ts | 429 +++++++++ .../src/version.ts | 18 + .../test/fixtures/google-http.json | 43 + .../test/fixtures/google-https.json | 43 + .../test/fixtures/server-cert.pem | 11 + .../test/fixtures/server-key.pem | 15 + .../test/functionals/http-disable.test.ts | 91 ++ .../test/functionals/http-enable.test.ts | 827 ++++++++++++++++++ .../test/functionals/http-package.test.ts | 153 ++++ .../test/functionals/https-disable.test.ts | 99 +++ .../test/functionals/https-enable.test.ts | 645 ++++++++++++++ .../test/functionals/https-package.test.ts | 154 ++++ .../test/functionals/utils.test.ts | 311 +++++++ .../test/integrations/http-enable.test.ts | 306 +++++++ .../test/integrations/https-enable.test.ts | 305 +++++++ .../test/utils/DummyPropagation.ts | 52 ++ .../test/utils/assertSpan.ts | 131 +++ .../test/utils/httpRequest.ts | 68 ++ .../test/utils/httpsRequest.ts | 72 ++ .../test/utils/utils.ts | 26 + .../tsconfig.json | 11 + .../src/platform/node/instrumentation.ts | 1 + .../instrumentationNodeModuleDefinition.ts | 2 +- .../src/utils.ts | 5 +- .../test/common/utils.test.ts | 44 +- 35 files changed, 4982 insertions(+), 4 deletions(-) create mode 100644 packages/opentelemetry-instrumentation-http/.eslintignore create mode 100644 packages/opentelemetry-instrumentation-http/.eslintrc.js create mode 100644 packages/opentelemetry-instrumentation-http/.npmignore create mode 100644 packages/opentelemetry-instrumentation-http/LICENSE create mode 100644 packages/opentelemetry-instrumentation-http/README.md create mode 100644 packages/opentelemetry-instrumentation-http/package.json create mode 100644 packages/opentelemetry-instrumentation-http/src/http.ts create mode 100644 packages/opentelemetry-instrumentation-http/src/index.ts create mode 100644 packages/opentelemetry-instrumentation-http/src/types.ts create mode 100644 packages/opentelemetry-instrumentation-http/src/utils.ts create mode 100644 packages/opentelemetry-instrumentation-http/src/version.ts create mode 100644 packages/opentelemetry-instrumentation-http/test/fixtures/google-http.json create mode 100644 packages/opentelemetry-instrumentation-http/test/fixtures/google-https.json create mode 100644 packages/opentelemetry-instrumentation-http/test/fixtures/server-cert.pem create mode 100644 packages/opentelemetry-instrumentation-http/test/fixtures/server-key.pem create mode 100644 packages/opentelemetry-instrumentation-http/test/functionals/http-disable.test.ts create mode 100644 packages/opentelemetry-instrumentation-http/test/functionals/http-enable.test.ts create mode 100644 packages/opentelemetry-instrumentation-http/test/functionals/http-package.test.ts create mode 100644 packages/opentelemetry-instrumentation-http/test/functionals/https-disable.test.ts create mode 100644 packages/opentelemetry-instrumentation-http/test/functionals/https-enable.test.ts create mode 100644 packages/opentelemetry-instrumentation-http/test/functionals/https-package.test.ts create mode 100644 packages/opentelemetry-instrumentation-http/test/functionals/utils.test.ts create mode 100644 packages/opentelemetry-instrumentation-http/test/integrations/http-enable.test.ts create mode 100644 packages/opentelemetry-instrumentation-http/test/integrations/https-enable.test.ts create mode 100644 packages/opentelemetry-instrumentation-http/test/utils/DummyPropagation.ts create mode 100644 packages/opentelemetry-instrumentation-http/test/utils/assertSpan.ts create mode 100644 packages/opentelemetry-instrumentation-http/test/utils/httpRequest.ts create mode 100644 packages/opentelemetry-instrumentation-http/test/utils/httpsRequest.ts create mode 100644 packages/opentelemetry-instrumentation-http/test/utils/utils.ts create mode 100644 packages/opentelemetry-instrumentation-http/tsconfig.json diff --git a/.circleci/config.yml b/.circleci/config.yml index c47b2d6439..3fd0472dc2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -31,6 +31,7 @@ cache_2: &cache_2 - packages/opentelemetry-plugin-http/node_modules - packages/opentelemetry-plugin-https/node_modules - packages/opentelemetry-exporter-collector/node_modules + - packages/opentelemetry-instrumentation-http/node_modules - packages/opentelemetry-instrumentation-xml-http-request/node_modules - packages/opentelemetry-resource-detector-aws/node_modules - packages/opentelemetry-resource-detector-gcp/node_modules diff --git a/packages/opentelemetry-instrumentation-http/.eslintignore b/packages/opentelemetry-instrumentation-http/.eslintignore new file mode 100644 index 0000000000..378eac25d3 --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/.eslintignore @@ -0,0 +1 @@ +build diff --git a/packages/opentelemetry-instrumentation-http/.eslintrc.js b/packages/opentelemetry-instrumentation-http/.eslintrc.js new file mode 100644 index 0000000000..f726f3becb --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + "env": { + "mocha": true, + "node": true + }, + ...require('../../eslint.config.js') +} diff --git a/packages/opentelemetry-instrumentation-http/.npmignore b/packages/opentelemetry-instrumentation-http/.npmignore new file mode 100644 index 0000000000..9505ba9450 --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/.npmignore @@ -0,0 +1,4 @@ +/bin +/coverage +/doc +/test diff --git a/packages/opentelemetry-instrumentation-http/LICENSE b/packages/opentelemetry-instrumentation-http/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/opentelemetry-instrumentation-http/README.md b/packages/opentelemetry-instrumentation-http/README.md new file mode 100644 index 0000000000..1a4332ab3a --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/README.md @@ -0,0 +1,85 @@ +# OpenTelemetry HTTP and HTTPS Instrumentation for Node.js + +[![Gitter chat][gitter-image]][gitter-url] +[![NPM Published Version][npm-img]][npm-url] +[![dependencies][dependencies-image]][dependencies-url] +[![devDependencies][devDependencies-image]][devDependencies-url] +[![Apache License][license-image]][license-image] + +This module provides automatic instrumentation for [`http`](https://nodejs.org/api/http.html) and [`https`](https://nodejs.org/api/https.html). + +For automatic instrumentation see the +[@opentelemetry/node](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-node) package. + +## Installation + +```bash +npm install --save @opentelemetry/instrumentation-http +``` + +## Usage + +OpenTelemetry HTTP Instrumentation allows the user to automatically collect trace data and export them to their backend of choice, to give observability to distributed systems. + +To load a specific instrumentation (HTTP in this case), specify it in the Node Tracer's configuration. + +```js +const { HttpInstrumentation } = require('@opentelemetry/instrumentation-graphql'); + +const { ConsoleSpanExporter, SimpleSpanProcessor } = require('@opentelemetry/tracing'); +const { NodeTracerProvider } = require('@opentelemetry/node'); + +const provider = new NodeTracerProvider({ + // be sure to disable old plugins + plugins: { + http: { enabled: false, path: '@opentelemetry/plugin-http' }, + https: { enabled: false, path: '@opentelemetry/plugin-https' } + }, +}); + +const httpInstrumentation = new HttpInstrumentation({ + // see under for available configuration +}); +httpInstrumentation.enable(); + +provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter())); +provider.register(); +``` + +See [examples/http](https://github.com/open-telemetry/opentelemetry-js/tree/master/examples/http) for a short example. + +### Http instrumentation Options + +Http instrumentation has few options available to choose from. You can set the following: + +| Options | Type | Description | +| ------- | ---- | ----------- | +| [`applyCustomAttributesOnSpan`](https://github.com/open-telemetry/opentelemetry-js/blob/master/packages/opentelemetry-instrumentation-http/src/types.ts#L79) | `HttpCustomAttributeFunction` | Function for adding custom attributes | +| [`requestHook`](https://github.com/open-telemetry/opentelemetry-js/blob/master/packages/opentelemetry-instrumentation-http/src/types.ts#81) | `HttpRequestCustomAttributeFunction` | Function for adding custom attributes before request is handled | +| [`responseHook`](https://github.com/open-telemetry/opentelemetry-js/blob/master/packages/opentelemetry-instrumentation-http/src/types.ts#L83) | `HttpResponseCustomAttributeFunction` | Function for adding custom attributes before response is handled | +| [`ignoreIncomingPaths`](https://github.com/open-telemetry/opentelemetry-js/blob/master/packages/opentelemetry-instrumentation-http/src/types.ts#L75) | `IgnoreMatcher[]` | Http instrumentation will not trace all incoming requests that match paths | +| [`ignoreOutgoingUrls`](https://github.com/open-telemetry/opentelemetry-js/blob/master/packages/opentelemetry-instrumentation-http/src/types.ts#L77) | `IgnoreMatcher[]` | Http instrumentation will not trace all outgoing requests that match urls | +| [`serverName`](https://github.com/open-telemetry/opentelemetry-js/blob/master/packages/opentelemetry-instrumentation-http/src/types.ts#L85) | `string` | The primary server name of the matched virtual host. | +| `requireParentforOutgoingSpans` | Boolean | Require that is a parent span to create new span for outgoing requests. | +| `requireParentforIncomingSpans` | Boolean | Require that is a parent span to create new span for incoming requests. | + +## Useful links + +- For more information on OpenTelemetry, visit: +- For more about OpenTelemetry JavaScript: +- For help or feedback on this project, join us on [gitter][gitter-url] + +## License + +Apache 2.0 - See [LICENSE][license-url] for more information. + +[gitter-image]: https://badges.gitter.im/open-telemetry/opentelemetry-js.svg +[gitter-url]: https://gitter.im/open-telemetry/opentelemetry-node?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge +[license-url]: https://github.com/open-telemetry/opentelemetry-js/blob/master/LICENSE +[license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat +[dependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/status.svg?path=packages/opentelemetry-instrumentation-http +[dependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-instrumentation-http +[devDependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/dev-status.svg?path=packages/opentelemetry-instrumentation-http +[devDependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-instrumentation-http&type=dev +[npm-url]: https://www.npmjs.com/package/@opentelemetry/instrumentation-http +[npm-img]: https://badge.fury.io/js/%40opentelemetry%instrumentation-http.svg diff --git a/packages/opentelemetry-instrumentation-http/package.json b/packages/opentelemetry-instrumentation-http/package.json new file mode 100644 index 0000000000..a627ab2e98 --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/package.json @@ -0,0 +1,80 @@ +{ + "name": "@opentelemetry/instrumentation-http", + "version": "0.13.0", + "description": "OpenTelemetry http/https automatic instrumentation package.", + "main": "build/src/index.js", + "types": "build/src/index.d.ts", + "repository": "open-telemetry/opentelemetry-js", + "scripts": { + "test": "nyc ts-mocha -p tsconfig.json test/**/*.test.ts", + "tdd": "npm run test -- --watch-extensions ts --watch", + "clean": "rimraf build/*", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../", + "precompile": "tsc --version", + "version:update": "node ../../scripts/version-update.js", + "compile": "npm run version:update && tsc -p .", + "prepare": "npm run compile", + "watch": "tsc -w" + }, + "keywords": [ + "opentelemetry", + "http", + "nodejs", + "tracing", + "profiling", + "instrumentation" + ], + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + }, + "files": [ + "build/src/**/*.js", + "build/src/**/*.js.map", + "build/src/**/*.d.ts", + "doc", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@opentelemetry/context-async-hooks": "^0.13.0", + "@opentelemetry/context-base": "^0.13.0", + "@opentelemetry/node": "^0.13.0", + "@opentelemetry/tracing": "^0.13.0", + "@types/got": "9.6.11", + "@types/mocha": "8.0.4", + "@types/node": "14.14.10", + "@types/request-promise-native": "1.0.17", + "@types/semver": "7.3.4", + "@types/sinon": "9.0.9", + "@types/superagent": "4.1.10", + "axios": "0.21.0", + "codecov": "3.8.1", + "got": "9.6.0", + "gts": "2.0.2", + "mocha": "7.2.0", + "nock": "12.0.3", + "nyc": "15.1.0", + "request": "2.88.2", + "request-promise-native": "1.0.9", + "rimraf": "3.0.2", + "sinon": "9.2.1", + "superagent": "6.1.0", + "ts-mocha": "8.0.0", + "ts-node": "9.0.0", + "typescript": "3.9.7" + }, + "dependencies": { + "@opentelemetry/api": "^0.13.0", + "@opentelemetry/core": "^0.13.0", + "@opentelemetry/instrumentation": "^0.13.0", + "@opentelemetry/semantic-conventions": "^0.13.0", + "semver": "^7.1.3" + } +} diff --git a/packages/opentelemetry-instrumentation-http/src/http.ts b/packages/opentelemetry-instrumentation-http/src/http.ts new file mode 100644 index 0000000000..072360dbb2 --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/src/http.ts @@ -0,0 +1,628 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + StatusCode, + context, + propagation, + Span, + SpanKind, + SpanOptions, + Status, + setActiveSpan, + SpanContext, + TraceFlags, +} from '@opentelemetry/api'; +import { NoRecordingSpan } from '@opentelemetry/core'; +import type * as http from 'http'; +import type * as https from 'https'; +import { Socket } from 'net'; +import * as semver from 'semver'; +import * as url from 'url'; +import { + Err, + Func, + Http, + HttpInstrumentationConfig, + HttpRequestArgs, + Https, + ParsedRequestOptions, + ResponseEndArgs, +} from './types'; +import * as utils from './utils'; +import { VERSION } from './version'; +import { + InstrumentationBase, + InstrumentationConfig, + InstrumentationNodeModuleDefinition, + isWrapped, + safeExecuteInTheMiddle, +} from '@opentelemetry/instrumentation'; + +/** + * Http instrumentation instrumentation for Opentelemetry + */ +export class HttpInstrumentation extends InstrumentationBase { + /** keep track on spans not ended */ + private readonly _spanNotEnded: WeakSet = new WeakSet(); + private readonly _version = process.versions.node; + private readonly _emptySpanContext: SpanContext = { + traceId: '', + spanId: '', + traceFlags: TraceFlags.NONE, + }; + + constructor(config: HttpInstrumentationConfig & InstrumentationConfig = {}) { + super( + '@opentelemetry/instrumentation-http', + VERSION, + Object.assign({}, config) + ); + } + + private _getConfig(): HttpInstrumentationConfig { + return this._config; + } + + setConfig(config: HttpInstrumentationConfig & InstrumentationConfig = {}) { + this._config = Object.assign({}, config); + } + + init() { + return [this._getHttpsInstrumentation(), this._getHttpInstrumentation()]; + } + + private _getHttpInstrumentation() { + return new InstrumentationNodeModuleDefinition( + 'http', + ['*'], + moduleExports => { + this._logger.debug(`Applying patch for http@${this._version}`); + if (isWrapped(moduleExports.request)) { + this._unwrap(moduleExports, 'request'); + } + this._wrap( + moduleExports, + 'request', + this._getPatchOutgoingRequestFunction('http') + ); + if (isWrapped(moduleExports.get)) { + this._unwrap(moduleExports, 'get'); + } + this._wrap( + moduleExports, + 'get', + this._getPatchOutgoingGetFunction(moduleExports.request) + ); + if (isWrapped(moduleExports.Server.prototype.emit)) { + this._unwrap(moduleExports.Server.prototype, 'emit'); + } + this._wrap( + moduleExports.Server.prototype, + 'emit', + this._getPatchIncomingRequestFunction('http') + ); + return moduleExports; + }, + moduleExports => { + if (moduleExports === undefined) return; + this._logger.debug(`Removing patch for http@${this._version}`); + + this._unwrap(moduleExports, 'request'); + this._unwrap(moduleExports, 'get'); + this._unwrap(moduleExports.Server.prototype, 'emit'); + } + ); + } + + private _getHttpsInstrumentation() { + return new InstrumentationNodeModuleDefinition( + 'https', + ['*'], + moduleExports => { + this._logger.debug(`Applying patch for https@${this._version}`); + if (isWrapped(moduleExports.request)) { + this._unwrap(moduleExports, 'request'); + } + this._wrap( + moduleExports, + 'request', + this._getPatchHttpsOutgoingRequestFunction('https') + ); + if (isWrapped(moduleExports.get)) { + this._unwrap(moduleExports, 'get'); + } + this._wrap( + moduleExports, + 'get', + this._getPatchHttpsOutgoingGetFunction(moduleExports.request) + ); + if (isWrapped(moduleExports.Server.prototype.emit)) { + this._unwrap(moduleExports.Server.prototype, 'emit'); + } + this._wrap( + moduleExports.Server.prototype, + 'emit', + this._getPatchIncomingRequestFunction('https') + ); + return moduleExports; + }, + moduleExports => { + if (moduleExports === undefined) return; + this._logger.debug(`Removing patch for https@${this._version}`); + + this._unwrap(moduleExports, 'request'); + this._unwrap(moduleExports, 'get'); + this._unwrap(moduleExports.Server.prototype, 'emit'); + } + ); + } + + /** + * Creates spans for incoming requests, restoring spans' context if applied. + */ + protected _getPatchIncomingRequestFunction(component: 'http' | 'https') { + return (original: (event: string, ...args: unknown[]) => boolean) => { + return this._incomingRequestFunction(component, original); + }; + } + + /** + * Creates spans for outgoing requests, sending spans' context for distributed + * tracing. + */ + protected _getPatchOutgoingRequestFunction(component: 'http' | 'https') { + return (original: Func): Func => { + return this._outgoingRequestFunction(component, original); + }; + } + + protected _getPatchOutgoingGetFunction( + clientRequest: ( + options: http.RequestOptions | string | url.URL, + ...args: HttpRequestArgs + ) => http.ClientRequest + ) { + return (_original: Func): Func => { + // Re-implement http.get. This needs to be done (instead of using + // getPatchOutgoingRequestFunction to patch it) because we need to + // set the trace context header before the returned http.ClientRequest is + // ended. The Node.js docs state that the only differences between + // request and get are that (1) get defaults to the HTTP GET method and + // (2) the returned request object is ended immediately. The former is + // already true (at least in supported Node versions up to v10), so we + // simply follow the latter. Ref: + // https://nodejs.org/dist/latest/docs/api/http.html#http_http_get_options_callback + // https://github.com/googleapis/cloud-trace-nodejs/blob/master/src/instrumentations/instrumentation-http.ts#L198 + return function outgoingGetRequest< + T extends http.RequestOptions | string | url.URL + >(options: T, ...args: HttpRequestArgs): http.ClientRequest { + const req = clientRequest(options, ...args); + req.end(); + return req; + }; + }; + } + + /** Patches HTTPS outgoing requests */ + private _getPatchHttpsOutgoingRequestFunction(component: 'http' | 'https') { + return (original: Func): Func => { + const instrumentation = this; + return function httpsOutgoingRequest( + options: https.RequestOptions | string | URL, + ...args: HttpRequestArgs + ): http.ClientRequest { + // Makes sure options will have default HTTPS parameters + if ( + component === 'https' && + typeof options === 'object' && + options?.constructor.name !== 'URL' + ) { + options = Object.assign({}, options); + instrumentation._setDefaultOptions(options); + } + return instrumentation._getPatchOutgoingRequestFunction(component)( + original + )(options, ...args); + }; + }; + } + + private _setDefaultOptions(options: https.RequestOptions) { + options.protocol = options.protocol || 'https:'; + options.port = options.port || 443; + } + + /** Patches HTTPS outgoing get requests */ + private _getPatchHttpsOutgoingGetFunction( + clientRequest: ( + options: http.RequestOptions | string | URL, + ...args: HttpRequestArgs + ) => http.ClientRequest + ) { + return (original: Func): Func => { + const instrumentation = this; + return function httpsOutgoingRequest( + options: https.RequestOptions | string | URL, + ...args: HttpRequestArgs + ): http.ClientRequest { + return instrumentation._getPatchOutgoingGetFunction(clientRequest)( + original + )(options, ...args); + }; + }; + } + + /** + * Attach event listeners to a client request to end span and add span attributes. + * + * @param request The original request object. + * @param options The arguments to the original function. + * @param span representing the current operation + */ + private _traceClientRequest( + component: 'http' | 'https', + request: http.ClientRequest, + options: ParsedRequestOptions, + span: Span + ): http.ClientRequest { + const hostname = + options.hostname || + options.host?.replace(/^(.*)(:[0-9]{1,5})/, '$1') || + 'localhost'; + const attributes = utils.getOutgoingRequestAttributes(options, { + component, + hostname, + }); + span.setAttributes(attributes); + if (this._getConfig().requestHook) { + this._callRequestHook(span, request); + } + + request.on( + 'response', + (response: http.IncomingMessage & { aborted?: boolean }) => { + const attributes = utils.getOutgoingRequestAttributesOnResponse( + response, + { hostname } + ); + span.setAttributes(attributes); + if (this._getConfig().responseHook) { + this._callResponseHook(span, response); + } + + this.tracer.bind(response); + this._logger.debug('outgoingRequest on response()'); + response.on('end', () => { + this._logger.debug('outgoingRequest on end()'); + let status: Status; + + if (response.aborted && !response.complete) { + status = { code: StatusCode.ERROR }; + } else { + status = utils.parseResponseStatus(response.statusCode!); + } + + span.setStatus(status); + + if (this._getConfig().applyCustomAttributesOnSpan) { + safeExecuteInTheMiddle( + () => + this._getConfig().applyCustomAttributesOnSpan!( + span, + request, + response + ), + () => {}, + true + ); + } + + this._closeHttpSpan(span); + }); + response.on('error', (error: Err) => { + utils.setSpanWithError(span, error, response); + this._closeHttpSpan(span); + }); + } + ); + request.on('close', () => { + if (!request.aborted) { + this._closeHttpSpan(span); + } + }); + request.on('error', (error: Err) => { + utils.setSpanWithError(span, error, request); + this._closeHttpSpan(span); + }); + + this._logger.debug('http.ClientRequest return request'); + return request; + } + + private _incomingRequestFunction( + component: 'http' | 'https', + original: (event: string, ...args: unknown[]) => boolean + ) { + const instrumentation = this; + return function incomingRequest( + this: {}, + event: string, + ...args: unknown[] + ): boolean { + // Only traces request events + if (event !== 'request') { + return original.apply(this, [event, ...args]); + } + + const request = args[0] as http.IncomingMessage; + const response = args[1] as http.ServerResponse & { socket: Socket }; + const pathname = request.url + ? url.parse(request.url).pathname || '/' + : '/'; + const method = request.method || 'GET'; + + instrumentation._logger.debug( + '%s instrumentation incomingRequest', + component + ); + + if ( + utils.isIgnored( + pathname, + instrumentation._getConfig().ignoreIncomingPaths, + (e: Error) => + instrumentation._logger.error( + 'caught ignoreIncomingPaths error: ', + e + ) + ) + ) { + return original.apply(this, [event, ...args]); + } + + const headers = request.headers; + + const spanOptions: SpanOptions = { + kind: SpanKind.SERVER, + attributes: utils.getIncomingRequestAttributes(request, { + component: component, + serverName: instrumentation._getConfig().serverName, + }), + }; + + return context.with(propagation.extract(headers), () => { + const span = instrumentation._startHttpSpan( + `${component.toLocaleUpperCase()} ${method}`, + spanOptions + ); + + return instrumentation.tracer.withSpan(span, () => { + context.bind(request); + context.bind(response); + + if (instrumentation._getConfig().requestHook) { + instrumentation._callRequestHook(span, request); + } + if (instrumentation._getConfig().responseHook) { + instrumentation._callResponseHook(span, response); + } + + // Wraps end (inspired by: + // https://github.com/GoogleCloudPlatform/cloud-trace-nodejs/blob/master/src/instrumentations/instrumentation-connect.ts#L75) + const originalEnd = response.end; + response.end = function ( + this: http.ServerResponse, + ..._args: ResponseEndArgs + ) { + response.end = originalEnd; + // Cannot pass args of type ResponseEndArgs, + const returned = safeExecuteInTheMiddle( + () => response.end.apply(this, arguments as any), + error => { + if (error) { + utils.setSpanWithError(span, error); + instrumentation._closeHttpSpan(span); + throw error; + } + } + ); + + const attributes = utils.getIncomingRequestAttributesOnResponse( + request, + response + ); + + span + .setAttributes(attributes) + .setStatus(utils.parseResponseStatus(response.statusCode)); + + if (instrumentation._getConfig().applyCustomAttributesOnSpan) { + safeExecuteInTheMiddle( + () => + instrumentation._getConfig().applyCustomAttributesOnSpan!( + span, + request, + response + ), + () => {}, + true + ); + } + + instrumentation._closeHttpSpan(span); + return returned; + }; + + return safeExecuteInTheMiddle( + () => original.apply(this, [event, ...args]), + error => { + if (error) { + utils.setSpanWithError(span, error); + instrumentation._closeHttpSpan(span); + throw error; + } + } + ); + }); + }); + }; + } + + private _outgoingRequestFunction( + component: 'http' | 'https', + original: Func + ): Func { + const instrumentation = this; + return function outgoingRequest( + this: {}, + options: url.URL | http.RequestOptions | string, + ...args: unknown[] + ): http.ClientRequest { + if (!utils.isValidOptionsType(options)) { + return original.apply(this, [options, ...args]); + } + const extraOptions = + typeof args[0] === 'object' && + (typeof options === 'string' || options instanceof url.URL) + ? (args.shift() as http.RequestOptions) + : undefined; + const { origin, pathname, method, optionsParsed } = utils.getRequestInfo( + options, + extraOptions + ); + /** + * Node 8's https module directly call the http one so to avoid creating + * 2 span for the same request we need to check that the protocol is correct + * See: https://github.com/nodejs/node/blob/v8.17.0/lib/https.js#L245 + */ + if ( + component === 'http' && + semver.lt(process.version, '9.0.0') && + optionsParsed.protocol === 'https:' + ) { + return original.apply(this, [optionsParsed, ...args]); + } + + if ( + utils.isIgnored( + origin + pathname, + instrumentation._getConfig().ignoreOutgoingUrls, + (e: Error) => + instrumentation._logger.error( + 'caught ignoreOutgoingUrls error: ', + e + ) + ) + ) { + return original.apply(this, [optionsParsed, ...args]); + } + + const operationName = `${component.toUpperCase()} ${method}`; + const spanOptions: SpanOptions = { + kind: SpanKind.CLIENT, + }; + const span = instrumentation._startHttpSpan(operationName, spanOptions); + if (!optionsParsed.headers) { + optionsParsed.headers = {}; + } + propagation.inject( + optionsParsed.headers, + undefined, + setActiveSpan(context.active(), span) + ); + + const request: http.ClientRequest = safeExecuteInTheMiddle( + () => original.apply(this, [optionsParsed, ...args]), + error => { + if (error) { + utils.setSpanWithError(span, error); + instrumentation._closeHttpSpan(span); + throw error; + } + } + ); + + instrumentation._logger.debug( + '%s instrumentation outgoingRequest', + component + ); + instrumentation.tracer.bind(request); + return instrumentation._traceClientRequest( + component, + request, + optionsParsed, + span + ); + }; + } + + private _startHttpSpan(name: string, options: SpanOptions) { + /* + * If a parent is required but not present, we use a `NoRecordingSpan` to still + * propagate context without recording it. + */ + const requireParent = + options.kind === SpanKind.CLIENT + ? this._getConfig().requireParentforOutgoingSpans + : this._getConfig().requireParentforIncomingSpans; + + let span: Span; + const currentSpan = this.tracer.getCurrentSpan(); + + if (requireParent === true && currentSpan === undefined) { + // TODO: Refactor this when a solution is found in + // https://github.com/open-telemetry/opentelemetry-specification/issues/530 + span = new NoRecordingSpan(this._emptySpanContext); + } else if (requireParent === true && currentSpan?.context().isRemote) { + span = currentSpan; + } else { + span = this.tracer.startSpan(name, options); + } + this._spanNotEnded.add(span); + return span; + } + + private _closeHttpSpan(span: Span) { + if (!this._spanNotEnded.has(span)) { + return; + } + + span.end(); + this._spanNotEnded.delete(span); + } + + private _callResponseHook( + span: Span, + response: http.IncomingMessage | http.ServerResponse + ) { + safeExecuteInTheMiddle( + () => this._getConfig().responseHook!(span, response), + () => {}, + true + ); + } + + private _callRequestHook( + span: Span, + request: http.ClientRequest | http.IncomingMessage + ) { + safeExecuteInTheMiddle( + () => this._getConfig().requestHook!(span, request), + () => {}, + true + ); + } +} diff --git a/packages/opentelemetry-instrumentation-http/src/index.ts b/packages/opentelemetry-instrumentation-http/src/index.ts new file mode 100644 index 0000000000..265bc235a7 --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/src/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './http'; +export * from './types'; +export * from './utils'; diff --git a/packages/opentelemetry-instrumentation-http/src/types.ts b/packages/opentelemetry-instrumentation-http/src/types.ts new file mode 100644 index 0000000000..e78dc25f4a --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/src/types.ts @@ -0,0 +1,98 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Span } from '@opentelemetry/api'; +import type * as http from 'http'; +import type * as https from 'https'; +import { + ClientRequest, + get, + IncomingMessage, + request, + ServerResponse, +} from 'http'; +import * as url from 'url'; +import { InstrumentationConfig } from '@opentelemetry/instrumentation'; + +export type IgnoreMatcher = string | RegExp | ((url: string) => boolean); +export type HttpCallback = (res: IncomingMessage) => void; +export type RequestFunction = typeof request; +export type GetFunction = typeof get; + +export type HttpCallbackOptional = HttpCallback | undefined; + +// from node 10+ +export type RequestSignature = [http.RequestOptions, HttpCallbackOptional] & + HttpCallback; + +export type HttpRequestArgs = Array; + +export type ParsedRequestOptions = + | (http.RequestOptions & Partial) + | http.RequestOptions; +export type Http = typeof http; +export type Https = typeof https; +/* tslint:disable-next-line:no-any */ +export type Func = (...args: any[]) => T; +export type ResponseEndArgs = + | [((() => void) | undefined)?] + | [unknown, ((() => void) | undefined)?] + | [unknown, string, ((() => void) | undefined)?]; + +export interface HttpCustomAttributeFunction { + ( + span: Span, + request: ClientRequest | IncomingMessage, + response: IncomingMessage | ServerResponse + ): void; +} + +export interface HttpRequestCustomAttributeFunction { + (span: Span, request: ClientRequest | IncomingMessage): void; +} + +export interface HttpResponseCustomAttributeFunction { + (span: Span, response: IncomingMessage | ServerResponse): void; +} + +/** + * Options available for the HTTP instrumentation (see [documentation](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-instrumentation-http#http-instrumentation-options)) + */ +export interface HttpInstrumentationConfig extends InstrumentationConfig { + /** Not trace all incoming requests that match paths */ + ignoreIncomingPaths?: IgnoreMatcher[]; + /** Not trace all outgoing requests that match urls */ + ignoreOutgoingUrls?: IgnoreMatcher[]; + /** Function for adding custom attributes after response is handled */ + applyCustomAttributesOnSpan?: HttpCustomAttributeFunction; + /** Function for adding custom attributes before request is handled */ + requestHook?: HttpRequestCustomAttributeFunction; + /** Function for adding custom attributes before response is handled */ + responseHook?: HttpResponseCustomAttributeFunction; + /** The primary server name of the matched virtual host. */ + serverName?: string; + /** Require parent to create span for outgoing requests */ + requireParentforOutgoingSpans?: boolean; + /** Require parent to create span for incoming requests */ + requireParentforIncomingSpans?: boolean; +} + +export interface Err extends Error { + errno?: number; + code?: string; + path?: string; + syscall?: string; + stack?: string; +} diff --git a/packages/opentelemetry-instrumentation-http/src/utils.ts b/packages/opentelemetry-instrumentation-http/src/utils.ts new file mode 100644 index 0000000000..32673cb6bf --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/src/utils.ts @@ -0,0 +1,429 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Attributes, StatusCode, Span, Status } from '@opentelemetry/api'; +import { + HttpAttribute, + GeneralAttribute, +} from '@opentelemetry/semantic-conventions'; +import { + ClientRequest, + IncomingHttpHeaders, + IncomingMessage, + OutgoingHttpHeaders, + RequestOptions, + ServerResponse, +} from 'http'; +import { Socket } from 'net'; +import * as url from 'url'; +import { Err, IgnoreMatcher, ParsedRequestOptions } from './types'; + +/** + * Get an absolute url + */ +export const getAbsoluteUrl = ( + requestUrl: ParsedRequestOptions | null, + headers: IncomingHttpHeaders | OutgoingHttpHeaders, + fallbackProtocol = 'http:' +): string => { + const reqUrlObject = requestUrl || {}; + const protocol = reqUrlObject.protocol || fallbackProtocol; + const port = (reqUrlObject.port || '').toString(); + const path = reqUrlObject.path || '/'; + let host = + reqUrlObject.host || reqUrlObject.hostname || headers.host || 'localhost'; + + // if there is no port in host and there is a port + // it should be displayed if it's not 80 and 443 (default ports) + if ( + (host as string).indexOf(':') === -1 && + port && + port !== '80' && + port !== '443' + ) { + host += `:${port}`; + } + + return `${protocol}//${host}${path}`; +}; +/** + * Parse status code from HTTP response. [More details](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-http.md#status) + */ +export const parseResponseStatus = ( + statusCode: number +): Omit => { + // 1xx, 2xx, 3xx are OK + if (statusCode >= 100 && statusCode < 400) { + return { code: StatusCode.OK }; + } + + // All other codes are error + return { code: StatusCode.ERROR }; +}; + +/** + * Returns whether the Expect header is on the given options object. + * @param options Options for http.request. + */ +export const hasExpectHeader = (options: RequestOptions): boolean => { + if (!options.headers) { + return false; + } + + const keys = Object.keys(options.headers); + return !!keys.find(key => key.toLowerCase() === 'expect'); +}; + +/** + * Check whether the given obj match pattern + * @param constant e.g URL of request + * @param obj obj to inspect + * @param pattern Match pattern + */ +export const satisfiesPattern = ( + constant: string, + pattern: IgnoreMatcher +): boolean => { + if (typeof pattern === 'string') { + return pattern === constant; + } else if (pattern instanceof RegExp) { + return pattern.test(constant); + } else if (typeof pattern === 'function') { + return pattern(constant); + } else { + throw new TypeError('Pattern is in unsupported datatype'); + } +}; + +/** + * Check whether the given request is ignored by configuration + * It will not re-throw exceptions from `list` provided by the client + * @param constant e.g URL of request + * @param [list] List of ignore patterns + * @param [onException] callback for doing something when an exception has + * occurred + */ +export const isIgnored = ( + constant: string, + list?: IgnoreMatcher[], + onException?: (error: Error) => void +): boolean => { + if (!list) { + // No ignored urls - trace everything + return false; + } + // Try/catch outside the loop for failing fast + try { + for (const pattern of list) { + if (satisfiesPattern(constant, pattern)) { + return true; + } + } + } catch (e) { + if (onException) { + onException(e); + } + } + + return false; +}; + +/** + * Sets the span with the error passed in params + * @param {Span} span the span that need to be set + * @param {Error} error error that will be set to span + * @param {(IncomingMessage | ClientRequest)} [obj] used for enriching the status by checking the statusCode. + */ +export const setSpanWithError = ( + span: Span, + error: Err, + obj?: IncomingMessage | ClientRequest +) => { + const message = error.message; + + span.setAttributes({ + [HttpAttribute.HTTP_ERROR_NAME]: error.name, + [HttpAttribute.HTTP_ERROR_MESSAGE]: message, + }); + + if (!obj) { + span.setStatus({ code: StatusCode.ERROR, message }); + return; + } + + let status: Status; + if ((obj as IncomingMessage).statusCode) { + status = parseResponseStatus((obj as IncomingMessage).statusCode!); + } else if ((obj as ClientRequest).aborted) { + status = { code: StatusCode.ERROR }; + } else { + status = { code: StatusCode.ERROR }; + } + + status.message = message; + + span.setStatus(status); +}; + +/** + * Makes sure options is an url object + * return an object with default value and parsed options + * @param options original options for the request + * @param [extraOptions] additional options for the request + */ +export const getRequestInfo = ( + options: url.URL | RequestOptions | string, + extraOptions?: RequestOptions +) => { + let pathname = '/'; + let origin = ''; + let optionsParsed: RequestOptions; + if (typeof options === 'string') { + optionsParsed = url.parse(options); + pathname = (optionsParsed as url.UrlWithStringQuery).pathname || '/'; + origin = `${optionsParsed.protocol || 'http:'}//${optionsParsed.host}`; + if (extraOptions !== undefined) { + Object.assign(optionsParsed, extraOptions); + } + } else if (options instanceof url.URL) { + optionsParsed = { + protocol: options.protocol, + hostname: + typeof options.hostname === 'string' && options.hostname.startsWith('[') + ? options.hostname.slice(1, -1) + : options.hostname, + path: `${options.pathname || ''}${options.search || ''}`, + }; + if (options.port !== '') { + optionsParsed.port = Number(options.port); + } + if (options.username || options.password) { + optionsParsed.auth = `${options.username}:${options.password}`; + } + pathname = options.pathname; + origin = options.origin; + if (extraOptions !== undefined) { + Object.assign(optionsParsed, extraOptions); + } + } else { + optionsParsed = Object.assign( + { protocol: options.host ? 'http:' : undefined }, + options + ); + pathname = (options as url.URL).pathname; + if (!pathname && optionsParsed.path) { + pathname = url.parse(optionsParsed.path).pathname || '/'; + } + origin = `${optionsParsed.protocol || 'http:'}//${ + optionsParsed.host || `${optionsParsed.hostname}:${optionsParsed.port}` + }`; + } + + if (hasExpectHeader(optionsParsed)) { + optionsParsed.headers = Object.assign({}, optionsParsed.headers); + } else if (!optionsParsed.headers) { + optionsParsed.headers = {}; + } + // some packages return method in lowercase.. + // ensure upperCase for consistency + const method = optionsParsed.method + ? optionsParsed.method.toUpperCase() + : 'GET'; + + return { origin, pathname, method, optionsParsed }; +}; + +/** + * Makes sure options is of type string or object + * @param options for the request + */ +export const isValidOptionsType = (options: unknown): boolean => { + if (!options) { + return false; + } + + const type = typeof options; + return type === 'string' || (type === 'object' && !Array.isArray(options)); +}; + +/** + * Returns outgoing request attributes scoped to the options passed to the request + * @param {ParsedRequestOptions} requestOptions the same options used to make the request + * @param {{ component: string, hostname: string }} options used to pass data needed to create attributes + */ +export const getOutgoingRequestAttributes = ( + requestOptions: ParsedRequestOptions, + options: { component: string; hostname: string } +): Attributes => { + const host = requestOptions.host; + const hostname = + requestOptions.hostname || + host?.replace(/^(.*)(:[0-9]{1,5})/, '$1') || + 'localhost'; + const requestMethod = requestOptions.method; + const method = requestMethod ? requestMethod.toUpperCase() : 'GET'; + const headers = requestOptions.headers || {}; + const userAgent = headers['user-agent']; + const attributes: Attributes = { + [HttpAttribute.HTTP_URL]: getAbsoluteUrl( + requestOptions, + headers, + `${options.component}:` + ), + [HttpAttribute.HTTP_METHOD]: method, + [HttpAttribute.HTTP_TARGET]: requestOptions.path || '/', + [GeneralAttribute.NET_PEER_NAME]: hostname, + }; + + if (userAgent !== undefined) { + attributes[HttpAttribute.HTTP_USER_AGENT] = userAgent; + } + return attributes; +}; + +/** + * Returns attributes related to the kind of HTTP protocol used + * @param {string} [kind] Kind of HTTP protocol used: "1.0", "1.1", "2", "SPDY" or "QUIC". + */ +export const getAttributesFromHttpKind = (kind?: string): Attributes => { + const attributes: Attributes = {}; + if (kind) { + attributes[HttpAttribute.HTTP_FLAVOR] = kind; + if (kind.toUpperCase() !== 'QUIC') { + attributes[GeneralAttribute.NET_TRANSPORT] = GeneralAttribute.IP_TCP; + } else { + attributes[GeneralAttribute.NET_TRANSPORT] = GeneralAttribute.IP_UDP; + } + } + return attributes; +}; + +/** + * Returns outgoing request attributes scoped to the response data + * @param {IncomingMessage} response the response object + * @param {{ hostname: string }} options used to pass data needed to create attributes + */ +export const getOutgoingRequestAttributesOnResponse = ( + response: IncomingMessage, + options: { hostname: string } +): Attributes => { + const { statusCode, statusMessage, httpVersion, socket } = response; + const { remoteAddress, remotePort } = socket; + const attributes: Attributes = { + [GeneralAttribute.NET_PEER_IP]: remoteAddress, + [GeneralAttribute.NET_PEER_PORT]: remotePort, + [HttpAttribute.HTTP_HOST]: `${options.hostname}:${remotePort}`, + }; + + if (statusCode) { + attributes[HttpAttribute.HTTP_STATUS_CODE] = statusCode; + attributes[HttpAttribute.HTTP_STATUS_TEXT] = ( + statusMessage || '' + ).toUpperCase(); + } + + const httpKindAttributes = getAttributesFromHttpKind(httpVersion); + return Object.assign(attributes, httpKindAttributes); +}; + +/** + * Returns incoming request attributes scoped to the request data + * @param {IncomingMessage} request the request object + * @param {{ component: string, serverName?: string }} options used to pass data needed to create attributes + */ +export const getIncomingRequestAttributes = ( + request: IncomingMessage, + options: { component: string; serverName?: string } +): Attributes => { + const headers = request.headers; + const userAgent = headers['user-agent']; + const ips = headers['x-forwarded-for']; + const method = request.method || 'GET'; + const httpVersion = request.httpVersion; + const requestUrl = request.url ? url.parse(request.url) : null; + const host = requestUrl?.host || headers.host; + const hostname = + requestUrl?.hostname || + host?.replace(/^(.*)(:[0-9]{1,5})/, '$1') || + 'localhost'; + const serverName = options.serverName; + const attributes: Attributes = { + [HttpAttribute.HTTP_URL]: getAbsoluteUrl( + requestUrl, + headers, + `${options.component}:` + ), + [HttpAttribute.HTTP_HOST]: host, + [GeneralAttribute.NET_HOST_NAME]: hostname, + [HttpAttribute.HTTP_METHOD]: method, + }; + + if (typeof ips === 'string') { + attributes[HttpAttribute.HTTP_CLIENT_IP] = ips.split(',')[0]; + } + + if (typeof serverName === 'string') { + attributes[HttpAttribute.HTTP_SERVER_NAME] = serverName; + } + + if (requestUrl) { + attributes[HttpAttribute.HTTP_ROUTE] = requestUrl.pathname || '/'; + attributes[HttpAttribute.HTTP_TARGET] = requestUrl.pathname || '/'; + } + + if (userAgent !== undefined) { + attributes[HttpAttribute.HTTP_USER_AGENT] = userAgent; + } + + const httpKindAttributes = getAttributesFromHttpKind(httpVersion); + return Object.assign(attributes, httpKindAttributes); +}; + +/** + * Returns incoming request attributes scoped to the response data + * @param {(ServerResponse & { socket: Socket; })} response the response object + */ +export const getIncomingRequestAttributesOnResponse = ( + request: IncomingMessage & { __ot_middlewares?: string[] }, + response: ServerResponse & { socket: Socket } +): Attributes => { + const { statusCode, statusMessage, socket } = response; + const { localAddress, localPort, remoteAddress, remotePort } = socket; + const { __ot_middlewares } = (request as unknown) as { + [key: string]: unknown; + }; + const route = Array.isArray(__ot_middlewares) + ? __ot_middlewares + .filter(path => path !== '/') + .map(path => { + return path[0] === '/' ? path : '/' + path; + }) + .join('') + : undefined; + + const attributes: Attributes = { + [GeneralAttribute.NET_HOST_IP]: localAddress, + [GeneralAttribute.NET_HOST_PORT]: localPort, + [GeneralAttribute.NET_PEER_IP]: remoteAddress, + [GeneralAttribute.NET_PEER_PORT]: remotePort, + [HttpAttribute.HTTP_STATUS_CODE]: statusCode, + [HttpAttribute.HTTP_STATUS_TEXT]: (statusMessage || '').toUpperCase(), + }; + + if (route !== undefined) { + attributes[HttpAttribute.HTTP_ROUTE] = route; + } + return attributes; +}; diff --git a/packages/opentelemetry-instrumentation-http/src/version.ts b/packages/opentelemetry-instrumentation-http/src/version.ts new file mode 100644 index 0000000000..707690f363 --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/src/version.ts @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// this is autogenerated file, see scripts/version-update.js +export const VERSION = '0.12.0'; diff --git a/packages/opentelemetry-instrumentation-http/test/fixtures/google-http.json b/packages/opentelemetry-instrumentation-http/test/fixtures/google-http.json new file mode 100644 index 0000000000..62301ab56b --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/test/fixtures/google-http.json @@ -0,0 +1,43 @@ +[ + { + "scope": "http://www.google.com", + "method": "GET", + "path": "/search?q=axios&oq=axios&aqs=chrome.0.69i59l2j0l3j69i60.811j0j7&sourceid=chrome&ie=UTF-8", + "body": "", + "status": 200, + "response": "3c21646f63747970652068746d6c3e3c68746d6c206c616e673d22656e2d4341223e3c686561643e3c6d65746120636861727365743d225554462d38223e3c6d65746120636f6e74656e743d222f696d616765732f6272616e64696e672f676f6f676c65672f31782f676f6f676c65675f7374616e646172645f636f6c6f725f31323864702e706e6722206974656d70726f703d22696d616765223e3c7469746c653e6178696f73202d20476f6f676c65205365617263683c2f7469746c653e3c736372697074206e6f6e63653d224f77774e4a497141533843544d31352f4351595531513d3d223e2866756e6374696f6e28297b76617220613d77696e646f772e706572666f726d616e63653b77696e646f772e73746172743d286e65772044617465292e67657454696d6528293b613a7b76617220623d77696e646f773b69662861297b76617220633d612e74696d696e673b69662863297b76617220643d632e6e617669676174696f6e53746172742c653d632e726573706f6e736553746172743b696628653e642626653c3d77696e646f772e7374617274297b77696e646f772e73746172743d653b622e777372743d652d643b627265616b20617d7d612e6e6f77262628622e777372743d4d6174682e666c6f6f7228612e6e6f77282929297d7d77696e646f772e676f6f676c653d77696e646f772e676f6f676c657c7c7b7d3b676f6f676c652e6166743d66756e6374696f6e2866297b662e7365744174747269627574652822646174612d696d6c222c2b6e65772044617465297d3b7d292e63616c6c2874686973293b2866756e6374696f6e28297b76617220633d5b5d2c653d303b77696e646f772e70696e673d66756e6374696f6e2862297b2d313d3d622e696e6465784f662822267a782229262628622b3d22267a783d222b286e65772044617465292e67657454696d652829293b76617220613d6e657720496d6167652c643d652b2b3b635b645d3d613b612e6f6e6572726f723d612e6f6e6c6f61643d612e6f6e61626f72743d66756e6374696f6e28297b64656c65746520635b645d7d3b612e7372633d627d3b7d292e63616c6c2874686973293b3c2f7363726970743e3c7374796c653e626f64797b6d617267696e3a30206175746f3b6d61782d77696474683a37333670783b70616464696e673a30203870787d617b636f6c6f723a233139363744323b746578742d6465636f726174696f6e3a6e6f6e653b7461702d686967686c696768742d636f6c6f723a7267626128302c302c302c2e31297d613a766973697465647b636f6c6f723a233442313141387d613a686f7665727b746578742d6465636f726174696f6e3a756e6465726c696e657d696d677b626f726465723a307d68746d6c7b666f6e742d66616d696c793a526f626f746f2c48656c7665746963614e6575652c417269616c2c73616e732d73657269663b666f6e742d73697a653a313470783b6c696e652d6865696768743a323070783b746578742d73697a652d61646a7573743a313030253b636f6c6f723a233343343034333b776f72642d777261703a627265616b2d776f72643b6261636b67726f756e642d636f6c6f723a236666667d2e625273576e637b6261636b67726f756e642d636f6c6f723a236666663b626f726465722d746f703a31707820736f6c696420236530653065303b6865696768743a333970783b6f766572666c6f773a68696464656e7d2e4e365257567b6865696768743a353170783b6f766572666c6f772d7363726f6c6c696e673a746f7563683b6f766572666c6f772d783a6175746f3b6f766572666c6f772d793a68696464656e7d2e5576363771627b626f782d7061636b3a6a7573746966793b666f6e742d73697a653a313270783b6c696e652d6865696768743a333770783b6a7573746966792d636f6e74656e743a73706163652d6265747765656e3b6a7573746966792d636f6e74656e743a73706163652d6265747765656e7d2e55763637716220612c2e557636377162207370616e7b636f6c6f723a233735373537353b646973706c61793a626c6f636b3b666c65783a6e6f6e653b70616464696e673a3020313670783b746578742d616c69676e3a63656e7465723b746578742d7472616e73666f726d3a7570706572636173653b7d7370616e2e4f585875707b626f726465722d626f74746f6d3a32707820736f6c696420233432383566343b636f6c6f723a233432383566343b666f6e742d7765696768743a626f6c647d612e655a743878643a766973697465647b636f6c6f723a233735373537357d2e46456c6273667b626f726465722d6c6566743a31707820736f6c6964207267626128302c302c302c2e3132297d6865616465722061727469636c657b6f766572666c6f773a76697369626c657d2e5067373062667b6865696768743a333970783b646973706c61793a626f783b646973706c61793a666c65783b646973706c61793a666c65783b77696474683a313030257d2e4830505165637b706f736974696f6e3a72656c61746976653b666c65783a317d2e7362637b646973706c61793a666c65783b77696474683a313030257d2e50673730626620696e7075747b6d617267696e3a3270782034707820327078203870783b7d2e787b77696474683a323670783b636f6c6f723a233735373537353b666f6e743a323770782f3338707820617269616c2c2073616e732d73657269663b6c696e652d6865696768743a343070783b7d237164436c77627b666c65783a302030206175746f3b77696474683a333970783b6865696768743a333970783b626f726465722d626f74746f6d3a303b70616464696e673a303b626f726465722d746f702d72696768742d7261646975733a3870783b6261636b67726f756e642d636f6c6f723a233362373865373b626f726465723a31707820736f6c696420233333363764363b6261636b67726f756e642d696d6167653a75726c28646174613a696d6167652f6769663b6261736536342c52306c474f4464684a41416a41504948414f44722f6e436b2b4d505a2f466d5639367a4b2b2f372b2f354b352b6b714c39697741414141414a41416a41454144616e693633503477796b6d624b635152584473635141454d586d6d65614c51564c43756b7a79433039416a66654b37762f4d41616a41434c68504d564167776a73556345695a61387867415972567176324b783269777349414141426b6e664242414b7254453449634d796f743875723864617471496251664a646e41666f3257453642563035775849694a69676b414f773d3d293b7d2e73637b666f6e742d73697a653a3b706f736974696f6e3a6162736f6c7574653b746f703a333970783b6c6566743a303b72696768743a303b626f782d736861646f773a3070782032707820357078207267626128302c302c302c302e32293b7a2d696e6465783a323b6261636b67726f756e642d636f6c6f723a236666667d2e73633e6469767b70616464696e673a3130707820313070783b70616464696e672d6c6566743a313670783b70616464696e672d6c6566743a313470783b626f726465722d746f703a31707820736f6c696420234446453145357d2e7363737b6261636b67726f756e642d636f6c6f723a236635663566353b7d2e6e6f484978637b646973706c61793a626c6f636b3b666f6e742d73697a653a313670783b70616464696e673a3020302030203870783b666c65783a313b6865696768743a333570783b6f75746c696e653a6e6f6e653b626f726465723a6e6f6e653b77696474683a313030253b2d7765626b69742d7461702d686967686c696768742d636f6c6f723a7267626128302c302c302c30293b6f766572666c6f773a68696464656e3b7d2e73626320696e7075745b747970653d746578745d7b6261636b67726f756e643a6e6f6e657d2e736d6c202e634f6c3449647b646973706c61793a6e6f6e657d2e6c7b646973706c61793a6e6f6e657d2e736d6c206865616465727b6261636b67726f756e643a6e6f6e657d2e736d6c202e6c7b646973706c61793a626c6f636b3b70616464696e673a30203870787d2e736d6c202e6c7b6c65747465722d73706163696e673a2d3170783b746578742d616c69676e3a63656e7465723b626f726465722d7261646975733a3270782030203020303b666f6e743a323270782f33367078204675747572612c20417269616c2c2073616e732d73657269663b666f6e742d736d6f6f7468696e673a616e7469616c69617365647d2e627a316c42627b6261636b67726f756e643a236666663b626f726465722d7261646975733a38707820387078203020303b626f782d736861646f773a30203170782036707820726762612833322c2033332c2033362c20302e3138293b6d617267696e2d746f703a313070787d2e4b50374c43627b626f726465722d7261646975733a30203020387078203870783b626f782d736861646f773a30203270782033707820726762612833322c2033332c2033362c20302e3138293b6d617267696e2d626f74746f6d3a313070783b6f766572666c6f773a68696464656e7d2e634f6c3449647b6c65747465722d73706163696e673a2d3170783b746578742d616c69676e3a63656e7465723b666f6e743a32327074204675747572612c20417269616c2c2073616e732d73657269663b70616464696e673a3130707820302035707820303b6865696768743a333770783b666f6e742d736d6f6f7468696e673a616e7469616c69617365647d2e634f6c344964207370616e7b646973706c61793a696e6c696e652d626c6f636b7d2e533539316a7b6865696768743a313030257d2e5636677756647b636f6c6f723a233432383546347d2e69576b7576647b636f6c6f723a234541343333357d2e63447251377b636f6c6f723a236662636330357d2e6e746c52397b636f6c6f723a233334413835337d2e744a334d79637b2d7765626b69742d7472616e73666f726d3a726f74617465282d3230646567293b706f736974696f6e3a72656c61746976653b6c6566743a2d3170783b646973706c61793a696e6c696e652d626c6f636b7d666f6f7465727b746578742d616c69676e3a63656e7465723b6d617267696e2d746f703a313870787d666f6f74657220612c666f6f74657220613a766973697465642c2e736d695562627b636f6c6f723a233566363336387d2e6b73545534637b6d617267696e3a3020313370787d236d436c6a6f627b6d617267696e2d746f703a333670787d236d436c6a6f623e6469767b6d617267696e3a323070787d3c2f7374796c653e3c2f686561643e3c626f6479206a736d6f64656c3d2220223e3c6865616465722069643d22686472223e3c736372697074206e6f6e63653d224f77774e4a497141533843544d31352f4351595531513d3d223e2866756e6374696f6e28297b76617220633d3530303b2866756e6374696f6e28297b77696e646f772e73637265656e262677696e646f772e73637265656e2e77696474683c3d63262677696e646f772e73637265656e2e6865696768743c3d632626646f63756d656e742e676574456c656d656e7442794964282268647222292e636c6173734c6973742e6164642822736d6c22293b7d292e63616c6c2874686973293b7d2928293b3c2f7363726970743e3c64697620636c6173733d22634f6c344964223e3c6120687265663d222f3f73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673514f776743223e3c7370616e20636c6173733d22563667775664223e473c2f7370616e3e3c7370616e20636c6173733d2269576b757664223e6f3c2f7370616e3e3c7370616e20636c6173733d226344725137223e6f3c2f7370616e3e3c7370616e20636c6173733d22563667775664223e673c2f7370616e3e3c7370616e20636c6173733d226e746c5239223e6c3c2f7370616e3e3c7370616e20636c6173733d2269576b75766420744a334d7963223e653c2f7370616e3e3c2f613e3c2f6469763e3c64697620636c6173733d22627a316c4262223e3c666f726d20636c6173733d22506737306266222069643d227366223e3c6120636c6173733d226c2220687265663d222f3f6f75747075743d73656172636826616d703b69653d5554462d3826616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735150416745223e3c7370616e20636c6173733d22563667775664223e473c2f7370616e3e3c7370616e20636c6173733d2269576b757664223e6f3c2f7370616e3e3c7370616e20636c6173733d226344725137223e6f3c2f7370616e3e3c7370616e20636c6173733d22563667775664223e673c2f7370616e3e3c7370616e20636c6173733d226e746c5239223e6c3c2f7370616e3e3c7370616e20636c6173733d2269576b75766420744a334d7963223e653c2f7370616e3e3c2f613e3c696e707574206e616d653d226965222076616c75653d2249534f2d383835392d312220747970653d2268696464656e223e3c64697620636c6173733d22483050516563223e3c64697620636c6173733d227362632065736263223e3c696e70757420636c6173733d226e6f48497863222076616c75653d226178696f7322206175746f6361706974616c697a653d226e6f6e6522206175746f636f6d706c6574653d226f666622206e616d653d227122207370656c6c636865636b3d2266616c73652220747970653d2274657874223e3c696e707574206e616d653d226f712220747970653d2268696464656e223e3c696e707574206e616d653d226171732220747970653d2268696464656e223e3c64697620636c6173733d2278223ed73c2f6469763e3c64697620636c6173733d227363223e3c2f6469763e3c2f6469763e3c2f6469763e3c627574746f6e2069643d227164436c77622220747970653d227375626d6974223e3c2f627574746f6e3e3c2f666f726d3e3c2f6469763e3c6e6f7363726970743e3c6d65746120636f6e74656e743d22303b75726c3d2f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b6762763d3126616d703b7365693d704d524f586250624472505339414f426d4b505942512220687474702d65717569763d2272656672657368223e3c7374796c653e7461626c652c6469762c7370616e2c707b646973706c61793a6e6f6e657d3c2f7374796c653e3c646976207374796c653d22646973706c61793a626c6f636b223e506c6561736520636c69636b203c6120687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b6762763d3126616d703b7365693d704d524f586250624472505339414f426d4b50594251223e686572653c2f613e20696620796f7520617265206e6f7420726564697265637465642077697468696e206120666577207365636f6e64732e3c2f6469763e3c2f6e6f7363726970743e3c2f6865616465723e3c6469762069643d226d61696e223e3c6469763e3c64697620636c6173733d224b50374c4362223e203c64697620636c6173733d22625273576e63223e203c64697620636c6173733d224e36525756223e203c64697620636c6173733d2250673730626620557636377162223e203c7370616e20636c6173733d224f58587570223e416c6c3c2f7370616e3e3c6120636c6173733d22655a743878642220687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e6d7326616d703b74626d3d6e777326616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673515f41554943436742223e4e6577733c2f613e3c6120636c6173733d22655a743878642220687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e6d7326616d703b74626d3d76696426616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673515f41554943536743223e566964656f733c2f613e3c6120636c6173733d22655a743878642220687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e6d7326616d703b74626d3d6973636826616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673515f41554943696744223e496d616765733c2f613e2020203c6120687265663d22687474703a2f2f6d6170732e676f6f676c652e636f6d2f6d6170733f713d6178696f7326616d703b756d3d3126616d703b69653d5554462d3826616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673515f41554943796745223e4d6170733c2f613e20203c6120687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e6d7326616d703b74626d3d73686f7026616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673515f41554944436746223e53686f7070696e673c2f613e20203c6120687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e6d7326616d703b74626d3d626b7326616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673515f41554944536747223e426f6f6b733c2f613e20202020203c64697620636c6173733d2246456c627366223e3c6120687265663d222f616476616e6365645f73656172636822207374796c653d2277686974652d73706163653a6e6f77726170222069643d2273742d746f67676c652220726f6c653d22627574746f6e223e53656172636820746f6f6c733c2f613e3c2f6469763e203c2f6469763e203c2f6469763e203c2f6469763e203c2f6469763e3c64697620636c6173733d22506737306266207745736a6264205a494e62626320787064204f396735636320755550476922207374796c653d22646973706c61793a6e6f6e65222069643d2273742d63617264223e3c7374796c653e2e7745736a62647b6261636b67726f756e642d636f6c6f723a236666663b6865696768743a343470783b77686974652d73706163653a6e6f777261707d2e636f505538637b6865696768743a363070783b6f766572666c6f772d7363726f6c6c696e673a746f7563683b6f766572666c6f772d783a6175746f3b6f766572666c6f772d793a68696464656e7d2e586a326175657b6865696768743a343470783b6f766572666c6f773a68696464656e7d2e526e4e477a657b6d617267696e3a3131707820313670787d2e7745736a6264206469762c2e7745736a626420612c2e7745736a6264206c697b6f75746c696e652d77696474683a303b6f75746c696e653a6e6f6e657d3c2f7374796c653e3c64697620636c6173733d22586a32617565223e3c64697620636c6173733d22636f50553863223e3c64697620636c6173733d22526e4e477a65223e3c7374796c653e2e5041394a357b646973706c61793a696e6c696e652d626c6f636b7d2e5258614f66647b646973706c61793a696e6c696e652d626c6f636b3b6865696768743a323270783b706f736974696f6e3a72656c61746976653b70616464696e672d746f703a303b70616464696e672d626f74746f6d3a303b70616464696e672d72696768743a313670783b70616464696e672d6c6566743a303b6c696e652d6865696768743a323270783b637572736f723a706f696e7465723b746578742d7472616e73666f726d3a7570706572636173653b666f6e742d73697a653a313270783b636f6c6f723a233735373537357d2e736131746f637b646973706c61793a6e6f6e653b706f736974696f6e3a6162736f6c7574653b6261636b67726f756e643a236666663b626f726465723a31707820736f6c696420236436643664363b626f782d736861646f773a302032707820347078207267626128302c302c302c302e33293b6d617267696e3a303b77686974652d73706163653a6e6f777261703b7a2d696e6465783a3130333b6c696e652d6865696768743a313770783b70616464696e672d746f703a3570783b70616464696e672d626f74746f6d3a3570783b70616464696e672d6c6566743a3070787d2e5041394a353a686f766572202e736131746f637b646973706c61793a626c6f636b7d2e6d475379386420613a6163746976652c2e5258614f66643a6163746976657b636f6c6f723a233432383566347d3c2f7374796c653e3c64697620636c6173733d225041394a35223e3c64697620636c6173733d225258614f66642220726f6c653d22627574746f6e2220746162696e6465783d2230223e3c7374796c653e2e54574d4f55637b646973706c61793a696e6c696e652d626c6f636b3b70616464696e672d72696768743a313470783b77686974652d73706163653a6e6f777261707d2e7651597547667b666f6e742d7765696768743a626f6c647d2e4f6d54497a667b626f726465722d636f6c6f723a23393039303930207472616e73706172656e743b626f726465722d7374796c653a736f6c69643b626f726465722d77696474683a347078203470782030203470783b77696474683a303b6865696768743a303b6d617267696e2d6c6566743a2d313070783b746f703a3530253b6d617267696e2d746f703a2d3270783b706f736974696f6e3a6162736f6c7574657d2e5258614f66643a616374697665202e4f6d54497a667b626f726465722d636f6c6f723a23343238356634207472616e73706172656e747d3c2f7374796c653e3c64697620636c6173733d2254574d4f5563223e416e792074696d653c2f6469763e3c7370616e20636c6173733d224f6d54497a66223e3c2f7370616e3e3c2f6469763e3c756c20636c6173733d22736131746f63206f7a61744d223e3c7374796c653e2e6f7a61744d7b666f6e742d73697a653a313270783b746578742d7472616e73666f726d3a7570706572636173657d2e6f7a61744d202e794e46736c2c2e6f7a61744d206c697b6c6973742d7374796c652d747970653a6e6f6e653b6c6973742d7374796c652d706f736974696f6e3a6f7574736964653b6c6973742d7374796c652d696d6167653a6e6f6e657d2e794e46736c2e536b556a34632c2e794e46736c20617b636f6c6f723a7267626128302c302c302c302e3534293b746578742d6465636f726174696f6e3a6e6f6e653b70616464696e673a36707820343470782036707820313470783b6c696e652d6865696768743a313770783b646973706c61793a626c6f636b7d2e536b556a34637b6261636b67726f756e642d696d6167653a75726c282f2f73736c2e677374617469632e636f6d2f75692f76312f6d656e752f636865636b6d61726b322e706e67293b6261636b67726f756e642d706f736974696f6e3a72696768742063656e7465723b6261636b67726f756e642d7265706561743a6e6f2d7265706561747d2e536b556a34633a6163746976657b6261636b67726f756e642d636f6c6f723a236635663566357d3c2f7374796c653e3c6c6920636c6173733d22794e46736c20536b556a3463223e416e792074696d653c2f6c693e3c6c6920636c6173733d22794e46736c223e3c6120687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e7426616d703b7462733d7164723a6826616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351707755494477223e5061737420686f75723c2f613e3c2f6c693e3c6c6920636c6173733d22794e46736c223e3c6120687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e7426616d703b7462733d7164723a6426616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351707755494541223e5061737420323420686f7572733c2f613e3c2f6c693e3c6c6920636c6173733d22794e46736c223e3c6120687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e7426616d703b7462733d7164723a7726616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351707755494551223e50617374207765656b3c2f613e3c2f6c693e3c6c6920636c6173733d22794e46736c223e3c6120687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e7426616d703b7462733d7164723a6d26616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351707755494567223e50617374206d6f6e74683c2f613e3c2f6c693e3c6c6920636c6173733d22794e46736c223e3c6120687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e7426616d703b7462733d7164723a7926616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351707755494577223e5061737420796561723c2f613e3c2f6c693e3c2f756c3e3c2f6469763e3c64697620636c6173733d225041394a35223e3c64697620636c6173733d225258614f66642220726f6c653d22627574746f6e2220746162696e6465783d2230223e3c64697620636c6173733d2254574d4f5563223e416c6c20726573756c74733c2f6469763e3c7370616e20636c6173733d224f6d54497a66223e3c2f7370616e3e3c2f6469763e3c756c20636c6173733d22736131746f63206f7a61744d223e3c6c6920636c6173733d22794e46736c20536b556a3463223e416c6c20726573756c74733c2f6c693e3c6c6920636c6173733d22794e46736c223e3c6120687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e7426616d703b7462733d6c693a3126616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351707755494651223e566572626174696d3c2f613e3c2f6c693e3c2f756c3e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c736372697074206e6f6e63653d224f77774e4a497141533843544d31352f4351595531513d3d223e2866756e6374696f6e28297b76617220613d646f63756d656e742e676574456c656d656e7442794964282273742d746f67676c6522292c623d646f63756d656e742e676574456c656d656e7442794964282273742d6361726422293b612626622626612e6164644576656e744c697374656e65722822636c69636b222c66756e6374696f6e2863297b622e7374796c652e646973706c61793d622e7374796c652e646973706c61793f22223a226e6f6e65223b632e70726576656e7444656661756c7428297d2c2131293b7d292e63616c6c2874686973293b3c2f7363726970743e3c2f6469763e3c2f6469763e3c7374796c653e2e5a494e6262637b6261636b67726f756e642d636f6c6f723a236666663b6d617267696e2d626f74746f6d3a313070783b626f782d736861646f773a30203170782036707820726762612833322c2033332c2033362c20302e3238293b626f726465722d7261646975733a3870787d2e75555047697b666f6e742d73697a653a313470783b6c696e652d6865696768743a323070783b7d2e4f39673563633e2a3a66697273742d6368696c647b626f726465722d746f702d6c6566742d7261646975733a3870783b626f726465722d746f702d72696768742d7261646975733a3870787d2e4f39673563633e2a3a6c6173742d6368696c647b626f726465722d626f74746f6d2d6c6566742d7261646975733a3870783b626f726465722d626f74746f6d2d72696768742d7261646975733a3870787d2e726c37696c627b646973706c61793a626c6f636b3b636c6561723a626f74687d2e6c634a4631647b6d617267696e2d6c6566743a313670783b666c6f61743a72696768743b7d2e6b437259547b70616464696e673a31327078203136707820313270787d612e6664597371667b636f6c6f723a233442313141387d2e424e656177657b77686974652d73706163653a7072652d6c696e653b776f72642d777261703a627265616b2d776f72647d2e76766a774a627b636f6c6f723a233139363744323b666f6e742d73697a653a313670783b6c696e652d6865696768743a323070787d2e76766a774a6220613a766973697465647b636f6c6f723a233442313141387d2e76766a774a622e4872476465627b636f6c6f723a72676261283235352c3235352c3235352c31297d2e76766a774a622e48724764656220613a766973697465647b636f6c6f723a72676261283235352c3235352c3235352c2e37297d2e55506d69747b666f6e742d73697a653a313470783b6c696e652d6865696768743a323070787d2e55506d69742e4872476465627b636f6c6f723a72676261283235352c3235352c3235352c2e37297d2e55506d69742e415037576e647b636f6c6f723a7267626128302c3130322c33332c31297d2e7835346774667b6865696768743a3170783b6261636b67726f756e642d636f6c6f723a236466653165353b6d617267696e3a3020313670787d2e4170354f53647b70616464696e672d626f74746f6d3a313270787d2e7333763972647b666f6e742d73697a653a313470783b6c696e652d6865696768743a323070787d2e7333763972642e4872476465627b636f6c6f723a72676261283235352c3235352c3235352c31297d2e7333763972642e415037576e647b636f6c6f723a233230323132347d2e6d53783145657b70616464696e672d6c6566743a343870783b6d617267696e3a307d2e7639693631657b70616464696e672d626f74746f6d3a3870787d2e584c6c6f58657b636f6c6f723a233139363744323b666f6e742d73697a653a313470783b6c696e652d6865696768743a323070787d2e584c6c6f586520613a766973697465647b636f6c6f723a233442313141387d2e584c6c6f58652e4872476465627b636f6c6f723a72676261283235352c3235352c3235352c31297d2e584c6c6f58652e48724764656220613a766973697465647b636f6c6f723a72676261283235352c3235352c3235352c2e37297d2e5a54763942627b646973706c61793a626c6f636b7d2e6465497643627b666f6e742d73697a653a313670783b6c696e652d6865696768743a323070783b666f6e742d7765696768743a3430307d2e6465497643622e4872476465627b636f6c6f723a72676261283235352c3235352c3235352c31297d2e6465497643622e415037576e647b636f6c6f723a233230323132347d2e4643557030637b666f6e742d7765696768743a626f6c647d2e58374e5456657b646973706c61793a7461626c653b77696474683a313030253b70616464696e672d72696768743a313670783b626f782d73697a696e673a626f726465722d626f787d2e74486d6651657b646973706c61793a7461626c652d63656c6c3b70616464696e673a313270782030203132707820313670787d2e554874726b7b77696474683a373270787d2e4842544d36647b77696474683a333070787d2e5853377947647b646973706c61793a7461626c652d63656c6c3b746578742d616c69676e3a63656e7465723b766572746963616c2d616c69676e3a6d6964646c653b70616464696e673a3132707820302031327078203870787d2e616d335142667b646973706c61793a7461626c653b766572746963616c2d616c69676e3a746f707d2e5862355652657b636f6c6f723a233139363744327d613a76697369746564202e5862355652657b636f6c6f723a233442313141387d2e5862355652652e74723064777b636f6c6f723a72676261283235352c3235352c3235352c31297d613a76697369746564202e5862355652652e74723064777b636f6c6f723a72676261283235352c3235352c3235352c2e37297d2e74416438447b666f6e742d73697a653a313470783b6c696e652d6865696768743a323070787d2e74416438442e4872476465627b636f6c6f723a72676261283235352c3235352c3235352c2e37297d2e74416438442e415037576e647b636f6c6f723a233730373537417d2e614a79694f637b636f6c6f723a233030363632317d2e6f754a3943627b666c6f61743a6c6566743b70616464696e672d72696768743a313670787d2e526f434c6e657b636c6561723a626f74687d2e45594f736c647b646973706c61793a696e6c696e652d626c6f636b3b706f736974696f6e3a72656c61746976657d2e424669395a627b6f766572666c6f773a68696464656e3b706f736974696f6e3a72656c61746976657d2e53374a647a657b616c69676e2d6974656d733a63656e7465723b646973706c61793a666c65783b666c65782d646972656374696f6e3a636f6c756d6e3b6a7573746966792d636f6e74656e743a73706163652d61726f756e647d2e58646c7230647b6f766572666c6f772d783a6175746f3b2d7765626b69742d6f766572666c6f772d7363726f6c6c696e673a746f7563683b6d617267696e3a30202d3870783b70616464696e673a313670782030203136707820323470783b70616464696e672d746f703a3270783b6d617267696e2d746f703a2d3270783b7472616e73666f726d3a7472616e736c617465336428302c302c30297d2e6964673862657b646973706c61793a7461626c653b626f726465722d636f6c6c617073653a73657061726174653b626f726465722d73706163696e673a38707820303b6d617267696e3a30202d3870783b70616464696e672d72696768743a313670787d2e425647304e627b646973706c61793a7461626c652d63656c6c3b766572746963616c2d616c69676e3a746f703b6261636b67726f756e642d636f6c6f723a236666663b626f726465722d7261646975733a3870783b626f782d736861646f773a30203170782036707820726762612833322c2033332c2033362c20302e3238293b6f766572666c6f773a68696464656e7d2e55796b5439647b646973706c61793a626c6f636b3b666c6f61743a72696768743b70616464696e672d6c6566743a313670787d2e6e59543751627b636c6561723a626f74687d2e736b566770627b646973706c61793a7461626c653b7461626c652d6c61796f75743a66697865643b77696474683a313030257d2e5647484d58647b646973706c61793a7461626c652d63656c6c3b766572746963616c2d616c69676e3a6d6964646c653b6865696768743a353270783b746578742d616c69676e3a63656e7465727d2e4c70614472627b6d617267696e3a30206175746f203870783b646973706c61793a626c6f636b7d2e766253684f657b70616464696e672d746f703a307d2e4156736570667b70616464696e672d626f74746f6d3a3870787d2e4156736570662e753278314f647b70616464696e672d626f74746f6d3a307d2e787063202e6877632c2e787078202e6877787b646973706c61793a6e6f6e657d2e5275386964627b6d617267696e2d746f703a2d313670787d2e70756e657a7b666f6e742d7765696768743a3730303b6c65747465722d73706163696e673a302e373570783b746578742d7472616e73666f726d3a7570706572636173657d2e7779727758637b666f6e742d73697a653a313270783b6c696e652d6865696768743a313670787d2e7779727758632e4872476465627b636f6c6f723a72676261283235352c3235352c3235352c31297d2e7779727758632e415037576e647b636f6c6f723a233230323132347d2e6d4868796c667b646973706c61793a7461626c652d63656c6c3b766572746963616c2d616c69676e3a6d6964646c657d2e575a35474a667b616c69676e2d6974656d733a63656e7465723b70616464696e673a3020323070783b6d696e2d77696474683a31313270787d2e714e394b65642c2e44586b354d657b6d617267696e3a30206175746f7d2e44586b354d657b6d617267696e2d626f74746f6d3a313270787d2e51693946647b6261636b67726f756e643a236666663b626f726465723a303b626f726465722d7261646975733a39393970783b646973706c61793a626c6f636b3b6865696768743a353670783b6a7573746966792d636f6e74656e743a63656e7465723b77696474683a353670783b7a2d696e6465783a307d2e51693946647b626f782d736861646f773a30203170782036707820726762612833322c2033332c2033362c20302e3238292c696e7365742030203020302030207267626128302c302c302c302e3130292c696e73657420302030203020302072676261283235352c3235352c3235352c302e3530297d2e51693946643a666f6375737b6f75746c696e653a6e6f6e657d2e5169394664202e685748754a7b646973706c61793a626c6f636b3b6d617267696e3a30206175746f7d2e6a69356a70667b746578742d616c69676e3a63656e7465727d68727b626f726465723a303b626f726465722d626f74746f6d3a31707820736f6c696420236466653165353b6d617267696e3a303b7d2e425579624b652c2e48736e4642667b6d617267696e2d6c6566743a313670787d2e425579624b652c2e6f4d3247417b6d617267696e2d72696768743a313670787d2e584f377268637b6d617267696e3a30202d313670787d2e6949576d34627b626f782d73697a696e673a626f726465722d626f783b6d696e2d6865696768743a343870787d2e664c745873637b70616464696e673a313470783b746578742d616c69676e3a63656e7465727d2e4c796d38577b77696474683a313470783b6865696768743a323070783b706f736974696f6e3a72656c61746976653b6d617267696e3a30206175746f7d2e4164665872627b6d617267696e2d6c6566743a2d313470783b766572746963616c2d616c69676e3a6d6964646c653b646973706c61793a696e6c696e652d626c6f636b7d2e4c796d3857206469767b706f736974696f6e3a6162736f6c7574653b626f726465722d6c6566743a37707820736f6c6964207472616e73706172656e743b626f726465722d72696768743a37707820736f6c6964207472616e73706172656e743b77696474683a303b6865696768743a303b6c6566743a307d2e4979596145647b746f703a3770783b626f726465722d746f703a37707820736f6c696420233735373537357d2e4543554851657b746f703a3470783b626f726465722d746f703a37707820736f6c696420236666667d2e4165515175627b626f74746f6d3a3770783b626f726465722d626f74746f6d3a37707820736f6c696420233735373537357d2e5943553765627b626f74746f6d3a3470783b626f726465722d626f74746f6d3a37707820736f6c696420236666667d2e4963783643647b6d617267696e3a30206175746f203870787d2e6d41646a51637b746578742d616c69676e3a72696768747d2e75456563337b666f6e742d73697a653a313270783b6c696e652d6865696768743a313670787d2e75456563332e4872476465627b636f6c6f723a72676261283235352c3235352c3235352c2e37297d2e75456563332e415037576e647b636f6c6f723a233730373537417d2e724c736879662c2e426d503574667b70616464696e672d746f703a313270783b70616464696e672d626f74746f6d3a313270787d2e773143334c652c2e426d503574662c2e47354e6242647b70616464696e672d6c6566743a313670783b70616464696e672d72696768743a313670783b7d2e47354e6242647b70616464696e672d626f74746f6d3a313270787d2e6e4d796d65667b646973706c61793a666c65787d2e473565466c667b666c65783a313b646973706c61793a626c6f636b7d2e6e4d796d6566207370616e7b746578742d616c69676e3a63656e7465727d3c2f7374796c653e3c6469763e3c212d2d53575f435f582d2d3e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c64697620636c6173733d226b43725954223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f26616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351466a4141656751494278414226616d703b7573673d414f76566177327466496430584346385649396a682d324f35555069223e3c64697620636c6173733d22424e656177652076766a774a6220415037576e64223e4178696f733c2f6469763e3c64697620636c6173733d22424e656177652055506d697420415037576e64223e68747470733a2f2f7777772e6178696f732e636f6d3c2f6469763e3c2f613e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d226b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c6469763e3c6469763e3c64697620636c6173733d224170354f5364223e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e536d6172742c20656666696369656e74206e65777320776f72746879206f6620796f75722074696d652c20617474656e74696f6e2c20616e642074727573742e3c2f6469763e3c2f6469763e3c64697620636c6173733d22763969363165223e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c7370616e20636c6173733d22424e65617765223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f706f6c697469637326616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673516a42417741586f4543416351417726616d703b7573673d414f765661773139685044464b57637a653030627974527851476576223e3c7370616e20636c6173733d22584c6c6f586520415037576e64223e506f6c69746963733c2f7370616e3e3c2f613e3c2f7370616e3e3c2f6469763e3c2f6469763e3c64697620636c6173733d22763969363165223e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c7370616e20636c6173733d22424e65617765223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f617574686f72732f6e6577736465736b26616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673516a424177416e6f4543416351425126616d703b7573673d414f7656617730315a6b4f30626d50386e4370657152386257474959223e3c7370616e20636c6173733d22584c6c6f586520415037576e64223e53746f72696573206279204178696f733c2f7370616e3e3c2f613e3c2f7370616e3e3c2f6469763e3c2f6469763e3c64697620636c6173733d22763969363165223e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c7370616e20636c6173733d22424e65617765223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f6178696f732d64617368626f6172642d313535303631393334332d64653734306436612d656163312d343539392d383262642d3434303533343537376639322e68746d6c26616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673516a42417741336f4543416351427726616d703b7573673d414f76566177305658706145566f434f444153557657784f61466652223e3c7370616e20636c6173733d22584c6c6f586520415037576e64223e4178696f732044617368626f6172643c2f7370616e3e3c2f613e3c2f7370616e3e3c2f6469763e3c2f6469763e3c64697620636c6173733d22763969363165223e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c7370616e20636c6173733d22424e65617765223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f6e6577736c65747465727326616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673516a42417742486f4543416351435126616d703b7573673d414f76566177316255574b54564d7a4944436d736a34536d614c4755223e3c7370616e20636c6173733d22584c6c6f586520415037576e64223e4e6577736c6574746572733c2f7370616e3e3c2f613e3c2f7370616e3e3c2f6469763e3c2f6469763e3c64697620636c6173733d22763969363165223e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c7370616e20636c6173733d22424e65617765223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f776f726c6426616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673516a42417742586f4543416351437726616d703b7573673d414f765661773246776f732d5f6e416d39666e4963486a754b325a53223e3c7370616e20636c6173733d22584c6c6f586520415037576e64223e576f726c643c2f7370616e3e3c2f613e3c2f7370616e3e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c7370616e20636c6173733d22424e65617765223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f627573696e65737326616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673516a424177426e6f4543416351445126616d703b7573673d414f76566177324366426a4a724a72716e5052623241754d614e4953223e3c7370616e20636c6173733d22584c6c6f586520415037576e64223e427573696e6573733c2f7370616e3e3c2f613e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c64697620636c6173733d226b43725954223e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e3c7370616e20636c6173733d224643557030632072514d516f64223e546f702073746f726965733c2f7370616e3e3c2f6469763e3c2f7370616e3e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f6e6577736c6574746572732f6178696f732d616d2d61653065626362352d616432352d346531632d623662642d3138633530393464656330312e68746d6c26616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351714f63424d4164364241674145414926616d703b7573673d414f765661773148445a7242345437756b327137384256725676426f223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e3c7370616e20636c6173733d2272514d516f6420586235565265223e4178696f7320414d202d2041756775737420392c20323031393c2f7370616e3e3c2f6469763e3c2f7370616e3e3c7370616e3e3c64697620636c6173733d22424e6561776520744164384420415037576e64223e3c7370616e20636c6173733d2272514d516f6420614a79694f63223e4178696f733c2f7370616e3e20b72031206461792061676f3c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f636f756e74726965732d6d6f73742d7269736b2d77617465722d6372697369732d66343066343866392d623032652d346536622d393965642d3136346335636239353333352e68746d6c26616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351714f63424d4168364241674145415526616d703b7573673d414f7656617733505243566f5a5532675a494b44336b62615072544e223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e3c7370616e20636c6173733d2272514d516f6420586235565265223e54686520636f756e7472696573206d6f7374206174207269736b206f662061207761746572206372697369733c2f7370616e3e3c2f6469763e3c2f7370616e3e3c7370616e3e3c64697620636c6173733d22424e6561776520744164384420415037576e64223e3c7370616e20636c6173733d2272514d516f6420614a79694f63223e4178696f733c2f7370616e3e20b72031206461792061676f3c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c6469763e3c6120687265663d222f75726c3f713d68747470733a2f2f747769747465722e636f6d2f6178696f732533467265665f73726325334474777372632532353545676f6f676c652532353743747763616d702532353545736572702532353743747767722532353545617574686f7226616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673513646347743586f4543415951416726616d703b7573673d414f7656617733626475774e6f5934356137397a30464775566e566d223e3c64697620636c6173733d226b43725954223e3c64697620636c6173733d226f754a3943622053374a647a6522207374796c653d2277696474683a343070783b6865696768743a34307078223e3c696d6720636c6173733d2245594f736c642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a343070783b6d61782d6865696768743a34307078222069643d2264696d675f312220646174612d64656665727265643d2231223e3c2f6469763e3c6469763e3c64697620636c6173733d22424e656177652076766a774a6220415037576e64223e4178696f7320262331303030333b3c2f6469763e3c64697620636c6173733d22424e656177652055506d697420415037576e64223e54776974746572202623383235303b206178696f733c2f6469763e3c2f6469763e3c64697620636c6173733d22526f434c6e65223e3c2f6469763e3c2f6469763e3c2f613e3c2f6469763e3c6469763e3c6469763e3c64697620636c6173733d2258646c723064223e3c64697620636c6173733d22696467386265223e3c6120636c6173733d22425647304e622220687265663d222f75726c3f713d68747470733a2f2f747769747465722e636f6d2f6178696f732f7374617475732f313135393838373636383733393733393634382533467265665f73726325334474777372632532353545676f6f676c652532353743747763616d702532353545736572702532353743747767722532353545747765657426616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351676c5177436e6f4543415951424126616d703b7573673d414f7656617733765932387a4a6374626a64305a365357505a344c63223e3c6469763e3c646976207374796c653d2277696474683a3233327078223e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e5477697474657220686173207265696e737461746564204d69746368204d63436f6e6e656c6c27732072652d656c656374696f6e2063616d706169676e206163636f756e74206166746572207468652070726f66696c65207761732073757370656e646564206561726c6965722074686973207765656b2e207777772e6178696f732e636f6d2f6d697463682d6d63636f6e2623383233303b3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d22424e6561776520744164384420415037576e64223e313920686f7572732061676f3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f75726c3f713d68747470733a2f2f747769747465722e636f6d2f6178696f732f7374617475732f313135393838333433333933313336363430322533467265665f73726325334474777372632532353545676f6f676c652532353743747763616d702532353545736572702532353743747767722532353545747765657426616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351676c517743336f4543415951426726616d703b7573673d414f76566177336c4b50674566354d5351724b75784655584d614f41223e3c6469763e3c646976207374796c653d2277696474683a3233327078223e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e4e657720746563686e6f6c6f677920697320726573686170696e67206173736574206d616e6167656d656e7420666f722061206e65772067656e65726174696f6e206f6620776f726b6572732c20616e64206d6f7374206f662074686520696e64757374727920686173206e6f74206b65707420706163652e207777772e6178696f732e636f6d2f61737365742d6d616e61672623383233303b3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d22424e6561776520744164384420415037576e64223e323020686f7572732061676f3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f75726c3f713d68747470733a2f2f747769747465722e636f6d2f6178696f732f7374617475732f313135393837373735323139323939353333332533467265665f73726325334474777372632532353545676f6f676c652532353743747763616d702532353545736572702532353743747767722532353545747765657426616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351676c517744486f4543415951434126616d703b7573673d414f7656617732554b4158715044627577556a736d37626342675874223e3c6469763e3c646976207374796c653d2277696474683a3233327078223e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e5374657068656e20526f73732022667265616b6564206f75742220617420746865206261636b6c61736820746f20686973205472756d702066756e64726169736572206173205472756d70206173736f636961746573207065727375616465642068696d20746f20676f206168656164207769746820746865206576656e742061742068697320536f757468616d70746f6e206d616e73696f6e2e207777772e6178696f732e636f6d2f7374657068656e2d726f732623383233303b3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d22424e6561776520744164384420415037576e64223e323020686f7572732061676f3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f75726c3f713d68747470733a2f2f747769747465722e636f6d2f6178696f732f7374617475732f313135393837323436373432383530373634392533467265665f73726325334474777372632532353545676f6f676c652532353743747763616d702532353545736572702532353743747767722532353545747765657426616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351676c517744586f4543415951436726616d703b7573673d414f7656617733683854556374412d32397252656e584742497a7a5f223e3c6469763e3c646976207374796c653d2277696474683a3233327078223e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e4265796f6e6420726570616972696e6720616e6420696d70726f76696e6720726f61647320616e64207369646577616c6b732c20636974696573206861766520616e206f70706f7274756e69747920746f206275696c6420696e667261737472756374757265207468617420636f756c64206f70656e207570206e6577206d6f62696c697479206f7074696f6e7320616e6420696e637265617365206163636573736962696c6974792c207772697465732045787065727420566f6963657320636f6e7472696275746f722048656e727920436c6179706f6f6c2e207777772e6178696f732e636f6d2f616d642d6c6973612d73752623383233303b3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d22424e6561776520744164384420415037576e64223e323020686f7572732061676f3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f75726c3f713d68747470733a2f2f747769747465722e636f6d2f6178696f732f7374617475732f313135393836373138323634333430343830322533467265665f73726325334474777372632532353545676f6f676c652532353743747763616d702532353545736572702532353743747767722532353545747765657426616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351676c5177446e6f4543415951444126616d703b7573673d414f765661773056326641356f4573505f6b723267467962676b5568223e3c6469763e3c646976207374796c653d2277696474683a3233327078223e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e414d442043454f204c697361205375206469736375737365732077697468204178696f732068657220636f6d70616e79277320726573757267656e636520616e6420626174746c6573207769746820496e74656c207777772e6178696f732e636f6d2f616d642d6c6973612d73752623383233303b3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d22424e6561776520744164384420415037576e64223e323120686f7572732061676f3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c64697620636c6173733d226b43725954223e3c6120687265663d222f75726c3f713d68747470733a2f2f6769746875622e636f6d2f6178696f732f6178696f7326616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351466a4150656751494242414226616d703b7573673d414f7656617733486a474b6775516d6342745456445566344d665072223e3c64697620636c6173733d22424e656177652076766a774a6220415037576e64223e6178696f732f6178696f733a2050726f6d697365206261736564204854545020636c69656e7420666f72207468652062726f7773657220616e64202e2e2e202d204769744875623c2f6469763e3c64697620636c6173733d22424e656177652055506d697420415037576e64223e68747470733a2f2f6769746875622e636f6d202623383235303b206178696f73202623383235303b206178696f733c2f6469763e3c2f613e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d226b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c6469763e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e636f6e7374206178696f73203d207265717569726528276178696f7327293b202f2f204d616b652061207265717565737420666f72206120757365722077697468206120676976656e204944206178696f732e2067657428272f757365723f49443d31323334352729202e7468656e2866756e6374696f6e2028726573706f6e736529207b202f2f2068616e646c652073756363657373a02e2e2e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c64697620636c6173733d226b43725954223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f26616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673517757347745486f4543416b51416726616d703b7573673d414f7656617733565033564769536e46355355474c45653730387037223e3c696d6720636c6173733d2255796b5439642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a373270783b6d61782d6865696768743a37327078222069643d2264696d675f32332220646174612d64656665727265643d2231223e3c2f613e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e4178696f733c2f6469763e3c2f7370616e3e3c7370616e3e3c64697620636c6173733d22424e6561776520744164384420415037576e64223e576562736974653c2f6469763e3c2f7370616e3e3c64697620636c6173733d226e5954375162223e3c2f6469763e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d226b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e4178696f7320697320616e20416d65726963616e206e65777320616e6420696e666f726d6174696f6e207765627369746520666f756e64656420696e203230313620627920666f726d657220506f6c697469636f207374616666657273204a696d2056616e64654865692c204d696b6520416c6c656e2c20616e6420526f7920536368776172747a2e204974206f6666696369616c6c79206c61756e6368656420696e20323031372e2054686520736974652773206e616d65206973206261736564206f6e2074686520477265656b3a202623373934303b26233935383b26233935333b26233935393b26233936323b2c206d65616e696e672022776f72746879222e203c7370616e20636c6173733d22424e65617765223e3c6120687265663d222f75726c3f713d68747470733a2f2f656e2e77696b6970656469612e6f72672f77696b692f4178696f735f28776562736974652926616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673516d684d77456e6f4543416b51427726616d703b7573673d414f76566177306a76706f32364a65425a684a7638496157384e7163223e3c7370616e20636c6173733d22584c6c6f586520415037576e64223e57696b6970656469613c2f7370616e3e3c2f613e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c64697620636c6173733d22766253684f65206b43725954223e3c64697620636c6173733d22415673657066223e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c7370616e3e3c7370616e20636c6173733d22424e656177652073337639726420415037576e64223e4f776e65723c2f7370616e3e3c2f7370616e3e3a203c7370616e3e3c7370616e20636c6173733d22424e6561776520744164384420415037576e64223e4178696f73204d6564696120496e633c2f7370616e3e3c2f7370616e3e3c2f6469763e3c2f6469763e3c64697620636c6173733d2241567365706620753278314f64223e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c7370616e3e3c7370616e20636c6173733d22424e656177652073337639726420415037576e64223e4c61756e636865643c2f7370616e3e3c2f7370616e3e3a203c7370616e3e3c7370616e20636c6173733d22424e6561776520744164384420415037576e64223e323031373c2f7370616e3e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c64697620636c6173733d226b43725954223e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e3c7370616e20636c6173733d224643557030632072514d516f64223e50656f706c6520616c736f2073656172636820666f723c2f7370616e3e3c2f6469763e3c2f7370616e3e3c2f6469763e3c64697620636c6173733d22787063223e3c64697620636c6173733d22783534677466223e3c2f6469763e3c6469763e3c64697620636c6173733d226b43725954223e3c7370616e20636c6173733d2270756e657a223e3c64697620636c6173733d22424e656177652077797277586320415037576e64223e4c69626572616c206e65777320736f7572636573206c6973743c2f6469763e3c2f7370616e3e3c2f6469763e3c6469763e3c6469763e3c64697620636c6173733d2258646c723064223e3c64697620636c6173733d22696467386265223e3c6120636c6173733d22425647304e622220687265663d222f7365617263683f69653d5554462d3826616d703b713d434e4e26616d703b737469636b3d483473494141414141414141414f4f5155654c517a3955335343394f4c7a4b537a4d6c4d5369314b7a4648495379307656696a4f4c79314b54693157794d6b734c6f6c6954617a497a43382d7851685866497152433851304d6f38334b726545636a4a79697772536b36434b6b67324b633644693859565a35686e707678676c6658435a333844437549695632646e504477435833376e476b414141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735173396f424d425a3642416749454149223e3c6469763e3c646976207374796c653d2277696474683a3131327078223e3c64697620636c6173733d2253374a647a6522207374796c653d2277696474683a31313270783b6865696768743a3131327078223e3c696d6720636c6173733d2245594f736c642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a31313270783b6d61782d6865696768743a3131327078222069643d2264696d675f332220646174612d64656665727265643d2231223e3c2f6469763e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e434e4e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f7365617263683f69653d5554462d3826616d703b713d506f6c697469636f26616d703b737469636b3d483473494141414141414141414f4f5155654c537a3955334d444b504e7971334e4a4c4d7955784b4c55724d5563684c4c5339574b4d34764c55704f4c5662497953777569574a4e724d6a4d4c7a3746794146536e6c3663586e534b45556b6e6c4a4f52573153516e6752566c4778516e414d566a795f4d4d7339495f38556f3659504c5f415957786b5773484148354f5a6b6c6d636e3541484749687547584141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735173396f424d425a364241674945414d223e3c6469763e3c646976207374796c653d2277696474683a3131327078223e3c64697620636c6173733d2253374a647a6522207374796c653d2277696474683a31313270783b6865696768743a3131327078223e3c696d6720636c6173733d2245594f736c642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a31313270783b6d61782d6865696768743a3131327078222069643d2264696d675f352220646174612d64656665727265643d2231223e3c2f6469763e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e506f6c697469636f3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f7365617263683f69653d5554462d3826616d703b713d48756666506f737426616d703b737469636b3d483473494141414141414141414f4f5155654c537a395533794d67744b6b68504d704c4d7955784b4c55724d5563684c4c5339574b4d34764c55704f4c5662497953777569574a4e724d6a4d4c7a3746794146536e6c3663586e534b45617a5479447a65714e7753796f45594131575562464363417857504c3877797a306a5f78536a7067387638426862475261776348715670615148357853554175376a463070634141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735173396f424d425a3642416749454151223e3c6469763e3c646976207374796c653d2277696474683a3131327078223e3c64697620636c6173733d2253374a647a6522207374796c653d2277696474683a31313270783b6865696768743a3131327078223e3c696d6720636c6173733d2245594f736c642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a31313270783b6d61782d6865696768743a3131327078222069643d2264696d675f372220646174612d64656665727265643d2231223e3c2f6469763e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e48756666506f73743c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f7365617263683f69653d5554462d3826616d703b713d4e505226616d703b737469636b3d483473494141414141414141414f4f5155654c517a3955335344596f7a6a47537a4d6c4d5369314b7a4648495379307656696a4f4c79314b54693157794d6b734c6f6c6954617a497a43382d785168576e463663586e534b6b5176454e444b504e79713368484979636f734b30704f67696b416d5173586a4337504d4d394a5f4d55723634444b5f675956784553757a583041514142514b584c32514141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735173396f424d425a3642416749454155223e3c6469763e3c646976207374796c653d2277696474683a3131327078223e3c64697620636c6173733d2253374a647a6522207374796c653d2277696474683a31313270783b6865696768743a3131327078223e3c696d6720636c6173733d2245594f736c642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a31313270783b6d61782d6865696768743a3131327078222069643d2264696d675f392220646174612d64656665727265643d2231223e3c2f6469763e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e4e6174696f6e616c205075626c69632052612e2e2e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d226d4868796c66223e3c64697620636c6173733d22575a35474a66223e3c6120636c6173733d22714e394b65642220687265663d222f7365617263683f69653d5554462d3826616d703b713d4c69626572616c2b6e6577732b736f75726365732b6c69737426616d703b737469636b3d483473494141414141414141414f4f514d5a4c4d7955784b4c55724d5563684c4c5339574b4d34764c55704f4c5662497953777569574a4e724d6a4d4c7a3746794b476671322d515870786564497152433851304d6f38334b726545636a4a79697772536b36434b6b67324b633644693859565a35686e707678676c6658435a3338444375496756747a514146695a305070774141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673517a4f30424d425a3642416749454159223e3c627574746f6e20636c6173733d2244586b354d65205169394664223e3c696d6720636c6173733d22685748754a2220616c743d224172726f7722207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a323470783b6d61782d6865696768743a32347078222069643d2264696d675f31312220646174612d64656665727265643d2231223e3c2f627574746f6e3e3c64697620636c6173733d22424e65617765206a69356a706620744164384420415037576e64223e4d6f726520726573756c74733c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c64697620636c6173733d22687763223e3c64697620636c6173733d226b43725954223e3c7370616e20636c6173733d2270756e657a223e3c64697620636c6173733d22424e656177652077797277586320415037576e64223e4c69626572616c20706f6c69746963616c2077656273697465733c2f6469763e3c2f7370616e3e3c2f6469763e3c6469763e3c6469763e3c64697620636c6173733d2258646c723064223e3c64697620636c6173733d22696467386265223e3c6120636c6173733d22425647304e622220687265663d222f7365617263683f69653d5554462d3826616d703b713d536c6174652b4d6167617a696e6526616d703b737469636b3d483473494141414141414141414f4f5155654c557a3955334d4449724e7259776b73724a54456f74537378524b4d6a5079537a4a54416179796c4f54696a4e4c556f756a57424d724d764f4c547a4679675a5762787875565730493547626c4642656c4a70786752426b456c7a4d334b636f70796f524c47795a55705656433253564a4b5354715562576c55626c7a796931484b423666564453794d69316a35676e4d5353314956664250544536737938314942452d4b766d72674141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735173396f424d425a3642416749454167223e3c6469763e3c646976207374796c653d2277696474683a3131327078223e3c64697620636c6173733d2253374a647a6522207374796c653d2277696474683a31313270783b6865696768743a3131327078223e3c696d6720636c6173733d2245594f736c642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a31313270783b6d61782d6865696768743a3131327078222069643d2264696d675f31332220646174612d64656665727265643d2231223e3c2f6469763e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e536c617465204d6167617a696e653c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f7365617263683f69653d5554462d3826616d703b713d42757a7a4665656426616d703b737469636b3d483473494141414141414141414f4f5155654c537a3955334d44637279796e4b4e5a4c4b7955784b4c55724d55536a497a386b7379557747737370546b346f7a53314b4c6f3167544b7a4c7a69303878677455626d6363626c5674434f526d3552515870536163594f6345795a735847466c414a694b6c514365506b797051714b4e736b4b61556b486371324e436f334c766e464b4f5744302d6f4746735a467242784f7056565662716d704b5144564e44327173774141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735173396f424d425a364241674945416b223e3c6469763e3c646976207374796c653d2277696474683a3131327078223e3c64697620636c6173733d2253374a647a6522207374796c653d2277696474683a31313270783b6865696768743a3131327078223e3c696d6720636c6173733d2245594f736c642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a31313270783b6d61782d6865696768743a3131327078222069643d2264696d675f31352220646174612d64656665727265643d2231223e3c2f6469763e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e42757a7a466565643c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f7365617263683f69653d5554462d3826616d703b713d4461696c792b4b6f7326616d703b737469636b3d483473494141414141414141414f4f5155654c557a3955334d453675544b6b796b73724a54456f74537378524b4d6a5079537a4a54416179796c4f54696a4e4c556f756a57424d724d764f4c547a4679675a51626d6363626c5674434f526d3552515870536163597751595a6d5255625730416c7a4d334b636f70796f524a67473642736b3653556b6e516f32394b6f334c6a6b46364f554430367247316759463746797569526d356c5171654f635841774447505a396673774141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735173396f424d425a364241674945416f223e3c6469763e3c646976207374796c653d2277696474683a3131327078223e3c64697620636c6173733d2253374a647a6522207374796c653d2277696474683a31313270783b6865696768743a3131327078223e3c696d6720636c6173733d2245594f736c642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a31313270783b6d61782d6865696768743a3131327078222069643d2264696d675f31372220646174612d64656665727265643d2231223e3c2f6469763e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e4461696c79204b6f733c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f7365617263683f69653d5554462d3826616d703b713d54616c6b696e672b506f696e74732b4d656d6f26616d703b737469636b3d483473494141414141414141414f4f5155654c557a3955334d456c4b4b556b336b73724a54456f74537378524b4d6a5079537a4a54416179796c4f54696a4e4c556f756a57424d724d764f4c547a4679675a51626d6363626c5674434f526d3552515870536163597751595a6d5255625730416c7a4d334b636f70796f524c47795a5570565641323244596f32394b6f334c6a6b46364f5544303672473167594637454b6879546d5a47666d705373453547666d6c5251722d4b626d35674d415637446f6d62304141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735173396f424d425a3642416749454173223e3c6469763e3c646976207374796c653d2277696474683a3131327078223e3c64697620636c6173733d2253374a647a6522207374796c653d2277696474683a31313270783b6865696768743a3131327078223e3c696d6720636c6173733d2245594f736c642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a31313270783b6d61782d6865696768743a3131327078222069643d2264696d675f31392220646174612d64656665727265643d2231223e3c2f6469763e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e54616c6b696e6720506f696e7473204d652e2e2e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d226d4868796c66223e3c64697620636c6173733d22575a35474a66223e3c6120636c6173733d22714e394b65642220687265663d222f7365617263683f69653d5554462d3826616d703b713d4c69626572616c2b706f6c69746963616c2b776562736974657326616d703b737469636b3d483473494141414141414141414f4f514d5a4c4b7955784b4c55724d55536a497a386b7379557747737370546b346f7a53314b4c6f3167544b7a4c7a6930387863756e6e366873596d6363626c5674434f526d3552515870536163594f6345795a735847466c414a63374f796e4b4a6371495278636d564b465a52746b7052536b67356c57787156473566385970547977576c314177766a496c5938386744456f63564e75514141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673517a4f30424d425a3642416749454177223e3c627574746f6e20636c6173733d2244586b354d65205169394664223e3c696d6720636c6173733d22685748754a2220616c743d224172726f7722207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a323470783b6d61782d6865696768743a32347078222069643d2264696d675f32312220646174612d64656665727265643d2231223e3c2f627574746f6e3e3c64697620636c6173733d22424e65617765206a69356a706620744164384420415037576e64223e4d6f726520726573756c74733c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c68723e3c64697620636c6173733d226475662d68223e3c64697620636c6173733d22664c74587363206949576d346222206f6e636c69636b3d227870287468697329223e3c64697620636c6173733d224c796d3857223e3c64697620636c6173733d2241655151756220687763223e3c2f6469763e3c64697620636c6173733d2259435537656220687763223e3c2f6469763e3c64697620636c6173733d2249795961456420687778223e3c2f6469763e3c64697620636c6173733d2245435548516520687778223e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c64697620636c6173733d226b43725954223e3c6120687265663d222f75726c3f713d68747470733a2f2f656e2e77696b6970656469612e6f72672f77696b692f4178696f735f28776562736974652926616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351466a4159656751494252414226616d703b7573673d414f76566177333867573437455f39394a326a683746384149573439223e3c64697620636c6173733d22424e656177652076766a774a6220415037576e64223e4178696f7320287765627369746529202d2057696b6970656469613c2f6469763e3c64697620636c6173733d22424e656177652055506d697420415037576e64223e68747470733a2f2f656e2e77696b6970656469612e6f7267202623383235303b2077696b69202623383235303b204178696f735f2877656273697465293c2f6469763e3c2f613e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d226b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c6469763e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e4178696f7320287374796c697a6564206173204158494f532920697320616e20416d65726963616e206e65777320616e6420696e666f726d6174696f6e207765627369746520666f756e64656420696e203230313620627920666f726d657220506f6c697469636f207374616666657273204a696d2056616e64654865692c204d696b6520416c6c656e2c20616e6420526f7920536368776172747a2e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c64697620636c6173733d226b43725954223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e66616365626f6f6b2e636f6d2f6178696f736e6577732f26616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351466a415a656751494178414226616d703b7573673d414f76566177306e6d4a4834444273432d317679714158357832346a223e3c64697620636c6173733d22424e656177652076766a774a6220415037576e64223e4178696f73202d20486f6d65207c2046616365626f6f6b3c2f6469763e3c64697620636c6173733d22424e656177652055506d697420415037576e64223e68747470733a2f2f7777772e66616365626f6f6b2e636f6d202623383235303b202e2e2e202623383235303b204272616e64202623383235303b2057656273697465202623383235303b204e6577732026616d703b204d6564696120576562736974653c2f6469763e3c2f613e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d226b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c6469763e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e5468657265206973206e6f207761792052657075626c6963616e732063616e206368616e6765206269727468207261746573206f7220637572622074686973207472656e64202623383231323b20616e642074686572652773206e6f7420612073696e676c652064656d6f67726170686963206d6567617472656e642074686174206661766f72732052657075626c6963616e732e206178696f732e636f6d2e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c64697620636c6173733d226b43725954223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e68626f2e636f6d2f6178696f7326616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351466a4161656751494168414226616d703b7573673d414f7656617730314959735a763273624653415f745778396f6a6850223e3c64697620636c6173733d22424e656177652076766a774a6220415037576e64223e4178696f73202d204f6666696369616c205765627369746520666f72207468652048424f20536572696573202d2048424f2e636f6d3c2f6469763e3c64697620636c6173733d22424e656177652055506d697420415037576e64223e68747470733a2f2f7777772e68626f2e636f6d202623383235303b206178696f733c2f6469763e3c2f613e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d226b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c6469763e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e4b6e6f776e20666f722064656c69766572696e67206e6577732c20636f7665726167652c20616e6420696e7369676874207769746820612064697374696e6374697665206272616e64206f6620736d61727420627265766974792c204178696f73206f6e2048424f2068656c707320766965776572732062657474657220756e6465727374616e642074686520626967207472656e6473a02e2e2e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c64697620636c6173733d226b43725954223e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e3c7370616e20636c6173733d224643557030632072514d516f64223e52656c617465642073656172636865733c2f7370616e3e3c2f6469763e3c2f7370616e3e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b6a7326616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735131514a3642416742454145223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e6178696f73206a733c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d224842544d366420585337794764223e3c6120687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b6a73223e3c64697620636c6173733d22424e65617765206d41646a516320754565633320415037576e64223e2667743b3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b68626f26616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735131514a3642416742454149223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e6178696f732068626f3c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d224842544d366420585337794764223e3c6120687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b68626f223e3c64697620636c6173733d22424e65617765206d41646a516320754565633320415037576e64223e2667743b3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b6d65616e696e6726616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735131514a364241674245414d223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e6178696f73206d65616e696e673c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d224842544d366420585337794764223e3c6120687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b6d65616e696e67223e3c64697620636c6173733d22424e65617765206d41646a516320754565633320415037576e64223e2667743b3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b76756526616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735131514a3642416742454151223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e6178696f73207675653c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d224842544d366420585337794764223e3c6120687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b767565223e3c64697620636c6173733d22424e65617765206d41646a516320754565633320415037576e64223e2667743b3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b76732b666574636826616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735131514a3642416742454155223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e6178696f732076732066657463683c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d224842544d366420585337794764223e3c6120687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b76732b6665746368223e3c64697620636c6173733d22424e65617765206d41646a516320754565633320415037576e64223e2667743b3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b72656163742b6e617469766526616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735131514a3642416742454159223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e6178696f73207265616374206e61746976653c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d224842544d366420585337794764223e3c6120687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b72656163742b6e6174697665223e3c64697620636c6173733d22424e65617765206d41646a516320754565633320415037576e64223e2667743b3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b74762b73686f7726616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735131514a3642416742454163223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e6178696f732074762073686f773c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d224842544d366420585337794764223e3c6120687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b74762b73686f77223e3c64697620636c6173733d22424e65617765206d41646a516320754565633320415037576e64223e2667743b3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b63646e26616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735131514a3642416742454167223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e6178696f732063646e3c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d224842544d366420585337794764223e3c6120687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b63646e223e3c64697620636c6173733d22424e65617765206d41646a516320754565633320415037576e64223e2667743b3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c666f6f7465723e203c6469763e20203c64697620636c6173733d225a494e62626320787064204f396735636320755550476920426d50357466223e3c64697620636c6173733d226e4d796d6566204d5578476264206c794c776c63223e3c6120636c6173733d226e424445316220473565466c662220687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b65693d704d524f586250624472505339414f426d4b5059425126616d703b73746172743d313026616d703b73613d4e2220617269612d6c6162656c3d224e6578742070616765223e4e657874202667743b3c2f613e3c2f6469763e3c2f6469763e203c2f6469763e2020203c6469762069643d226d436c6a6f62223e3c6469763e3c6120687265663d222f75726c3f713d68747470733a2f2f6163636f756e74732e676f6f676c652e636f6d2f536572766963654c6f67696e253346636f6e74696e7565253344687474703a2f2f7777772e676f6f676c652e636f6d2f73656172636825323533467125323533446178696f7325323532366f7125323533446178696f73253235323661717325323533446368726f6d652e302e36396935396c326a306c336a36396936302e3831316a306a372532353236736f75726365696425323533446368726f6d652532353236696525323533445554462d38253236686c253344656e26616d703b73613d5526616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673517873384343475126616d703b7573673d414f765661773338654a695f306b32366743644b726d315057417979223e5369676e20696e3c2f613e3c2f6469763e3c6469763e3c6120636c6173733d226b73545534632220687265663d22687474703a2f2f7777772e676f6f676c652e636f6d2f707265666572656e6365733f686c3d656e2d434126616d703b66673d3126616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735135665543434755223e53657474696e67733c2f613e3c6120636c6173733d226b73545534632220687265663d222f2f7777772e676f6f676c652e636f6d2f696e746c2f656e5f63612f706f6c69636965732f707269766163792f3f66673d31223e507269766163793c2f613e3c6120636c6173733d226b73545534632220687265663d222f2f7777772e676f6f676c652e636f6d2f696e746c2f656e5f63612f706f6c69636965732f7465726d732f3f66673d31223e5465726d733c2f613e3c2f6469763e3c2f6469763e20203c2f666f6f7465723e3c736372697074206e6f6e63653d224f77774e4a497141533843544d31352f4351595531513d3d223e2866756e6374696f6e28297b76617220686c3d27656e2d4341273b2866756e6374696f6e28297b76617220623d746869737c7c73656c662c643d2f5e5b5c772b2f5f2d5d2b5b3d5d7b302c327d242f2c653d6e756c6c3b76617220663d646f63756d656e742e717565727953656c6563746f7228222e6c22292c673d646f63756d656e742e717565727953656c6563746f72282223736622292c6b3d672e717565727953656c6563746f7228222e73626322292c6c3d672e717565727953656c6563746f7228225b747970653d746578745d22292c6d3d672e717565727953656c6563746f7228225b747970653d7375626d69745d22292c6e3d672e717565727953656c6563746f7228222e736322292c703d672e717565727953656c6563746f7228222e7822292c713d6c2e76616c75652c723d5b5d2c743d2d312c753d712c772c782c793b717c7c2870262628702e7374796c652e646973706c61793d226e6f6e6522292c7a28213129293b66756e6374696f6e207a2861297b6966286b2e636c6173734c6973742e636f6e7461696e732822657362632229297b76617220633d6b2e636c6173734c6973742e636f6e7461696e732822636873626322292c683d6b2e636c6173734c6973742e636f6e7461696e73282272746c73626322293b612626286e2e7374796c652e646973706c61793d22626c6f636b222c633f28672e7374796c652e626f726465725261646975733d2232307078203230707820302030222c6e2e7374796c652e626f72646572426f74746f6d3d2231707820736f6c69642023444645314535222c6d2e7374796c652e626f726465725261646975733d683f2232307078203020302030223a223020323070782030203022293a6b2e7374796c652e626f726465725261646975733d683f22302038707820302030223a2238707820302030203022293b617c7c286e2e7374796c652e646973706c61793d226e6f6e65222c633f28672e7374796c652e626f726465725261646975733d2232307078222c6e2e7374796c652e626f72646572426f74746f6d3d226e6f6e65222c6d2e7374796c652e626f726465725261646975733d683f2232307078203020302032307078223a223020323070782032307078203022293a6b2e7374796c652e626f726465725261646975733d683f223020387078203870782030223a22387078203020302038707822297d7d66756e6374696f6e204128297b672e717565727953656c6563746f7228225b6e616d653d6f715d22292e76616c75653d753b672e717565727953656c6563746f7228225b6e616d653d6171735d22292e76616c75653d22686569726c6f6f6d2d7372702e222b28303c3d743f743a2222292b222e222b28303c722e6c656e6774683f22306c222b722e6c656e6774683a2222297d0a66756e6374696f6e204328297b773d6e756c6c3b69662878297b76617220613d222f636f6d706c6574652f7365617263683f636c69656e743d686569726c6f6f6d2d73727026686c3d222b686c2b22266a736f6e3d742663616c6c6261636b3d685326713d222b656e636f6465555249436f6d706f6e656e742878293b22756e646566696e656422213d3d747970656f6620647326266473262628612b3d222664733d222b6473293b76617220633d646f63756d656e742e637265617465456c656d656e74282273637269707422293b632e7372633d613b6966286e756c6c3d3d3d6529613a7b613d622e646f63756d656e743b69662828613d612e717565727953656c6563746f722626612e717565727953656c6563746f7228227363726970745b6e6f6e63655d222929262628613d612e6e6f6e63657c7c612e67657441747472696275746528226e6f6e63652229292626642e74657374286129297b653d613b627265616b20617d653d22227d28613d65292626632e73657441747472696275746528226e6f6e6365222c61293b646f63756d656e742e626f64792e617070656e644368696c642863293b783d6e756c6c3b773d73657454696d656f757428432c353030297d7d0a66756e6374696f6e204428297b666f72283b6e2e66697273744368696c643b296e2e72656d6f76654368696c64286e2e66697273744368696c64293b723d5b5d3b743d2d313b7a282131297d66756e6374696f6e204528297b76617220613d6e2e717565727953656c6563746f7228222e73637322293b61262628612e636c6173734e616d653d2222293b303c3d743f28613d6e2e6368696c644e6f6465735b745d2c612e636c6173734e616d653d22736373222c713d612e74657874436f6e74656e74293a713d753b6c2e76616c75653d717d6c2e6164644576656e744c697374656e65722822666f637573222c66756e6374696f6e28297b66262628662e7374796c652e646973706c61793d226e6f6e6522297d2c2131293b6c2e6164644576656e744c697374656e65722822626c7572222c66756e6374696f6e28297b4428293b66262628662e7374796c652e646973706c61793d2222297d2c2131293b6c2e6164644576656e744c697374656e657228226b65797570222c66756e6374696f6e2861297b713d6c2e76616c75653b793d21313b31333d3d612e77686963683f4128293a32373d3d612e77686963683f284428292c66262628662e7374796c652e646973706c61793d2222292c713d752c6c2e76616c75653d71293a34303d3d612e77686963683f28742b2b2c743e3d722e6c656e677468262628743d2d31292c452829293a33383d3d612e77686963683f28742d2d2c2d313e74262628743d722e6c656e6774682d31292c452829293a28613d71293f2870262628702e7374796c652e646973706c61793d2222292c783d612c777c7c4328292c753d61293a2870262628702e7374796c652e646973706c61793d226e6f6e6522292c7a282131292c4428292c753d22222c793d2130297d2c2131293b6d2e6164644576656e744c697374656e65722822636c69636b222c412c2131293b702e6164644576656e744c697374656e65722822636c69636b222c66756e6374696f6e28297b6c2e76616c75653d22223b702e7374796c652e646973706c61793d226e6f6e65223b7a282131297d2c2131293b6b2e6164644576656e744c697374656e65722822636c69636b222c66756e6374696f6e28297b6c2e666f63757328297d2c2131293b77696e646f772e68533d66756e6374696f6e2861297b6966282179297b4428293b303d3d615b315d2e6c656e67746826267a282131293b666f722876617220633d303b633c615b315d2e6c656e6774683b632b2b297b76617220683d615b315d5b635d5b305d2c763d646f63756d656e742e637265617465456c656d656e74282264697622293b762e696e6e657248544d4c3d683b762e6164644576656e744c697374656e657228226d6f757365646f776e222c66756e6374696f6e2842297b422e70726576656e7444656661756c7428293b72657475726e21317d2c2131293b683d682e7265706c616365282f3c5c2f3f623e2f672c2222293b762e6164644576656e744c697374656e65722822636c69636b222c66756e6374696f6e2842297b72657475726e2066756e6374696f6e28297b743d423b4128293b4528293b4428293b672e7375626d697428297d7d2863292c2131293b6e2e617070656e644368696c642876293b7a282130293b722e707573682868297d7d7d3b7d292e63616c6c2874686973293b7d2928293b2866756e6374696f6e28297b66756e6374696f6e20622861297b666f7228613d612e7461726765747c7c612e737263456c656d656e743b612626224122213d612e6e6f64654e616d653b29613d612e706172656e74456c656d656e743b61262628612e687265667c7c2222292e6d61746368282f5c2f7365617263682e2a5b3f265d74626d3d697363682f29262628612e687265662b3d22266269773d222b646f63756d656e742e646f63756d656e74456c656d656e742e636c69656e7457696474682c612e687265662b3d22266269683d222b646f63756d656e742e646f63756d656e74456c656d656e742e636c69656e74486569676874297d646f63756d656e742e6164644576656e744c697374656e65722822636c69636b222c622c2131293b646f63756d656e742e6164644576656e744c697374656e65722822746f7563685374617274222c622c2131293b7d292e63616c6c2874686973293b3c2f7363726970743e3c2f6469763e3c212d2d206363746c636d2035206363746c636d202d2d3e3c746578746172656120636c6173733d2263736922206e616d653d2263736922207374796c653d22646973706c61793a6e6f6e65223e3c2f74657874617265613e3c736372697074206e6f6e63653d224f77774e4a497141533843544d31352f4351595531513d3d223e2866756e6374696f6e28297b76617220653d27704d524f586250624472505339414f426d4b50594251273b76617220736e3d27776562273b2866756e6374696f6e28297b76617220713d66756e6374696f6e2861297b76617220633d77696e646f772c643d646f63756d656e743b69662821617c7c226e6f6e65223d3d612e7374796c652e646973706c61792972657475726e20303b696628642e64656661756c74566965772626642e64656661756c74566965772e676574436f6d70757465645374796c65297b76617220623d642e64656661756c74566965772e676574436f6d70757465645374796c652861293b696628622626282268696464656e223d3d622e7669736962696c6974797c7c22307078223d3d622e686569676874262622307078223d3d622e7769647468292972657475726e20307d69662821612e676574426f756e64696e67436c69656e74526563742972657475726e20313b76617220663d612e676574426f756e64696e67436c69656e745265637428293b613d662e6c6566742b632e70616765584f66667365743b623d662e746f702b632e70616765594f66667365743b766172206b3d662e77696474683b663d662e6865696768743b72657475726e20303e3d662626303e3d6b3f303a303e622b663f323a623e3d28632e696e6e65724865696768747c7c642e646f63756d656e74456c656d656e742e636c69656e74486569676874293f333a303e612b6b7c7c613e3d28632e696e6e657257696474687c7c642e646f63756d656e74456c656d656e742e636c69656e745769647468293f343a317d3b76617220723d652c763d736e3b66756e6374696f6e207728612c632c64297b613d222f67656e5f3230343f617479703d63736926733d222b28767c7c2277656222292b2226743d222b612b2822266c6974653d312665693d222b722b2226636f6e6e3d222b2877696e646f772e6e6176696761746f72262677696e646f772e6e6176696761746f722e636f6e6e656374696f6e3f77696e646f772e6e6176696761746f722e636f6e6e656374696f6e2e747970653a2d31292b63293b633d222672743d223b666f7228766172206220696e206429612b3d632b622b222e222b645b625d2c633d222c223b72657475726e20617d66756e6374696f6e20782861297b613d7b7072743a617d3b77696e646f772e77737274262628612e777372743d77696e646f772e77737274293b72657475726e20617d66756e6374696f6e20792861297b77696e646f772e70696e673f77696e646f772e70696e672861293a286e657720496d616765292e7372633d617d0a2866756e6374696f6e28297b666f722876617220613d2b6e657720446174652d77696e646f772e73746172742c633d782861292c643d302c623d302c663d302c6b3d646f63756d656e742e676574456c656d656e747342795461674e616d652822696d6722292c6d3d2226696d6e3d222b6b2e6c656e6774682b22266269773d222b77696e646f772e696e6e657257696474682b22266269683d222b77696e646f772e696e6e65724865696768742c413d66756e6374696f6e28682c7a297b682e6f6e6c6f61643d66756e6374696f6e28297b623d2b6e657720446174652d77696e646f772e73746172743b7a26262b2b6e3d3d66262628643d622c742829293b682e6f6e6c6f61643d6e756c6c7d7d2c743d66756e6374696f6e28297b6d2b3d2226696d613d222b663b632e6166743d643b7928772822616674222c6d2c6329297d2c6e3d302c423d302c673d766f696420303b673d6b5b422b2b5d3b297b766172206c3d313d3d712867293b6c26262b2b663b76617220753d672e636f6d706c657465262621672e6765744174747269627574652822646174612d646566657272656422292c703d7526264e756d62657228672e6765744174747269627574652822646174612d696d6c2229297c7c303b752626703f286c26262b2b6e2c70262628673d702d77696e646f772e73746172742c6c262628643d4d6174682e6d617828642c6729292c623d4d6174682e6d617828622c672929293a4128672c6c297d647c7c28643d61293b627c7c28623d64293b6e3d3d6626267428293b77696e646f772e6164644576656e744c697374656e657228226c6f6164222c66756e6374696f6e28297b77696e646f772e73657454696d656f75742866756e6374696f6e28297b632e6f6c3d2b6e657720446174652d77696e646f772e73746172743b632e696d6c3d623b76617220683d77696e646f772e706572666f726d616e6365262677696e646f772e706572666f726d616e63652e74696d696e673b68262628632e727173743d682e726573706f6e7365456e642d682e7265717565737453746172742c632e727370743d682e726573706f6e7365456e642d682e726573706f6e73655374617274293b7928772822616c6c222c6d2c6329297d2c30297d2c2131297d2928293b7d292e63616c6c2874686973293b7d2928293b3c2f7363726970743e3c736372697074206e6f6e63653d224f77774e4a497141533843544d31352f4351595531513d3d223e66756e6374696f6e205f736574496d6167657353726328652c63297b66756e6374696f6e20662861297b612e6f6e6572726f723d66756e6374696f6e28297b612e7374796c652e646973706c61793d226e6f6e65227d3b612e7372633d637d666f722876617220673d302c623d766f696420303b623d655b672b2b5d3b297b76617220643d646f63756d656e742e676574456c656d656e74427949642862293b643f662864293a2877696e646f772e676f6f676c653d77696e646f772e676f6f676c657c7c7b7d2c676f6f676c652e6969723d676f6f676c652e6969727c7c7b7d2c676f6f676c652e6969725b625d3d63297d7d3b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f6a7065673b6261736536342c2f396a2f34414151536b5a4a5267414241514141415141424141442f3277434541416b474277674842676b494277674b43676b4c445259504451774d445273554652415749423069496941644878386b4b4451734a4359784a7838664c5430744d5455334f6a6f364979732f52443834517a51354f6a634243676f4b4451774e47673850476a636c487955334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e2f2f4141424549414367414b414d4249674143455145444551482f7841415a4141414441514542414141414141414141414141414141414267634243414c2f784141734541414241774d434241554541774141414141414141414241674d454141555242694548456a4642453147426b61456959634852464256782f3851414751454141674d424141414141414141414141414141414141514944424155412f385141485245414177454141674d424141414141414141414141414141454341784a4242424d7842662f61414177444151414345514d5241443841754e655843416e4a3644725735706631746450362b794f6870584b2b2b5044622b3265703971664f48644b56325236364c4b4862364632343858744c57366139456b6d64346a4b696c5254487944397763394b66597237556d4d3149595746744f6f43304c4232556b6a4949394b3566313559333151303374686e4d6474774d504c386c455a542b765565645666675271495858536f746a7a67564a7468434d5a33384d377039747836553232667274794a342b767479566c506f7242306f71496e4d5053705a7275354766656a486279576f333044486452362f67565272784a6369323139356c70546a6752394345444a4a376256504e4c574b5a4c76726239776a504962625558564b6451527a4b37664e58764334787931726f797630656438635a58305a4470526d586f682b7879414f61537965645248527737672b687837564265476c346530667841615a6d6b7474716456436d4950626647663841516f44357271554441785541343161496e6a564b62725a59456951334f547a752f7741644255554f4a786b37644d6a487a564f716455365a705a776f68536a6f45644b4b576548647a6e584c53634a64316a5078357a4b664265513867704a4b647562667a4739464b4f4d7441414859555555414734464742525252434742355555555678782f2f32515c7833645c783364273b76617220693d5b2764696d675f31275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414945414141424943414d414141446d6d575965414141416f6c424d5645556949694c2f2f2f38414141415a47526b644852337537753751304e41534568494b6d654156465256675947414f4467352b666e3757317462382f507a643364314c533073784d5447506a342b656e703675727134714b69705a57566d30744c544477384e766232382f507a2f303950546a342b4f38764c783364336547686f596a484255556572416545414137676138416d657133312f496a46674d586135704570654d416b64375336506f61585952666b376f4f6a63346453474d476e2b6f564141416a445141664f6b77684c6a636b4141416256485a4d6b39507941414143636b6c455156526f6765325732334c694d41794748646b4a635135415349434568435464412b774a324c624c2b372f61536e4b67334f4f5a6e646e526431484c78575039746e384a6c42494551524145515241455152442b41347a57326a794d48476a3661397a6e4d3044306662314e30356e784b7142496b6d53706c55356f744e4d2f6d7655475a355149564c614e366e49444c6d74716d374c4d647542527761665051524345566b474634787a504452454743326944494d633855474a414c48597036656e647450497034637458334447624b656877334550615577616c777943495145456533476c5342634e747376636d3458434d762b47474853697a6f353033674b6d4441517772674a7254686537675331764d6359676958756a4c43324d63662f394247326f464b787a7a4d7544444f77554e35616f4c6f2f6f35793654374b5145324e4b536542507938785045766c395135674e78677a4b5341336d434c486a5441463751636e414b6f4275767246635a54484d66734144532b556650626730384b5345374231773137537235685439617235554e3150696e6766496b764c37387a50697157576e4d33475374594f3463517475645046704d5238384b54684665386773766261454e33566e594350346854554842644f67556b726f5a37626379564879652b6f51744f72316a30624331584456514a44362f51756c54634c6b704949616c797168612b732b6435666345724f492f4b724e45414c62416670764d354252326e785a57574c5a4c73686d67414d4d6e48367a7a4a4f39727739416344324f4b6550643345766e2b73526d35417051565930726c4436685768776c7277706542775242396552347a342f6b4f36434e634673395170534e6c355965344d6d5042547446585a6565764c65415778693236746f48646d6d4538393052547452316465676448686255493934326d7747313075787750486d6973647a613563515554544e354f355351766168507870706e4c73646a3671635478667239663361574c364c4d75474e5956416b634a3551316c67557932365254335947532f4457716a7262652b704934334966574a6e6941733577726e4c6f6748774e387273746b3754377858724a623867434949674349496743494c77442f6b4c6b327368546d377038716741414141415355564f524b35435949495c783364273b76617220693d5b2764696d675f3233275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414841414141427743414d414141447850675235414141416231424d5645584d4141442f2f2f2f4b4141444c4141442b2b2f763435655879304e44514e7a666f72713754516b4c72744c54696b5a484d4a53584b44772f34346548484141446f7036663939766235367572383866483031396676784d54676834664f49694c61626d37636433666b6d4a6a78793876575631666e6f4b445453456a55546b3759596d4c4e4768726666332f514d444475766231676d3047614141414758306c455156526f6762576234614b714b68434662636a4b4c44484e637165563758722f5a37774774484d424b70347238394f5154324341575441464732584868635757595855706e337661373571345341363249734e325349713432584861503875344374385642467a5a31564a36382f50675243774941736149732f4b6154634e6c61636d346546395538506a5a484261424d6b714e316d31755442622b4b30507265446d68645a63316f2b373762585852706864346a446a51354376385562676969783033336d387236414e7548695a4f764c452f4f65475746374a5830414d383952656e327346356c67333176573844486b365733756751387a46653367785659414b76773539487a566a37546d507451324131396e6e384d6779386a725950674e6c75654c686265367947654b45447277754d4866716a48484b636376534441626a614f78546e6d333765316f585841526f443369354b6e477654684c4665337145304b6e67766e58385039394c3442376a55696a4d57586174566358706f79317a762f452f3043756965726c61766e346436486b704c506e4e7269794e4f74304a3953486f4734726e50627a51586f4470524c592f6c34715758787736682b727442464d38756b637165525256396e48362b377255395734444c573763384f3364724c634364714c44796374776736753576477a4b4279647053616162325876416e74724d3273514967443858444d4a466c617a4c47734470333637794c702b6b39716f2f79363646544c58753261735633524d537a2b4261565969535037592f616a722f64473156754f574e734a346862614f4c5446705a63756b5834567644614375695734612b6665516856306e74364c392f393041372b7531507942707234612b6c5563464b7133674d6933494c452b68762b6a64676645476f5577456738496a454e30472b3470596b4958416d497245414f6a304d4c4a5a44747865386c64756f556f4278505a3241673136495170366b5265765544412f3661426d5272345767704e44454b3359464d5241727577486235464835547779676253326f2f55506252424744417a4b6b52554f494f4446672b445569524b464a446b667345494a585467476f65342f354457336467384b696d41646b394d2b70557a39794172446c4d416759557638746b734b507732423359397047786c6734436c593973594a506d6f5475513751496c44394f6a47314175463276636974324241656e37345168512b6367524e3732584f394459674d654163726e4157423745787478413556304a646d7271455269516d41596f48336a75452f676a436d4b3038657352474f78465a495842307237774347534e384a73664b4e6b632f41454445744e673159337631444c72436368494e43666d38477a704436693233514d63644644734561695544473746753556486f4654417141537057666f444b744652595a5261654151714266794c3673596a554a3263354f673346342f4134476d4a55742f507641476c6b74485554656b524b4a58526f734374654f5552794e6269425467325a48655051435658387a31757852642f514b566b674d42714f506561462f694a55753851382b424a78377a41674552732b656f2f444a77644b4b505565793978426941396f454b706276712f3650384464316563427161366d526e347a4c4842596d7145634877316377767871456b706d5733506f664d73774d5664587a34586d72715a47336a456f4e73537063344d314134564c65706d6271424e795352506a304474356b5a47716462626e4c6d416d674b5736755a70385a765a674158347a554e737862596c6454626745734931715752513363774d58427a4252355477683256325a714332664d717a31462b6a69544d434678685a53415673584f504f436453556a4e694b4e336f5435775271536b61637752386172596c7a416e55464c4657785436436d5a4f5235757a5931484941702b4d4967634d6b4d4a625049344a6b445542755a51534436694f304d33674749303273456d454f594c34562f6a74757a425a68324c483571517a414d314f3970684d6948726467473542335438794847674f676a37436165645a74744177375a4b44416a773866674c4856326f4f59335574313074754c3567526e653030683138333168667544696851366852616b65674b69413154334e3766504d4278447661526775715436416d674a4764654d46695063303671723336524734534347456b3165393667782b4b6c43635347493269415834395248426b506c4c612f6f486f4c7a4777767966737757493236343862792f454d3345496b4a783731314c4e784257734a6c335746694157495855477a786e6a49764249554479477653595073554f494a5042693632505961536f314a3637726b336c6b79347a304673304f6d4f4245396e77767a572f6b6c554b75726d74776f6f34416c31712b45612f7335664463464c6f427732506a4f67397871316f54373953547449634b5746374179797069644645654a4c3157706555443239664b335a37304d6b334a554b507550784d74556d58314a776e4c596b5941454e6853495a536875676b5933654c5836334c5455305a356f566335624e594d476d6b587266506679576e4746374d6f6d38537a547772567158754839615231386d6c4150512b696177574e767339752b5351672f78336757525777586b47786d414a5561553239466f356c62516f56366335546556674474724b6b4d5864355a5434467950686754716d777a5a446a55435136794a56483535354644657a566b7a76397275416d46796e48356c467035434c5a652f566d397878474a3756477565484f71577565666e62694669522f76443672367a694d7a6c45364957462b6357783257696f7165386266377833613856746a55586c7847547845786e64362f366d674e5349656c476c334f4437704c5262626271746a6b76334466783765365a7176754978327a3374357262522f54667748444f747262744e7643415541414141415355564f524b35435949495c783364273b76617220693d5b2764696d675f33275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414841414141427743414d414141447850675235414141416456424d5645584e4742332f2f2f2f4a4141444d414144373850444d44685856556c4c4d4642726e73612f37382f505556316a76794d62504953583032747276797372464141446a6b35585150554c4d42512f787a39444d41416a36362b765351304c2b2b76726e724b7a32354f5471754c6661654866585a4754737672375a6348445961576e6669597664666f48504d54446a6f364854536b765350446e6a6d70765a704856754141414461306c455156526f67653258323361714d42424177345159735642705141533543496a2b2f79636553494a415346307439727a4e6672484e5a546245535449536769414967694149676941496769414973676b6d597141514338614e446a3567616541324c4d4f48475749646e43625876413772706a6f5145504f78784f395a7a423461664b342b5448547a776769304c664a4742743852594c6f314f6a6b4b4c3768387a5a5277433370536d466169692f71474c49597957484d5a65353947547545615a57636433413175766c59366337496948683845396b5044783079343834615747414a6e545342554c7832466774646e5938776c595376685949692f46626f7668653563534c6c6c56456774517363723645736832495673496153352b58707a59526e6d6564366b6d57342b4a2f53566b454a6d695a5574684841596661653036594f48355578344261423938736230324f37564d4f386758676e4a555350484a7570766d43397058476a6635533644437771515a3072495366794d4b4b4251442b2f434b2b4759467141656a7131377756566871696e704f6656444b61526b546e794d354e413966555049495657724449766748496f6d4a72526343416e7a737a484d5a69487235462b6e75316a47376e644b502b70734e677048657a594c395174326c4b7942306c6b3133527731636276514d535a4f634f7173684379525231314e7477726a5175367473583042726464434168394b7446564935596f477134746a6a4c312b696c784f3844634c5a614b6e31685674413475513357574f4a5275467a4a636e5832564c476461644c4549563646787346616f63574f304a4f6162772f6c346f4f696b6b7470775277354833763453326e486b6c394137764c656d5864556b50746955563356386b5457354e6d7353574e46444c6262483561465062596d2f64467235745736674a356561545268326c67572f37456d6c704f646f365479334a357150744d4877366865314c48413456737731554e66444f625347664f4c4f7436544448664962453053665464754644686d6a57526e6c314c5a766952463265795273584d4c2f4c532f78636d5562654d706134792f654c56526e2b654b6645494e4449494e357559525351317054514734466e4f674856785849774c364b456876326969464a35344e7734544c70447049756f422b744c4f646f586370422f716e48756e5432466e2b32587068552f466e4b69612b5567684b4d4d666b777535366b517a737048474b625065746f564d714f5663474950507859534e6c586e575271476a314c566a665a5350306a55446a4b46743138492b2b71764e4f4e2b49335276342b2b3474345345482b755459394c454b3245354658656d4d41586a42356b7058505953656a63435a4a5536376937425365616d6d333355414e4f39435858304f534f7149536d48542f45554476316c4e77714e3371485568766f6a6b2b2f746e614a39432f72484b764375714b7271756d75424c6737634958466e304434564271597838742f6e453571394b735239642b324446776d647651706e61702f5a6a7665335563475a72654241454152424541524245415242454154354466384152776f2b4948597561487741414141415355564f524b35435949495c783364273b76617220693d5b2764696d675f35275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414555414141424643414d414141417255397362414141415946424d564555414141442f2f2f2b4b696f724f7a733553556c4c613274716e70366672362b7648783866382f507a7a382f4e61576c712b7672342b506a373239765a7763484376723639396658316b5a47524d544579676f4b41774d44415044772b31746257616d706f6749434156465255714b6971546b354e455245527061576b354f546d4d703246774141414239456c455156525968613257366136434d4243465757565241554655334f3737762b55464539716d6338704d577336764575424c4d3265324b4e354455534c53536631775171386a6d5470467959522f494f574b4d7670446e73554b4b57372b6c4c4f3653756f506958704675516451486f7079436143552b317255424667304e53736c44376a4b50686131697649496f4f6a3862774d6f2b31696b382f2f7044376d70713452594e4f6a6d456b424a464157336f3965677a3333716b67704c2f4957554d74626e613879712b6f4f55334b436b504b57476b4c6b2b39455042553742466333336f454e55383551417072554535386843485258654430764951522f366e4275584151796f4969577144346d39526246427946754c492f3936676a41304c69612b51636a556f47513978744b695451526d366736334b707277677054446a516a5353752b447633473857585777497a7639736d2f4b774b5868456a2b30734e3657304b572f3374323652477666702f792b5351443558365730497a6e3947695530706653696454656c384b4b51366662616f4737486f374548356b43727947644666472b49316f742b37574554794837636f527353696e76306c79596c4963486d4c79505770696f6d6c43495972586c476e2f766a54373447484f4b624932672b587336542f347847394474526663415555624e487170456e636b474f4c576f764e4a47366f674a447a6d672f4c67324334596f745550357a5049386b774b707a2f4b71435262502f424c55706c7139436934365a46432b584f513271592f35504b2b5569306f6d4b4c50757239334745465659547a58776430336c7745467545527259654e7a434b38464f6946456d775956425675555471676f686256514d696b5034674769555766444d6859746951646974632f7555735465446a676a616741414141415355564f524b35435949495c783364273b76617220693d5b2764696d675f37275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414841414141427743414d414141447850675235414141417146424d56455541414141795a737a2f4d77372f2f2f2f304e42466246776f70544a497a614e424f546b374c793873725973744f6564497156616f735a644258614a442f4141442f4c77436d57452f4e4b51757071616e2f61463956664e4c4d31753854574d6b2b506a372f7a4d725330744b4868346663334e79337437664377734a3565586d546b355072362b76464b51314b45676767505855794d6a4b656e7035746257332f3239556348427a2b764c622f66572f2f6548442b7070372b366555724b7973524552466a5932502f5755784663744259574668634477426c5555395759486b6636744c75414141427a456c455156526f67653359363036444d42694134627036574f64685536596955416271594e4e4e6e4b4c652f35324a5543627041637570662f7a65784d515136324f686443424345415242454152424550516675314a302f3944686c7a36654b554d6a525964484863446a4532564467516571414151515141414e67444e6a344351506a615a7955674358646c365366657453516f676678633341367a794556334f704b49414f7a725057464f2f7a466d746463484a614445485a31354e736b674a496967472b673674353478596758756e4d6b474235555173776d324e37454339616750685a4f4b6e36494259587a392b6765464a72514e393141387572584d6357494e37775531534374447751684d6f70616f4262585a442b486c6d57496d6b437672427a796938624657685644746c4d444e4d47344a62396c573141564f34422f454b74417a64647741524c447634427a727541355172674c2b4a77494c7335516d4f677a30596241796d41413131446334754762545838396a333466656962416c3032324455466c7338334f304f677863593672326241674133464166656a76594c59693232453176624f33582f67383364687a324232317a6d4f56336d6b6b5479323951787943536430594a434b337042674b486b4d48684b6b306c654c57764474726b67586a464c693565737a3942782b67366b4633322f79304778614e4e4945732f737753654d343371574a696c4f384839376d36623979537a66764a6d4152674141434347414e474a47663150746e3732435461762b62714f716a43336975444632712b687933372b74435759647051424145515241455152436b33546552454559374c6f71374d6741414141424a52553545726b4a6767675c7833645c783364273b76617220693d5b2764696d675f39275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414267414141415941674d414141436447645672414141414446424d5645564d615846436866524368665243686654307443505a4141414141335253546c4d4167464a456b47784e414141414c306c455156523441575041447867647742543342544446394155697568644336574e4b2f762f2f2f792b55676772436c53413037455756676c6d4546774141356559534578654377696741414141415355564f524b35435949495c783364273b76617220693d5b2764696d675f3131272c2764696d675f3231275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414649414141425343414d4141414477386e4f70414141415946424d5645582f2f2f3933476b6c3247556a6d3074794b4e324a39496c43494e46393548557a31376648372b5071424b46584f714c7a392b2f796556337672322b547a36652f446c61336276383678655a6152516d75565358436a59594c77342b7135686143454c6c716f615972486e62505875636d2b6a71657262343751726344697939636a4536764241414144756b6c455156525968653259323961794b6853474253564555544655464866336635632f696943536c562b4e64624a4737306b6a6841636d7a41306142442f39394e4e502f325052754e434b36616345767948764d7130752f7778597a6a744c2f34513371425746487848374b72562f73746b6777614c506b48534b3049344531667731556847686939544d6235416b5662766d496d4753666f636b556f31316b62426c38566449496c7331314558654a416d2b51524c5772754d63354b526439464d6b59516e306b5a76546634714d4d7742385a504157535568634e49304b56584953713339486b727958517356715656565a5051705a357551724a4733536a69634951794f4d576a354f78546262784a53475349384475474e61386755796c686b43436752324c662b5362467058576c52346b5832474e3758506b666d59484841574331717865484a546e5478556a354f6e794c7a475a3043396277767a7a38696965307063686b6e365a79526c79424b6833696667626750506734622f44526c794f7835463958325154497856757739454d696a71534f6d4754464d626166467a4a4758324a4b4d68584632636b714c5039715633684f616855726d5a44394551626a7048787257317348644370686e4e544a446e76717450723130394e33596a6565686c323245372b38673330564f3257377a775937326b6432774f6f6677724d754e61776774706153782f6853526e534a4a764b72795a552f5157535949317543376d53784b4f3731665a424568634b68516b7a7675686a717758506b655767537151394356534f5754597933764745777a322b486d4f6c4d486d5975644930737970364b70494a553177544574506b56536f7177456536436d53354f6c59335235676235426b564568594e536449476772755a2b424c794870424a76306a4d7062384c4c764261386a566367385a6938514871765769396e595243556269752f7141344a4547634d493756724b337271365273493739674c7a74524c6a517370475644626b53506152626b566c78524d616473385132593331596248483748726b3630534e79746f7545714376644634494c4d533431306a4f63375847337051416a2b52355a726b6a76654d686f7265364f784743346b6a62555879794f546d544c78455032337964376c64793267533679794179796e59394957796865707542742b3836525a71425a774f367572354771674236527074755755586269644873616b4b72726a6f544a736d47483437467543626d7a7a4a6a5a63465449336b50434b6a54544131415848704b4b2f5972484a33336d744f68724e2b794e6e5935466e4d3135735742687443376b344a64393469776e45326d355a48576e446577466e745237473235357661547a684433576e6959376844684b45504c7a4d4759615365394f493453334a554f63565568716738523042623677324459756462747531352f6745626d376b53396b64376b6a767176613063455a4d706a504c342b524d49652b567066566375636d2b684a4a792b7168546b436339633439793768584d54713332794e53767849413078694f4c58424e676a67532b584c412b72304249476263734243746d663641624d52646139674d436b672f526e627263464b4a6d5377426444656162474352307654554a3237626a5a796d63424c7256355436727371454a6c44623778437171756459582f7a596f74346634394f58783865656139645050776e39394e4e502f34332b41627851504c6a33385835594141414141456c46546b5375516d4343273b76617220693d5b2764696d675f3133275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414841414141427743414d41414144785067523541414141636c424d564558754d794c2f2f2f2f74494144744a513334747250754d522f74496762306a346a326f3537744b5250744667442b2b666a326e706a796533502b39765838337433754c5272764f536a765244627550433334757258765545583936756e316c354c734141443731644c3372367636794d587861562f30683444367a63727858564c796347667857457a76537a2f35774c3378596c6a796447355a6e6968724141414745456c455156526f67653261375a616a4b424347465270554e49716d51324b4d332b6e37763855315569674b5a6e706d31396b357537352f2b675331486f47694b4d70326e454f4844683036644f6a51763667414c555578333564583970384c3362324f426a7343306164724b446b6873682f7777775336626848735272514433527a395a714162375558634173624f546f4f71674c6e3030534a577848496e563158413634324e776a3041377a754e71514a475750346d5349436a4d6b4a6677765051347247424f774531684231754e744c76414231306c773039612f315271627156583866664433727944586e3461546236747069314366545a616677626e6c555873657837644c50456970795646732b7a64644563306b51324e45674334786d5977477a2f493844726252783168487a3557374267583241566a614e2b682f36355462414e74437a646e7765754644486e4454434d6c6454394869766e786841612b2b3843512f48466e4733676241592f775061647a52594a675957564d46757773674772376e58724779435234686d597a6c2b4c46526f4a79313134304c715432346530542f6d506530696f4d70317070686c346e6476674e65766448496f7a2f7947517759703148357070656f4c474b31756a6c6b42526a6371564779546f5230436b2f4e4c546f69362f774b7757477a776a654163656d436e5a6532445167756c65343545415a6c56737067784770474641544735663734416b5662366f6d326146624179377a645450694b57457931454e7337644156494844364b5a5a424d507a5a5863594b3942526f394b73675651445472366f6d38594e4e50706245326a74495947352b57706764496936494e2f4577356f7636716b507a38446a716a65384751692b4e3832683237547937776c65686176667765434c594c725148515a47774256763036485a53386445483538564c2b7a4f7371733534764a4e594a4f6f4f6546674f694663785a64686c4e574b767543356352736f45716b7048417547495a44305a34596f493272664967524233684d323355574a6f437338654d326d7876514e304642457356725a636546485062695336794f734273454e703775465535754e626d745a484674414d517957476a6c646354614e37614b5a322f62446e7743477a79457444657251754843697a68354155592b75695a3969325236576731762b54614435644a686348556937672f4f6e33736d71485265394252682b47386766336c4c6c5634766f354e4345645634685242774c6b5565315843444749362b6e5347733258732b3268634878537348797451686d2b4a786c35344252767658496f4f452b55373938487470617849634f2f5235685a74567535536a734a315a39375558633269333271696e386a34476e2f3177502b334b3574625237526573705457544c6e555775437349483264446a685639614f57617150346b6a31443261356e6c6d36343074594b52746d73636c6d4f494430594d47665664743351515364726d4c4d55754e63382f5279774d42505658696c586d4534764d686b53544c5a2f582b56376f64716261415161706e4d364b634377536f3062504871734d6a634a6e667858317472536873413347397974632b564a366a4b6b655437596161774e634447314f385067484c7542303854515079334b4a4b6362504352324144756b6e36746f7152464570567a6164366944746b61387255654a536654684375454f6f6563535932344d617832374c7748787a4279546c76686b51764b36586c49636d666177623353307253467037395241434d723145552b644d78704c6564453231412f46536a474c7a794e5a724b5939516471544e384f446a6e717a44453543485a37626745436f516f5259796f3038377a65366e2b4138784f70537334326f717067314f684241373150674d67684176326b4a597153786e62426954796b466a53715a6f6c5061566863676146616963384857394e71414c434b366f536743564b32594279524d504f5355466348756d6a6d79786446456864534e4534326d4732416849755454586d6d453666456536544d706943574a4d4533615137684e6f464f63623165516c30626e4c614c51467a64635a2f69526c4c445a52547358486c7441624367662f446e45524c704448584e696a68573841795851484278766541314e38774b394157304673447764463973795a734151627962426d577035556171506a6e7a66704b6c36336e554d36365a2b594e4675425541574c4255687a4f766d4934524b36757249415949734a6c323073314949477053725449784d645868524c716f6b4936426b7979424b7276536647626861383773504b612b3030744b33595a69336845726e4d337270556c7a737258755a726f6b5959674469556c7978526167535144582b6a54327a4234394f5a4559546a577464566d474a654d3451437a573132356e327747336c366258486546636c5a6f32793673477a425664632b77754a616c583732736a5846782b6b5467697274586576667831394235745433705879324743375a553037376a30386f314e4e5a4a6739713845472f7368354731696d6b48456d34533551524e33306230563845326f4765766d6d376b4e4d52592f5155555866426c5659454c5332784a4d61724f2b726c794145497063753151424c57465a7252345476394c77476d70495957665973334e704a4a3769376279524e4c566f7a4c446f54684b7934387153664c4362357a4638356932555a4550562f7072693255326c7a346d3156324b4e33474f537472742b587a7753686b6f4d6a4e706a7448347458482b527847753653676948547030364e436851332b492f674a2f6948506855784b5a387741414141424a52553545726b4a6767675c7833645c783364273b76617220693d5b2764696d675f3135275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414841414141427743414d414141447850675235414141413156424d5645582f2f2f2f6a6641446a6567442b2f2f33696467442b2f6672686367447267514438367450366a774436376433392f5066376c41447468674431696744326a67443335395037394f763738655437307148706a525866625144716b437a3133735432343833686677446d6b555838324c50366f4450316c532f766d5437726b68623030375478794a767477492f737549626967436636776f58346e682f76304b726c6a546a6f6744583236642f77785a37686642726e706d3369677833747670546c6e316e6c70574c7172336a767a4c54706c32546e6a457a7670337a757771586f754a6278313862696e573770716f44706c46726b63786e707359336b6479626d6d555030723176347a4a4c367533583272453331707a6e796641416c4e7748774141414e4f6b6c455156526f67653161435865627568494749555143735931494d4574365578616e45474e6a77456e644e4c6b70625872372f332f536d7846657744464f2b3972653838343731546c3144466f2b7a66624e534b346b2f576c2f6d6954706a764c76417159732f6e634259325a6e36722b45705a704f6b6b526c3551572f46555a426d366e635361496f4468304c704d75383548666953524a33677a444b484175526c314f4657776c3166694f674730616861786e71536a47347362793965372f696269616276774e4b6b525365784a4659652f7268357537526c5054567a6430484b6244693334496f57566b5563496c7a2b4d72763339383833457553385948666378377965577239636a676c695630442f6b342f6d737370574f2b653636725264446d426c4e37785834786e785948424a5555314832372b2f6d67595169442b734a713647495768593651706c33346c366269527a706533793655424b452f4c6577752b54766e7138655a6d4b5a416a79357a4d6a562b495a325a63637039752f6e34414b62677133542f64664870554662363865586f537174526a3157585672304a554a434d7955484f3371395855584b71414b443038725a61415051554f554666674e30346b42617871614f46486d3272736b614d6942655953564b644d5665582b34656239453677364258633137724558465875376c494b466c4c487352385341565254446955732f54564f2f7142795541324d503133534d3230635142722f6533393138584973396658783457454a77726a37646656436c49414d6d2f333545574a6f48706363596f35544370383238644747756a6349742f75456a35782f76707841504b78302b5631504f7265586a337a667751706f7570616b687157456d56665148614e55745a45706b797168334d523566654d7932575435763071756c532b7239394f6e75426f4d4e325852353933674c2b6c773933433978543462705a4658783643564759627666475278475261684d364b514954554f465a7568426d524a477834474b4b6f5552717252386646677041516362547a393875734e34554131754a6d57614531414c4a64544f72474c6d66422b694a594e3065597955714268476f306f6556446c6a4261416c316e513152564c6a397838665079355269397930544366326852474954414253546f73713043635439377438315a68545670677730417a6e5256484d4936665256557a735757796f7a764c6d41373551514c59555a654d414e674555496f4e654743506a4b6a41744c756d754b6166485357367a47576353414942546778454a7754586b306855305855337373614d75507a6f636542706b57306d75365251455147515a4a5a756c5253783242794670756645386d666d39524f3434727237656a6d4c6f7742636c6f374973316b46513667665971382f424f4a6c6a38506550364a4f4b75386852697a434b73627859424d337975684e557151632b586c5330364b4e564d70756b666855366131413342654667475a764b6e6b647444424130494d696432677755646676707653475a3551785641426133535a3370584b774d6476426e464e2b4467744f616c54324151686a302f334673416c63795849616d6c646941345559467247485861466770385364317348726931687832497850687a346e516f384b64796f4d6c7945597a6146453250317a4a4351386a73676544714a2f46384a336d6d623774567430596c4a64487770526d4465487470417a5870537864514671436e5668426e446236625a7948356e355a5a566e655536334738364b2b386968722f47514738685549703570516b6f574e2f536f3070616952696b5179557970304d733461492b6978543344487370444c5330736f347a6a475645425a654241526d77484c627979775142724a636c7330576d656754726541674335637a6c4f663179674c545350554a444968452f71464c55304b384952326367706d374768394448566d56736f32556d39513037576e676a337a79704855424879504641587a4f5a71496f5249555351304b4559516757516f6875423936536b624a41566f3158644f4569712f784b46555059567255474754394151717264456b76684d6f4c6e734c6e584e6a4f4c46435473435566597643414b416f75394c4a6142616468784b766e433064764a716d7863417268414c5278426b595758457053384c7859435169424f67495330344b674c6d6b6148366d415935496e69643539742b594b614f6b3852426f4e476d334b746c66456933672b526a6f68794b62365975794471424e427a55614a5157446e432f306730726f5a637a7333793636755a6449347449416c525741347775666f4c425231764b71363155526f452b5253515a57576a7a7970566a614d6b6550584b444f344b6f776f366567625a524f71417852505a737a50664d51726d7a4a61444c55574535446172674e3834503745456d6f67496c526579516e6f536c62534b617a4b4f49497a5546586e744b454b4975646774424c4e704b7933696145497a6b396e4651686b2b5a6a52462f435938316678444e77536a77377241644a6f4261456f484b3851626d68594a684237737a3048765a546c4652786169472b7046534835556573317a635777304d6442377744754c487749725654486f45344b344473354c7a4a584646475a54443343434a59676754716e4e483431772f4b7342713752493538654f65594144356432444869574c33684863456770504e2b7462524763784450564f614842712f717357424942305537314e44326f6a653138437a626b354f766f4545514e365263316d3077457363774e55436b4162707054654634644748754c38594b526770656f564f36422f6a6331683246425357426961644275657472435130677952796e4e4259526c376b4a55554c6f7450335655425756374b755a7a34413861524d4b546e5a6d2f586439504a396977376c336e625253315a4774713230484f49714d3532467649526f51754e6b3463436b3244545a554f6e7544305764794d4356697838656c4e496f4d3932757969456c63456b726e6d377162794578555a556b2b77325359576c4e746a537967327838436d324153615752533132475735325542696232714f6c684249595153344558705355596e6c2f72794b36367277476b716c644f343258474241766f72567459534f344e3563654b496f594c4d696c367661677856537652454c2f73563270625941642b6f4476302f6e5a73556735775563366d48466764494a3868415269574d3246304b61516f556237634555396851466872736f2f43696569486f6e72584d303455374851496178514e7a774b476c68676d4e414a7471716a417450424f71454d5978556a6d6e57744c3159646c755559516d6c43474e6a483072773962364a434b2b7432786f5162736865776e5a55744b6141587550626a72546d584463555a346e41593432553873534459544e33742f756b71442f58496c74646c6339466d712f334c644c594267386a57325433744b35434b4a69686d55465531716d6f4d325235374a6c626c30326174586b6c6c4e5559414136656d36556b4d362f6e4f5661487773316d6564346b764265485954316c45435237784771345765566a535261466a754d45436251677159786d70795a6b4b747155675836727a6e564c767832316f7444304638464c796a596639555a6174524f7a687557416a39554146416175726b50524e747659533958424138453865627972354257706d70464f30424c6d69663439366b4f37624f6d47367934495a4f716362354b6c6f613831596b4174587252337937757370455232687947673673694f33797a6f77514943527751353549597143397a3167716f494f5967513072706a656e454d4d7964647644537a6a755a4a7330724a2b6b5167693978414356535969626d656f70666f664939746d62715153725848756f5630504339543175564d4d516e4f476e4a64786c41494d4b5152747569627265427070447435612f4365746b2f534f314f497542512b554279706c51713276397669754157372f747763457072513337773869756441475541365732356e79694f41614d645a366b4d5431774e6b632f4b43343246777a43497847344f564f3468302f446f6768634e313572696d446f514441524c45425a366e6f486e56676668744e33644771705475696167663878717347576764384937694651684d427a6a47354b2f39504d4342712b7439382f6366384646435173756675423958387a3054436f55647538576b6e754346677a72346a7173575657516a7236745463757a32792f2b357133456a6878716b5342685a6b306254614e3476346b39656a484e303745714a684950765774702f32763676726c4e62737a4754637367776534332f334c72484d562b2b2b71565837582f612f3262375152736648373435632f534d4f6a382f747a70394f727a5a6352322f684d645737776b386e6a514c4e6c39664c4b75494b514a5076317750627072686e702b6230764230654e6d5a64546b38506233657a7462666e5a362b32394b47496e71666d306a344446387644346877636746543849767a35757a30644c6462342f6b4d73553750686c3836347939507a3971414d4f6c646936663432646e5a6c57416e3638335a326274447565766b4c2b6941762f71625578693841377947523967687648767542355432416457764d45316f2f424b6d667a356754365542564b527a4242687541666b376741494a34632f587a6a365041307268734f6c57727358307778494b6c594a4570322b2b624731344d6b4c5a54416c322f4f616b50523441523063415964377050336945523145504d6e38624d4f784d504c7343306237412b70304566446b364b71454651434d41636c476a42354e514737436c6767336735664273314e454d7642683241643932317232472f6e4f7830654868484e5143484c595338516251484936473139327747413362586a6f616451477848397a736e2b466f644c67304f666c724e477741523375414977513865547361646c537a447a6a73416972577851676d5771505273443663764c65417346426270594f52414c7a385042792b365155456c513648585543706867484f35546363645a4339476b4246414c596c58414e2b67666544397333554869444d3367504541654831743948686f4e67434b732b48415a2f4434556872543330464550783041455a4d766f314750655551417236786f756774614c316c5a584d4e57434c416c355a7958675530726f624469355042384f74687641625130595a6f3552616c624143726b7776776d7462345142747062634442384f31652b523343694f543532376c307544574173487a582f5465416f54556544713561485945323048592f34756a6a7757425051736b5a444c546e685051567443642f44515a6a52394d47304671324d7545524152336c737a6277576c346a414c63624f41536f587730474638355833704e685432444b57502f7935545073712b3443347630386c36344249646831584c3471496337346c6a67393578774641532f575579396167466f4443446d66447251575356322b4b7145596374316236514b67686b426d48364144674e4865616c304a3932585259613766653237635347694f4e61304e364347674969574a4b512b3063696645433556714c77417438496a443139685349356c32434642754a4e5249634b567072534e6b6f476d76417372616f502f5132512b6f43554161786c547a646e6544416446494731445458746751414c556a67424e74432b6931415455425346695a554533626364424c77497344456d72392f77477141376a7a4c484d394b556b632f434670393139453967466c4d745a6a614e4675376d7541736a5a6541354979324f775741655653614e4c305a46707570516a7765726b4c364e68346f3746547245586b3434414541546e2b6173593278333246343732744c616a4838676c4a6577454a47627455396d537641796a334137707251476e426d6d7668426c434b6d4f77316c305a4741524a733577633273376658765a492b5933622b516b4a343767384c39784e4d77533971525a6d3975394177596e6755706c4f634d41793363617a44512b74734155384a666f54687a6f5947767579394e75415a39445a6654566971355848773246774a39303374613072727336667a5545632f7a7038542b702f32702f312f742f38412b724a44466c344139755541414141415355564f524b35435949495c783364273b76617220693d5b2764696d675f3137275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414841414141427743414d414141447850675235414141416231424d5645575a4141442f2f2f2b574141435a45684c7a354f535341414474323976713164585772713743686f61765656586e304e432f666e372b2b2f763538764b705330754e414143694f546e66774d446275626d5a4367717153456a323675713965586e52704b53474141446b794d6969516b4b36635847684e44544a6c4a537457566d644b69716149534764485232775947433161576b305454574c4141414376306c455156526f6765325759584f6a4942434764654e714c4e56456a4d6247746b6d622f502f665747424230574454752f4e6d626d37322f59514c374f4d43757842464c42614c78574b7857437757693856697356697366307962425230696d4a73414944544a39335a59374848434e416d7150554c547a6d785a2f626f787944642f5576734f6f7a63784759386859426d486c654d755945324f326b6c5654477744454d36547765304b77446a756c5074714f756e7369466a2f45664170334b465763427068584476484d4c5758693843796c764a6b42376c6d6a7057553076374f53526c623636626f34564c4c4f7445687044546e772f4a793353316f5746624c4c53774278517675477a744f4e63316135684168496e6b734b745838644f76566f65725a622f562f584d6c794a4d2b5971625a384a64767a486b4d42456a446252324158554a30736175626143566a677a767549552f32424769695177685947434f5a487a3065374a6148776667324975563331616744754f7a4a56777a2b30384c7761454b357545776367374f6959644b5a667539726962774450433043374f3855594964684e31716b495a6933376878466d716954495759537471545233533270505462735a675743585761556943724f2f6a34415267436d524870427331476d4250514b364c5978504f414b6a4379574f536b57546e506c6a6f4f5036774e46716765393533746b7a47736533385a53717543516470452f5535366634694659425469534e4a77654547316d504a6b4d6b7267394d71617734594252524b6c494e754d4c6177474a3767436b514b52564e5257763159566746574769565a646f3177385867674e434d4662746243316a6365715871416a6834475a63557831586f56774d3264374e486f4574464e64643872674c6366514e307165694b30393848326c5455536668546f436f6a376a355556364f37783754563359657a797733785252394f6f63786a4b756f455656507339625230487872506653704552754e4b49635357526a6171535365777949536f76542b2b6e49526f615843717468636f46585553536945536370514963566f737037506e556f6f42612b4c4e39743830543242547354334d58306a46556f687a59425943706a3751383675416b626b5664524a4f6763464831426f52556970576438446c434a736b3955585076706c56657341334d5a695478747939535772324749552f4a666a794a754a4d49665053684f453734476d4a78324b78574377576938566973566773466f7646597630332b67492b6453615a504562466d6741414141424a52553545726b4a6767675c7833645c783364273b76617220693d5b2764696d675f3139275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220653d27704d524f586250624472505339414f426d4b50594251273b2866756e6374696f6e28297b76617220613d652c623d77696e646f772e706572666f726d616e6365262677696e646f772e706572666f726d616e63652e6e617669676174696f6e3b622626323d3d622e74797065262677696e646f772e70696e6728222f67656e5f3230343f63743d6261636b627574746f6e2665693d222b61293b7d292e63616c6c2874686973293b7d2928293b2866756e6374696f6e28297b2866756e6374696f6e28297b676f6f676c652e637363743d7b7d3b676f6f676c652e637363742e70733d27414f765661773144704144365477704f5055674e344e6f72467065555c7832367573745c78336431353635353239363336323834373838273b7d2928293b7d2928293b2866756e6374696f6e28297b2866756e6374696f6e28297b676f6f676c652e637363742e72643d747275653b7d2928293b7d2928293b2866756e6374696f6e28297b77696e646f772e78703d66756e6374696f6e2861297b66756e6374696f6e206528682c662c67297b72657475726e227870222b282278223d3d663f2263223a227822292b677d666f722876617220623d2f5c62787028787c6329285c643f295c622f3b613b297b76617220633d612e636c6173734e616d652c643d632e6d617463682862293b69662864297b612e636c6173734e616d653d632e7265706c61636528622c65293b6966282263223d3d645b315d29666f7228613d612e676574456c656d656e747342795461674e616d652822696d6722292c623d303b623c612e6c656e6774683b2b2b6229696628633d615b625d2c643d632e6765744174747269627574652822646174612d6c6c222929632e7372633d642c632e72656d6f76654174747269627574652822646174612d6c6c22293b627265616b7d613d612e706172656e74456c656d656e747d7d3b7d2928293b676f6f676c652e647274792626676f6f676c652e6472747928293b3c2f7363726970743e3c2f626f64793e3c2f68746d6c3e", + "rawHeaders": [ + "Content-Type", + "text/html; charset=ISO-8859-1", + "Date", + "Sat, 10 Aug 2019 01:21:31 GMT", + "Expires", + "-1", + "Cache-Control", + "private, max-age=0", + "P3P", + "CP=\"This is not a P3P policy! See g.co/p3phelp for more info.\"", + "Server", + "gws", + "X-XSS-Protection", + "0", + "X-Frame-Options", + "SAMEORIGIN", + "Set-Cookie", + "1P_JAR=2019-08-10-01; expires=Mon, 09-Sep-2019 01:21:31 GMT; path=/; domain=.google.com", + "Set-Cookie", + "CGIC=IiFhcHBsaWNhdGlvbi9qc29uLCB0ZXh0L3BsYWluLCAqLyo; expires=Thu, 06-Feb-2020 01:21:31 GMT; path=/complete/search; domain=.google.com; HttpOnly", + "Set-Cookie", + "CGIC=IiFhcHBsaWNhdGlvbi9qc29uLCB0ZXh0L3BsYWluLCAqLyo; expires=Thu, 06-Feb-2020 01:21:31 GMT; path=/search; domain=.google.com; HttpOnly", + "Set-Cookie", + "NID=188=vTMutucOBO-Yl5bpVtVnzkN1voOukQ24RkD0wuuzeNL_BDPMEB90MqBF06HFaILh_fs-PO8JGLhIjkSb3nxl9Rzf8L7CxJtk_yJF0aEgi2znY0rMT_dQr6_5tYfVNKU9u0d2BoXOVOWHEN3ZzaD7q6yRUb44yH3vjL0kue6Ki0s; expires=Sun, 09-Feb-2020 01:21:31 GMT; path=/; domain=.google.com; HttpOnly", + "Accept-Ranges", + "none", + "Vary", + "Accept-Encoding", + "Connection", + "close" + ], + "responseIsBinary": true + } +] diff --git a/packages/opentelemetry-instrumentation-http/test/fixtures/google-https.json b/packages/opentelemetry-instrumentation-http/test/fixtures/google-https.json new file mode 100644 index 0000000000..550bb764b3 --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/test/fixtures/google-https.json @@ -0,0 +1,43 @@ +[ + { + "scope": "https://www.google.com", + "method": "GET", + "path": "/search?q=axios&oq=axios&aqs=chrome.0.69i59l2j0l3j69i60.811j0j7&sourceid=chrome&ie=UTF-8", + "body": "", + "status": 200, + "response": "3c21646f63747970652068746d6c3e3c68746d6c206c616e673d22656e2d4341223e3c686561643e3c6d65746120636861727365743d225554462d38223e3c6d65746120636f6e74656e743d222f696d616765732f6272616e64696e672f676f6f676c65672f31782f676f6f676c65675f7374616e646172645f636f6c6f725f31323864702e706e6722206974656d70726f703d22696d616765223e3c7469746c653e6178696f73202d20476f6f676c65205365617263683c2f7469746c653e3c736372697074206e6f6e63653d224f77774e4a497141533843544d31352f4351595531513d3d223e2866756e6374696f6e28297b76617220613d77696e646f772e706572666f726d616e63653b77696e646f772e73746172743d286e65772044617465292e67657454696d6528293b613a7b76617220623d77696e646f773b69662861297b76617220633d612e74696d696e673b69662863297b76617220643d632e6e617669676174696f6e53746172742c653d632e726573706f6e736553746172743b696628653e642626653c3d77696e646f772e7374617274297b77696e646f772e73746172743d653b622e777372743d652d643b627265616b20617d7d612e6e6f77262628622e777372743d4d6174682e666c6f6f7228612e6e6f77282929297d7d77696e646f772e676f6f676c653d77696e646f772e676f6f676c657c7c7b7d3b676f6f676c652e6166743d66756e6374696f6e2866297b662e7365744174747269627574652822646174612d696d6c222c2b6e65772044617465297d3b7d292e63616c6c2874686973293b2866756e6374696f6e28297b76617220633d5b5d2c653d303b77696e646f772e70696e673d66756e6374696f6e2862297b2d313d3d622e696e6465784f662822267a782229262628622b3d22267a783d222b286e65772044617465292e67657454696d652829293b76617220613d6e657720496d6167652c643d652b2b3b635b645d3d613b612e6f6e6572726f723d612e6f6e6c6f61643d612e6f6e61626f72743d66756e6374696f6e28297b64656c65746520635b645d7d3b612e7372633d627d3b7d292e63616c6c2874686973293b3c2f7363726970743e3c7374796c653e626f64797b6d617267696e3a30206175746f3b6d61782d77696474683a37333670783b70616464696e673a30203870787d617b636f6c6f723a233139363744323b746578742d6465636f726174696f6e3a6e6f6e653b7461702d686967686c696768742d636f6c6f723a7267626128302c302c302c2e31297d613a766973697465647b636f6c6f723a233442313141387d613a686f7665727b746578742d6465636f726174696f6e3a756e6465726c696e657d696d677b626f726465723a307d68746d6c7b666f6e742d66616d696c793a526f626f746f2c48656c7665746963614e6575652c417269616c2c73616e732d73657269663b666f6e742d73697a653a313470783b6c696e652d6865696768743a323070783b746578742d73697a652d61646a7573743a313030253b636f6c6f723a233343343034333b776f72642d777261703a627265616b2d776f72643b6261636b67726f756e642d636f6c6f723a236666667d2e625273576e637b6261636b67726f756e642d636f6c6f723a236666663b626f726465722d746f703a31707820736f6c696420236530653065303b6865696768743a333970783b6f766572666c6f773a68696464656e7d2e4e365257567b6865696768743a353170783b6f766572666c6f772d7363726f6c6c696e673a746f7563683b6f766572666c6f772d783a6175746f3b6f766572666c6f772d793a68696464656e7d2e5576363771627b626f782d7061636b3a6a7573746966793b666f6e742d73697a653a313270783b6c696e652d6865696768743a333770783b6a7573746966792d636f6e74656e743a73706163652d6265747765656e3b6a7573746966792d636f6e74656e743a73706163652d6265747765656e7d2e55763637716220612c2e557636377162207370616e7b636f6c6f723a233735373537353b646973706c61793a626c6f636b3b666c65783a6e6f6e653b70616464696e673a3020313670783b746578742d616c69676e3a63656e7465723b746578742d7472616e73666f726d3a7570706572636173653b7d7370616e2e4f585875707b626f726465722d626f74746f6d3a32707820736f6c696420233432383566343b636f6c6f723a233432383566343b666f6e742d7765696768743a626f6c647d612e655a743878643a766973697465647b636f6c6f723a233735373537357d2e46456c6273667b626f726465722d6c6566743a31707820736f6c6964207267626128302c302c302c2e3132297d6865616465722061727469636c657b6f766572666c6f773a76697369626c657d2e5067373062667b6865696768743a333970783b646973706c61793a626f783b646973706c61793a666c65783b646973706c61793a666c65783b77696474683a313030257d2e4830505165637b706f736974696f6e3a72656c61746976653b666c65783a317d2e7362637b646973706c61793a666c65783b77696474683a313030257d2e50673730626620696e7075747b6d617267696e3a3270782034707820327078203870783b7d2e787b77696474683a323670783b636f6c6f723a233735373537353b666f6e743a323770782f3338707820617269616c2c2073616e732d73657269663b6c696e652d6865696768743a343070783b7d237164436c77627b666c65783a302030206175746f3b77696474683a333970783b6865696768743a333970783b626f726465722d626f74746f6d3a303b70616464696e673a303b626f726465722d746f702d72696768742d7261646975733a3870783b6261636b67726f756e642d636f6c6f723a233362373865373b626f726465723a31707820736f6c696420233333363764363b6261636b67726f756e642d696d6167653a75726c28646174613a696d6167652f6769663b6261736536342c52306c474f4464684a41416a41504948414f44722f6e436b2b4d505a2f466d5639367a4b2b2f372b2f354b352b6b714c39697741414141414a41416a41454144616e693633503477796b6d624b635152584473635141454d586d6d65614c51564c43756b7a79433039416a66654b37762f4d41616a41434c68504d564167776a73556345695a61387867415972567176324b783269777349414141426b6e664242414b7254453449634d796f743875723864617471496251664a646e41666f3257453642563035775849694a69676b414f773d3d293b7d2e73637b666f6e742d73697a653a3b706f736974696f6e3a6162736f6c7574653b746f703a333970783b6c6566743a303b72696768743a303b626f782d736861646f773a3070782032707820357078207267626128302c302c302c302e32293b7a2d696e6465783a323b6261636b67726f756e642d636f6c6f723a236666667d2e73633e6469767b70616464696e673a3130707820313070783b70616464696e672d6c6566743a313670783b70616464696e672d6c6566743a313470783b626f726465722d746f703a31707820736f6c696420234446453145357d2e7363737b6261636b67726f756e642d636f6c6f723a236635663566353b7d2e6e6f484978637b646973706c61793a626c6f636b3b666f6e742d73697a653a313670783b70616464696e673a3020302030203870783b666c65783a313b6865696768743a333570783b6f75746c696e653a6e6f6e653b626f726465723a6e6f6e653b77696474683a313030253b2d7765626b69742d7461702d686967686c696768742d636f6c6f723a7267626128302c302c302c30293b6f766572666c6f773a68696464656e3b7d2e73626320696e7075745b747970653d746578745d7b6261636b67726f756e643a6e6f6e657d2e736d6c202e634f6c3449647b646973706c61793a6e6f6e657d2e6c7b646973706c61793a6e6f6e657d2e736d6c206865616465727b6261636b67726f756e643a6e6f6e657d2e736d6c202e6c7b646973706c61793a626c6f636b3b70616464696e673a30203870787d2e736d6c202e6c7b6c65747465722d73706163696e673a2d3170783b746578742d616c69676e3a63656e7465723b626f726465722d7261646975733a3270782030203020303b666f6e743a323270782f33367078204675747572612c20417269616c2c2073616e732d73657269663b666f6e742d736d6f6f7468696e673a616e7469616c69617365647d2e627a316c42627b6261636b67726f756e643a236666663b626f726465722d7261646975733a38707820387078203020303b626f782d736861646f773a30203170782036707820726762612833322c2033332c2033362c20302e3138293b6d617267696e2d746f703a313070787d2e4b50374c43627b626f726465722d7261646975733a30203020387078203870783b626f782d736861646f773a30203270782033707820726762612833322c2033332c2033362c20302e3138293b6d617267696e2d626f74746f6d3a313070783b6f766572666c6f773a68696464656e7d2e634f6c3449647b6c65747465722d73706163696e673a2d3170783b746578742d616c69676e3a63656e7465723b666f6e743a32327074204675747572612c20417269616c2c2073616e732d73657269663b70616464696e673a3130707820302035707820303b6865696768743a333770783b666f6e742d736d6f6f7468696e673a616e7469616c69617365647d2e634f6c344964207370616e7b646973706c61793a696e6c696e652d626c6f636b7d2e533539316a7b6865696768743a313030257d2e5636677756647b636f6c6f723a233432383546347d2e69576b7576647b636f6c6f723a234541343333357d2e63447251377b636f6c6f723a236662636330357d2e6e746c52397b636f6c6f723a233334413835337d2e744a334d79637b2d7765626b69742d7472616e73666f726d3a726f74617465282d3230646567293b706f736974696f6e3a72656c61746976653b6c6566743a2d3170783b646973706c61793a696e6c696e652d626c6f636b7d666f6f7465727b746578742d616c69676e3a63656e7465723b6d617267696e2d746f703a313870787d666f6f74657220612c666f6f74657220613a766973697465642c2e736d695562627b636f6c6f723a233566363336387d2e6b73545534637b6d617267696e3a3020313370787d236d436c6a6f627b6d617267696e2d746f703a333670787d236d436c6a6f623e6469767b6d617267696e3a323070787d3c2f7374796c653e3c2f686561643e3c626f6479206a736d6f64656c3d2220223e3c6865616465722069643d22686472223e3c736372697074206e6f6e63653d224f77774e4a497141533843544d31352f4351595531513d3d223e2866756e6374696f6e28297b76617220633d3530303b2866756e6374696f6e28297b77696e646f772e73637265656e262677696e646f772e73637265656e2e77696474683c3d63262677696e646f772e73637265656e2e6865696768743c3d632626646f63756d656e742e676574456c656d656e7442794964282268647222292e636c6173734c6973742e6164642822736d6c22293b7d292e63616c6c2874686973293b7d2928293b3c2f7363726970743e3c64697620636c6173733d22634f6c344964223e3c6120687265663d222f3f73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673514f776743223e3c7370616e20636c6173733d22563667775664223e473c2f7370616e3e3c7370616e20636c6173733d2269576b757664223e6f3c2f7370616e3e3c7370616e20636c6173733d226344725137223e6f3c2f7370616e3e3c7370616e20636c6173733d22563667775664223e673c2f7370616e3e3c7370616e20636c6173733d226e746c5239223e6c3c2f7370616e3e3c7370616e20636c6173733d2269576b75766420744a334d7963223e653c2f7370616e3e3c2f613e3c2f6469763e3c64697620636c6173733d22627a316c4262223e3c666f726d20636c6173733d22506737306266222069643d227366223e3c6120636c6173733d226c2220687265663d222f3f6f75747075743d73656172636826616d703b69653d5554462d3826616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735150416745223e3c7370616e20636c6173733d22563667775664223e473c2f7370616e3e3c7370616e20636c6173733d2269576b757664223e6f3c2f7370616e3e3c7370616e20636c6173733d226344725137223e6f3c2f7370616e3e3c7370616e20636c6173733d22563667775664223e673c2f7370616e3e3c7370616e20636c6173733d226e746c5239223e6c3c2f7370616e3e3c7370616e20636c6173733d2269576b75766420744a334d7963223e653c2f7370616e3e3c2f613e3c696e707574206e616d653d226965222076616c75653d2249534f2d383835392d312220747970653d2268696464656e223e3c64697620636c6173733d22483050516563223e3c64697620636c6173733d227362632065736263223e3c696e70757420636c6173733d226e6f48497863222076616c75653d226178696f7322206175746f6361706974616c697a653d226e6f6e6522206175746f636f6d706c6574653d226f666622206e616d653d227122207370656c6c636865636b3d2266616c73652220747970653d2274657874223e3c696e707574206e616d653d226f712220747970653d2268696464656e223e3c696e707574206e616d653d226171732220747970653d2268696464656e223e3c64697620636c6173733d2278223ed73c2f6469763e3c64697620636c6173733d227363223e3c2f6469763e3c2f6469763e3c2f6469763e3c627574746f6e2069643d227164436c77622220747970653d227375626d6974223e3c2f627574746f6e3e3c2f666f726d3e3c2f6469763e3c6e6f7363726970743e3c6d65746120636f6e74656e743d22303b75726c3d2f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b6762763d3126616d703b7365693d704d524f586250624472505339414f426d4b505942512220687474702d65717569763d2272656672657368223e3c7374796c653e7461626c652c6469762c7370616e2c707b646973706c61793a6e6f6e657d3c2f7374796c653e3c646976207374796c653d22646973706c61793a626c6f636b223e506c6561736520636c69636b203c6120687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b6762763d3126616d703b7365693d704d524f586250624472505339414f426d4b50594251223e686572653c2f613e20696620796f7520617265206e6f7420726564697265637465642077697468696e206120666577207365636f6e64732e3c2f6469763e3c2f6e6f7363726970743e3c2f6865616465723e3c6469762069643d226d61696e223e3c6469763e3c64697620636c6173733d224b50374c4362223e203c64697620636c6173733d22625273576e63223e203c64697620636c6173733d224e36525756223e203c64697620636c6173733d2250673730626620557636377162223e203c7370616e20636c6173733d224f58587570223e416c6c3c2f7370616e3e3c6120636c6173733d22655a743878642220687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e6d7326616d703b74626d3d6e777326616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673515f41554943436742223e4e6577733c2f613e3c6120636c6173733d22655a743878642220687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e6d7326616d703b74626d3d76696426616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673515f41554943536743223e566964656f733c2f613e3c6120636c6173733d22655a743878642220687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e6d7326616d703b74626d3d6973636826616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673515f41554943696744223e496d616765733c2f613e2020203c6120687265663d22687474703a2f2f6d6170732e676f6f676c652e636f6d2f6d6170733f713d6178696f7326616d703b756d3d3126616d703b69653d5554462d3826616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673515f41554943796745223e4d6170733c2f613e20203c6120687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e6d7326616d703b74626d3d73686f7026616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673515f41554944436746223e53686f7070696e673c2f613e20203c6120687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e6d7326616d703b74626d3d626b7326616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673515f41554944536747223e426f6f6b733c2f613e20202020203c64697620636c6173733d2246456c627366223e3c6120687265663d222f616476616e6365645f73656172636822207374796c653d2277686974652d73706163653a6e6f77726170222069643d2273742d746f67676c652220726f6c653d22627574746f6e223e53656172636820746f6f6c733c2f613e3c2f6469763e203c2f6469763e203c2f6469763e203c2f6469763e203c2f6469763e3c64697620636c6173733d22506737306266207745736a6264205a494e62626320787064204f396735636320755550476922207374796c653d22646973706c61793a6e6f6e65222069643d2273742d63617264223e3c7374796c653e2e7745736a62647b6261636b67726f756e642d636f6c6f723a236666663b6865696768743a343470783b77686974652d73706163653a6e6f777261707d2e636f505538637b6865696768743a363070783b6f766572666c6f772d7363726f6c6c696e673a746f7563683b6f766572666c6f772d783a6175746f3b6f766572666c6f772d793a68696464656e7d2e586a326175657b6865696768743a343470783b6f766572666c6f773a68696464656e7d2e526e4e477a657b6d617267696e3a3131707820313670787d2e7745736a6264206469762c2e7745736a626420612c2e7745736a6264206c697b6f75746c696e652d77696474683a303b6f75746c696e653a6e6f6e657d3c2f7374796c653e3c64697620636c6173733d22586a32617565223e3c64697620636c6173733d22636f50553863223e3c64697620636c6173733d22526e4e477a65223e3c7374796c653e2e5041394a357b646973706c61793a696e6c696e652d626c6f636b7d2e5258614f66647b646973706c61793a696e6c696e652d626c6f636b3b6865696768743a323270783b706f736974696f6e3a72656c61746976653b70616464696e672d746f703a303b70616464696e672d626f74746f6d3a303b70616464696e672d72696768743a313670783b70616464696e672d6c6566743a303b6c696e652d6865696768743a323270783b637572736f723a706f696e7465723b746578742d7472616e73666f726d3a7570706572636173653b666f6e742d73697a653a313270783b636f6c6f723a233735373537357d2e736131746f637b646973706c61793a6e6f6e653b706f736974696f6e3a6162736f6c7574653b6261636b67726f756e643a236666663b626f726465723a31707820736f6c696420236436643664363b626f782d736861646f773a302032707820347078207267626128302c302c302c302e33293b6d617267696e3a303b77686974652d73706163653a6e6f777261703b7a2d696e6465783a3130333b6c696e652d6865696768743a313770783b70616464696e672d746f703a3570783b70616464696e672d626f74746f6d3a3570783b70616464696e672d6c6566743a3070787d2e5041394a353a686f766572202e736131746f637b646973706c61793a626c6f636b7d2e6d475379386420613a6163746976652c2e5258614f66643a6163746976657b636f6c6f723a233432383566347d3c2f7374796c653e3c64697620636c6173733d225041394a35223e3c64697620636c6173733d225258614f66642220726f6c653d22627574746f6e2220746162696e6465783d2230223e3c7374796c653e2e54574d4f55637b646973706c61793a696e6c696e652d626c6f636b3b70616464696e672d72696768743a313470783b77686974652d73706163653a6e6f777261707d2e7651597547667b666f6e742d7765696768743a626f6c647d2e4f6d54497a667b626f726465722d636f6c6f723a23393039303930207472616e73706172656e743b626f726465722d7374796c653a736f6c69643b626f726465722d77696474683a347078203470782030203470783b77696474683a303b6865696768743a303b6d617267696e2d6c6566743a2d313070783b746f703a3530253b6d617267696e2d746f703a2d3270783b706f736974696f6e3a6162736f6c7574657d2e5258614f66643a616374697665202e4f6d54497a667b626f726465722d636f6c6f723a23343238356634207472616e73706172656e747d3c2f7374796c653e3c64697620636c6173733d2254574d4f5563223e416e792074696d653c2f6469763e3c7370616e20636c6173733d224f6d54497a66223e3c2f7370616e3e3c2f6469763e3c756c20636c6173733d22736131746f63206f7a61744d223e3c7374796c653e2e6f7a61744d7b666f6e742d73697a653a313270783b746578742d7472616e73666f726d3a7570706572636173657d2e6f7a61744d202e794e46736c2c2e6f7a61744d206c697b6c6973742d7374796c652d747970653a6e6f6e653b6c6973742d7374796c652d706f736974696f6e3a6f7574736964653b6c6973742d7374796c652d696d6167653a6e6f6e657d2e794e46736c2e536b556a34632c2e794e46736c20617b636f6c6f723a7267626128302c302c302c302e3534293b746578742d6465636f726174696f6e3a6e6f6e653b70616464696e673a36707820343470782036707820313470783b6c696e652d6865696768743a313770783b646973706c61793a626c6f636b7d2e536b556a34637b6261636b67726f756e642d696d6167653a75726c282f2f73736c2e677374617469632e636f6d2f75692f76312f6d656e752f636865636b6d61726b322e706e67293b6261636b67726f756e642d706f736974696f6e3a72696768742063656e7465723b6261636b67726f756e642d7265706561743a6e6f2d7265706561747d2e536b556a34633a6163746976657b6261636b67726f756e642d636f6c6f723a236635663566357d3c2f7374796c653e3c6c6920636c6173733d22794e46736c20536b556a3463223e416e792074696d653c2f6c693e3c6c6920636c6173733d22794e46736c223e3c6120687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e7426616d703b7462733d7164723a6826616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351707755494477223e5061737420686f75723c2f613e3c2f6c693e3c6c6920636c6173733d22794e46736c223e3c6120687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e7426616d703b7462733d7164723a6426616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351707755494541223e5061737420323420686f7572733c2f613e3c2f6c693e3c6c6920636c6173733d22794e46736c223e3c6120687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e7426616d703b7462733d7164723a7726616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351707755494551223e50617374207765656b3c2f613e3c2f6c693e3c6c6920636c6173733d22794e46736c223e3c6120687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e7426616d703b7462733d7164723a6d26616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351707755494567223e50617374206d6f6e74683c2f613e3c2f6c693e3c6c6920636c6173733d22794e46736c223e3c6120687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e7426616d703b7462733d7164723a7926616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351707755494577223e5061737420796561723c2f613e3c2f6c693e3c2f756c3e3c2f6469763e3c64697620636c6173733d225041394a35223e3c64697620636c6173733d225258614f66642220726f6c653d22627574746f6e2220746162696e6465783d2230223e3c64697620636c6173733d2254574d4f5563223e416c6c20726573756c74733c2f6469763e3c7370616e20636c6173733d224f6d54497a66223e3c2f7370616e3e3c2f6469763e3c756c20636c6173733d22736131746f63206f7a61744d223e3c6c6920636c6173733d22794e46736c20536b556a3463223e416c6c20726573756c74733c2f6c693e3c6c6920636c6173733d22794e46736c223e3c6120687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b736f757263653d6c6e7426616d703b7462733d6c693a3126616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351707755494651223e566572626174696d3c2f613e3c2f6c693e3c2f756c3e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c736372697074206e6f6e63653d224f77774e4a497141533843544d31352f4351595531513d3d223e2866756e6374696f6e28297b76617220613d646f63756d656e742e676574456c656d656e7442794964282273742d746f67676c6522292c623d646f63756d656e742e676574456c656d656e7442794964282273742d6361726422293b612626622626612e6164644576656e744c697374656e65722822636c69636b222c66756e6374696f6e2863297b622e7374796c652e646973706c61793d622e7374796c652e646973706c61793f22223a226e6f6e65223b632e70726576656e7444656661756c7428297d2c2131293b7d292e63616c6c2874686973293b3c2f7363726970743e3c2f6469763e3c2f6469763e3c7374796c653e2e5a494e6262637b6261636b67726f756e642d636f6c6f723a236666663b6d617267696e2d626f74746f6d3a313070783b626f782d736861646f773a30203170782036707820726762612833322c2033332c2033362c20302e3238293b626f726465722d7261646975733a3870787d2e75555047697b666f6e742d73697a653a313470783b6c696e652d6865696768743a323070783b7d2e4f39673563633e2a3a66697273742d6368696c647b626f726465722d746f702d6c6566742d7261646975733a3870783b626f726465722d746f702d72696768742d7261646975733a3870787d2e4f39673563633e2a3a6c6173742d6368696c647b626f726465722d626f74746f6d2d6c6566742d7261646975733a3870783b626f726465722d626f74746f6d2d72696768742d7261646975733a3870787d2e726c37696c627b646973706c61793a626c6f636b3b636c6561723a626f74687d2e6c634a4631647b6d617267696e2d6c6566743a313670783b666c6f61743a72696768743b7d2e6b437259547b70616464696e673a31327078203136707820313270787d612e6664597371667b636f6c6f723a233442313141387d2e424e656177657b77686974652d73706163653a7072652d6c696e653b776f72642d777261703a627265616b2d776f72647d2e76766a774a627b636f6c6f723a233139363744323b666f6e742d73697a653a313670783b6c696e652d6865696768743a323070787d2e76766a774a6220613a766973697465647b636f6c6f723a233442313141387d2e76766a774a622e4872476465627b636f6c6f723a72676261283235352c3235352c3235352c31297d2e76766a774a622e48724764656220613a766973697465647b636f6c6f723a72676261283235352c3235352c3235352c2e37297d2e55506d69747b666f6e742d73697a653a313470783b6c696e652d6865696768743a323070787d2e55506d69742e4872476465627b636f6c6f723a72676261283235352c3235352c3235352c2e37297d2e55506d69742e415037576e647b636f6c6f723a7267626128302c3130322c33332c31297d2e7835346774667b6865696768743a3170783b6261636b67726f756e642d636f6c6f723a236466653165353b6d617267696e3a3020313670787d2e4170354f53647b70616464696e672d626f74746f6d3a313270787d2e7333763972647b666f6e742d73697a653a313470783b6c696e652d6865696768743a323070787d2e7333763972642e4872476465627b636f6c6f723a72676261283235352c3235352c3235352c31297d2e7333763972642e415037576e647b636f6c6f723a233230323132347d2e6d53783145657b70616464696e672d6c6566743a343870783b6d617267696e3a307d2e7639693631657b70616464696e672d626f74746f6d3a3870787d2e584c6c6f58657b636f6c6f723a233139363744323b666f6e742d73697a653a313470783b6c696e652d6865696768743a323070787d2e584c6c6f586520613a766973697465647b636f6c6f723a233442313141387d2e584c6c6f58652e4872476465627b636f6c6f723a72676261283235352c3235352c3235352c31297d2e584c6c6f58652e48724764656220613a766973697465647b636f6c6f723a72676261283235352c3235352c3235352c2e37297d2e5a54763942627b646973706c61793a626c6f636b7d2e6465497643627b666f6e742d73697a653a313670783b6c696e652d6865696768743a323070783b666f6e742d7765696768743a3430307d2e6465497643622e4872476465627b636f6c6f723a72676261283235352c3235352c3235352c31297d2e6465497643622e415037576e647b636f6c6f723a233230323132347d2e4643557030637b666f6e742d7765696768743a626f6c647d2e58374e5456657b646973706c61793a7461626c653b77696474683a313030253b70616464696e672d72696768743a313670783b626f782d73697a696e673a626f726465722d626f787d2e74486d6651657b646973706c61793a7461626c652d63656c6c3b70616464696e673a313270782030203132707820313670787d2e554874726b7b77696474683a373270787d2e4842544d36647b77696474683a333070787d2e5853377947647b646973706c61793a7461626c652d63656c6c3b746578742d616c69676e3a63656e7465723b766572746963616c2d616c69676e3a6d6964646c653b70616464696e673a3132707820302031327078203870787d2e616d335142667b646973706c61793a7461626c653b766572746963616c2d616c69676e3a746f707d2e5862355652657b636f6c6f723a233139363744327d613a76697369746564202e5862355652657b636f6c6f723a233442313141387d2e5862355652652e74723064777b636f6c6f723a72676261283235352c3235352c3235352c31297d613a76697369746564202e5862355652652e74723064777b636f6c6f723a72676261283235352c3235352c3235352c2e37297d2e74416438447b666f6e742d73697a653a313470783b6c696e652d6865696768743a323070787d2e74416438442e4872476465627b636f6c6f723a72676261283235352c3235352c3235352c2e37297d2e74416438442e415037576e647b636f6c6f723a233730373537417d2e614a79694f637b636f6c6f723a233030363632317d2e6f754a3943627b666c6f61743a6c6566743b70616464696e672d72696768743a313670787d2e526f434c6e657b636c6561723a626f74687d2e45594f736c647b646973706c61793a696e6c696e652d626c6f636b3b706f736974696f6e3a72656c61746976657d2e424669395a627b6f766572666c6f773a68696464656e3b706f736974696f6e3a72656c61746976657d2e53374a647a657b616c69676e2d6974656d733a63656e7465723b646973706c61793a666c65783b666c65782d646972656374696f6e3a636f6c756d6e3b6a7573746966792d636f6e74656e743a73706163652d61726f756e647d2e58646c7230647b6f766572666c6f772d783a6175746f3b2d7765626b69742d6f766572666c6f772d7363726f6c6c696e673a746f7563683b6d617267696e3a30202d3870783b70616464696e673a313670782030203136707820323470783b70616464696e672d746f703a3270783b6d617267696e2d746f703a2d3270783b7472616e73666f726d3a7472616e736c617465336428302c302c30297d2e6964673862657b646973706c61793a7461626c653b626f726465722d636f6c6c617073653a73657061726174653b626f726465722d73706163696e673a38707820303b6d617267696e3a30202d3870783b70616464696e672d72696768743a313670787d2e425647304e627b646973706c61793a7461626c652d63656c6c3b766572746963616c2d616c69676e3a746f703b6261636b67726f756e642d636f6c6f723a236666663b626f726465722d7261646975733a3870783b626f782d736861646f773a30203170782036707820726762612833322c2033332c2033362c20302e3238293b6f766572666c6f773a68696464656e7d2e55796b5439647b646973706c61793a626c6f636b3b666c6f61743a72696768743b70616464696e672d6c6566743a313670787d2e6e59543751627b636c6561723a626f74687d2e736b566770627b646973706c61793a7461626c653b7461626c652d6c61796f75743a66697865643b77696474683a313030257d2e5647484d58647b646973706c61793a7461626c652d63656c6c3b766572746963616c2d616c69676e3a6d6964646c653b6865696768743a353270783b746578742d616c69676e3a63656e7465727d2e4c70614472627b6d617267696e3a30206175746f203870783b646973706c61793a626c6f636b7d2e766253684f657b70616464696e672d746f703a307d2e4156736570667b70616464696e672d626f74746f6d3a3870787d2e4156736570662e753278314f647b70616464696e672d626f74746f6d3a307d2e787063202e6877632c2e787078202e6877787b646973706c61793a6e6f6e657d2e5275386964627b6d617267696e2d746f703a2d313670787d2e70756e657a7b666f6e742d7765696768743a3730303b6c65747465722d73706163696e673a302e373570783b746578742d7472616e73666f726d3a7570706572636173657d2e7779727758637b666f6e742d73697a653a313270783b6c696e652d6865696768743a313670787d2e7779727758632e4872476465627b636f6c6f723a72676261283235352c3235352c3235352c31297d2e7779727758632e415037576e647b636f6c6f723a233230323132347d2e6d4868796c667b646973706c61793a7461626c652d63656c6c3b766572746963616c2d616c69676e3a6d6964646c657d2e575a35474a667b616c69676e2d6974656d733a63656e7465723b70616464696e673a3020323070783b6d696e2d77696474683a31313270787d2e714e394b65642c2e44586b354d657b6d617267696e3a30206175746f7d2e44586b354d657b6d617267696e2d626f74746f6d3a313270787d2e51693946647b6261636b67726f756e643a236666663b626f726465723a303b626f726465722d7261646975733a39393970783b646973706c61793a626c6f636b3b6865696768743a353670783b6a7573746966792d636f6e74656e743a63656e7465723b77696474683a353670783b7a2d696e6465783a307d2e51693946647b626f782d736861646f773a30203170782036707820726762612833322c2033332c2033362c20302e3238292c696e7365742030203020302030207267626128302c302c302c302e3130292c696e73657420302030203020302072676261283235352c3235352c3235352c302e3530297d2e51693946643a666f6375737b6f75746c696e653a6e6f6e657d2e5169394664202e685748754a7b646973706c61793a626c6f636b3b6d617267696e3a30206175746f7d2e6a69356a70667b746578742d616c69676e3a63656e7465727d68727b626f726465723a303b626f726465722d626f74746f6d3a31707820736f6c696420236466653165353b6d617267696e3a303b7d2e425579624b652c2e48736e4642667b6d617267696e2d6c6566743a313670787d2e425579624b652c2e6f4d3247417b6d617267696e2d72696768743a313670787d2e584f377268637b6d617267696e3a30202d313670787d2e6949576d34627b626f782d73697a696e673a626f726465722d626f783b6d696e2d6865696768743a343870787d2e664c745873637b70616464696e673a313470783b746578742d616c69676e3a63656e7465727d2e4c796d38577b77696474683a313470783b6865696768743a323070783b706f736974696f6e3a72656c61746976653b6d617267696e3a30206175746f7d2e4164665872627b6d617267696e2d6c6566743a2d313470783b766572746963616c2d616c69676e3a6d6964646c653b646973706c61793a696e6c696e652d626c6f636b7d2e4c796d3857206469767b706f736974696f6e3a6162736f6c7574653b626f726465722d6c6566743a37707820736f6c6964207472616e73706172656e743b626f726465722d72696768743a37707820736f6c6964207472616e73706172656e743b77696474683a303b6865696768743a303b6c6566743a307d2e4979596145647b746f703a3770783b626f726465722d746f703a37707820736f6c696420233735373537357d2e4543554851657b746f703a3470783b626f726465722d746f703a37707820736f6c696420236666667d2e4165515175627b626f74746f6d3a3770783b626f726465722d626f74746f6d3a37707820736f6c696420233735373537357d2e5943553765627b626f74746f6d3a3470783b626f726465722d626f74746f6d3a37707820736f6c696420236666667d2e4963783643647b6d617267696e3a30206175746f203870787d2e6d41646a51637b746578742d616c69676e3a72696768747d2e75456563337b666f6e742d73697a653a313270783b6c696e652d6865696768743a313670787d2e75456563332e4872476465627b636f6c6f723a72676261283235352c3235352c3235352c2e37297d2e75456563332e415037576e647b636f6c6f723a233730373537417d2e724c736879662c2e426d503574667b70616464696e672d746f703a313270783b70616464696e672d626f74746f6d3a313270787d2e773143334c652c2e426d503574662c2e47354e6242647b70616464696e672d6c6566743a313670783b70616464696e672d72696768743a313670783b7d2e47354e6242647b70616464696e672d626f74746f6d3a313270787d2e6e4d796d65667b646973706c61793a666c65787d2e473565466c667b666c65783a313b646973706c61793a626c6f636b7d2e6e4d796d6566207370616e7b746578742d616c69676e3a63656e7465727d3c2f7374796c653e3c6469763e3c212d2d53575f435f582d2d3e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c64697620636c6173733d226b43725954223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f26616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351466a4141656751494278414226616d703b7573673d414f76566177327466496430584346385649396a682d324f35555069223e3c64697620636c6173733d22424e656177652076766a774a6220415037576e64223e4178696f733c2f6469763e3c64697620636c6173733d22424e656177652055506d697420415037576e64223e68747470733a2f2f7777772e6178696f732e636f6d3c2f6469763e3c2f613e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d226b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c6469763e3c6469763e3c64697620636c6173733d224170354f5364223e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e536d6172742c20656666696369656e74206e65777320776f72746879206f6620796f75722074696d652c20617474656e74696f6e2c20616e642074727573742e3c2f6469763e3c2f6469763e3c64697620636c6173733d22763969363165223e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c7370616e20636c6173733d22424e65617765223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f706f6c697469637326616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673516a42417741586f4543416351417726616d703b7573673d414f765661773139685044464b57637a653030627974527851476576223e3c7370616e20636c6173733d22584c6c6f586520415037576e64223e506f6c69746963733c2f7370616e3e3c2f613e3c2f7370616e3e3c2f6469763e3c2f6469763e3c64697620636c6173733d22763969363165223e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c7370616e20636c6173733d22424e65617765223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f617574686f72732f6e6577736465736b26616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673516a424177416e6f4543416351425126616d703b7573673d414f7656617730315a6b4f30626d50386e4370657152386257474959223e3c7370616e20636c6173733d22584c6c6f586520415037576e64223e53746f72696573206279204178696f733c2f7370616e3e3c2f613e3c2f7370616e3e3c2f6469763e3c2f6469763e3c64697620636c6173733d22763969363165223e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c7370616e20636c6173733d22424e65617765223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f6178696f732d64617368626f6172642d313535303631393334332d64653734306436612d656163312d343539392d383262642d3434303533343537376639322e68746d6c26616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673516a42417741336f4543416351427726616d703b7573673d414f76566177305658706145566f434f444153557657784f61466652223e3c7370616e20636c6173733d22584c6c6f586520415037576e64223e4178696f732044617368626f6172643c2f7370616e3e3c2f613e3c2f7370616e3e3c2f6469763e3c2f6469763e3c64697620636c6173733d22763969363165223e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c7370616e20636c6173733d22424e65617765223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f6e6577736c65747465727326616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673516a42417742486f4543416351435126616d703b7573673d414f76566177316255574b54564d7a4944436d736a34536d614c4755223e3c7370616e20636c6173733d22584c6c6f586520415037576e64223e4e6577736c6574746572733c2f7370616e3e3c2f613e3c2f7370616e3e3c2f6469763e3c2f6469763e3c64697620636c6173733d22763969363165223e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c7370616e20636c6173733d22424e65617765223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f776f726c6426616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673516a42417742586f4543416351437726616d703b7573673d414f765661773246776f732d5f6e416d39666e4963486a754b325a53223e3c7370616e20636c6173733d22584c6c6f586520415037576e64223e576f726c643c2f7370616e3e3c2f613e3c2f7370616e3e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c7370616e20636c6173733d22424e65617765223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f627573696e65737326616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673516a424177426e6f4543416351445126616d703b7573673d414f76566177324366426a4a724a72716e5052623241754d614e4953223e3c7370616e20636c6173733d22584c6c6f586520415037576e64223e427573696e6573733c2f7370616e3e3c2f613e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c64697620636c6173733d226b43725954223e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e3c7370616e20636c6173733d224643557030632072514d516f64223e546f702073746f726965733c2f7370616e3e3c2f6469763e3c2f7370616e3e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f6e6577736c6574746572732f6178696f732d616d2d61653065626362352d616432352d346531632d623662642d3138633530393464656330312e68746d6c26616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351714f63424d4164364241674145414926616d703b7573673d414f765661773148445a7242345437756b327137384256725676426f223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e3c7370616e20636c6173733d2272514d516f6420586235565265223e4178696f7320414d202d2041756775737420392c20323031393c2f7370616e3e3c2f6469763e3c2f7370616e3e3c7370616e3e3c64697620636c6173733d22424e6561776520744164384420415037576e64223e3c7370616e20636c6173733d2272514d516f6420614a79694f63223e4178696f733c2f7370616e3e20b72031206461792061676f3c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f636f756e74726965732d6d6f73742d7269736b2d77617465722d6372697369732d66343066343866392d623032652d346536622d393965642d3136346335636239353333352e68746d6c26616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351714f63424d4168364241674145415526616d703b7573673d414f7656617733505243566f5a5532675a494b44336b62615072544e223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e3c7370616e20636c6173733d2272514d516f6420586235565265223e54686520636f756e7472696573206d6f7374206174207269736b206f662061207761746572206372697369733c2f7370616e3e3c2f6469763e3c2f7370616e3e3c7370616e3e3c64697620636c6173733d22424e6561776520744164384420415037576e64223e3c7370616e20636c6173733d2272514d516f6420614a79694f63223e4178696f733c2f7370616e3e20b72031206461792061676f3c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c6469763e3c6120687265663d222f75726c3f713d68747470733a2f2f747769747465722e636f6d2f6178696f732533467265665f73726325334474777372632532353545676f6f676c652532353743747763616d702532353545736572702532353743747767722532353545617574686f7226616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673513646347743586f4543415951416726616d703b7573673d414f7656617733626475774e6f5934356137397a30464775566e566d223e3c64697620636c6173733d226b43725954223e3c64697620636c6173733d226f754a3943622053374a647a6522207374796c653d2277696474683a343070783b6865696768743a34307078223e3c696d6720636c6173733d2245594f736c642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a343070783b6d61782d6865696768743a34307078222069643d2264696d675f312220646174612d64656665727265643d2231223e3c2f6469763e3c6469763e3c64697620636c6173733d22424e656177652076766a774a6220415037576e64223e4178696f7320262331303030333b3c2f6469763e3c64697620636c6173733d22424e656177652055506d697420415037576e64223e54776974746572202623383235303b206178696f733c2f6469763e3c2f6469763e3c64697620636c6173733d22526f434c6e65223e3c2f6469763e3c2f6469763e3c2f613e3c2f6469763e3c6469763e3c6469763e3c64697620636c6173733d2258646c723064223e3c64697620636c6173733d22696467386265223e3c6120636c6173733d22425647304e622220687265663d222f75726c3f713d68747470733a2f2f747769747465722e636f6d2f6178696f732f7374617475732f313135393838373636383733393733393634382533467265665f73726325334474777372632532353545676f6f676c652532353743747763616d702532353545736572702532353743747767722532353545747765657426616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351676c5177436e6f4543415951424126616d703b7573673d414f7656617733765932387a4a6374626a64305a365357505a344c63223e3c6469763e3c646976207374796c653d2277696474683a3233327078223e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e5477697474657220686173207265696e737461746564204d69746368204d63436f6e6e656c6c27732072652d656c656374696f6e2063616d706169676e206163636f756e74206166746572207468652070726f66696c65207761732073757370656e646564206561726c6965722074686973207765656b2e207777772e6178696f732e636f6d2f6d697463682d6d63636f6e2623383233303b3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d22424e6561776520744164384420415037576e64223e313920686f7572732061676f3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f75726c3f713d68747470733a2f2f747769747465722e636f6d2f6178696f732f7374617475732f313135393838333433333933313336363430322533467265665f73726325334474777372632532353545676f6f676c652532353743747763616d702532353545736572702532353743747767722532353545747765657426616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351676c517743336f4543415951426726616d703b7573673d414f76566177336c4b50674566354d5351724b75784655584d614f41223e3c6469763e3c646976207374796c653d2277696474683a3233327078223e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e4e657720746563686e6f6c6f677920697320726573686170696e67206173736574206d616e6167656d656e7420666f722061206e65772067656e65726174696f6e206f6620776f726b6572732c20616e64206d6f7374206f662074686520696e64757374727920686173206e6f74206b65707420706163652e207777772e6178696f732e636f6d2f61737365742d6d616e61672623383233303b3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d22424e6561776520744164384420415037576e64223e323020686f7572732061676f3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f75726c3f713d68747470733a2f2f747769747465722e636f6d2f6178696f732f7374617475732f313135393837373735323139323939353333332533467265665f73726325334474777372632532353545676f6f676c652532353743747763616d702532353545736572702532353743747767722532353545747765657426616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351676c517744486f4543415951434126616d703b7573673d414f7656617732554b4158715044627577556a736d37626342675874223e3c6469763e3c646976207374796c653d2277696474683a3233327078223e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e5374657068656e20526f73732022667265616b6564206f75742220617420746865206261636b6c61736820746f20686973205472756d702066756e64726169736572206173205472756d70206173736f636961746573207065727375616465642068696d20746f20676f206168656164207769746820746865206576656e742061742068697320536f757468616d70746f6e206d616e73696f6e2e207777772e6178696f732e636f6d2f7374657068656e2d726f732623383233303b3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d22424e6561776520744164384420415037576e64223e323020686f7572732061676f3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f75726c3f713d68747470733a2f2f747769747465722e636f6d2f6178696f732f7374617475732f313135393837323436373432383530373634392533467265665f73726325334474777372632532353545676f6f676c652532353743747763616d702532353545736572702532353743747767722532353545747765657426616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351676c517744586f4543415951436726616d703b7573673d414f7656617733683854556374412d32397252656e584742497a7a5f223e3c6469763e3c646976207374796c653d2277696474683a3233327078223e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e4265796f6e6420726570616972696e6720616e6420696d70726f76696e6720726f61647320616e64207369646577616c6b732c20636974696573206861766520616e206f70706f7274756e69747920746f206275696c6420696e667261737472756374757265207468617420636f756c64206f70656e207570206e6577206d6f62696c697479206f7074696f6e7320616e6420696e637265617365206163636573736962696c6974792c207772697465732045787065727420566f6963657320636f6e7472696275746f722048656e727920436c6179706f6f6c2e207777772e6178696f732e636f6d2f616d642d6c6973612d73752623383233303b3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d22424e6561776520744164384420415037576e64223e323020686f7572732061676f3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f75726c3f713d68747470733a2f2f747769747465722e636f6d2f6178696f732f7374617475732f313135393836373138323634333430343830322533467265665f73726325334474777372632532353545676f6f676c652532353743747763616d702532353545736572702532353743747767722532353545747765657426616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351676c5177446e6f4543415951444126616d703b7573673d414f765661773056326641356f4573505f6b723267467962676b5568223e3c6469763e3c646976207374796c653d2277696474683a3233327078223e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e414d442043454f204c697361205375206469736375737365732077697468204178696f732068657220636f6d70616e79277320726573757267656e636520616e6420626174746c6573207769746820496e74656c207777772e6178696f732e636f6d2f616d642d6c6973612d73752623383233303b3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d22424e6561776520744164384420415037576e64223e323120686f7572732061676f3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c64697620636c6173733d226b43725954223e3c6120687265663d222f75726c3f713d68747470733a2f2f6769746875622e636f6d2f6178696f732f6178696f7326616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351466a4150656751494242414226616d703b7573673d414f7656617733486a474b6775516d6342745456445566344d665072223e3c64697620636c6173733d22424e656177652076766a774a6220415037576e64223e6178696f732f6178696f733a2050726f6d697365206261736564204854545020636c69656e7420666f72207468652062726f7773657220616e64202e2e2e202d204769744875623c2f6469763e3c64697620636c6173733d22424e656177652055506d697420415037576e64223e68747470733a2f2f6769746875622e636f6d202623383235303b206178696f73202623383235303b206178696f733c2f6469763e3c2f613e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d226b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c6469763e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e636f6e7374206178696f73203d207265717569726528276178696f7327293b202f2f204d616b652061207265717565737420666f72206120757365722077697468206120676976656e204944206178696f732e2067657428272f757365723f49443d31323334352729202e7468656e2866756e6374696f6e2028726573706f6e736529207b202f2f2068616e646c652073756363657373a02e2e2e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c64697620636c6173733d226b43725954223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e6178696f732e636f6d2f26616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673517757347745486f4543416b51416726616d703b7573673d414f7656617733565033564769536e46355355474c45653730387037223e3c696d6720636c6173733d2255796b5439642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a373270783b6d61782d6865696768743a37327078222069643d2264696d675f32332220646174612d64656665727265643d2231223e3c2f613e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e4178696f733c2f6469763e3c2f7370616e3e3c7370616e3e3c64697620636c6173733d22424e6561776520744164384420415037576e64223e576562736974653c2f6469763e3c2f7370616e3e3c64697620636c6173733d226e5954375162223e3c2f6469763e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d226b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e4178696f7320697320616e20416d65726963616e206e65777320616e6420696e666f726d6174696f6e207765627369746520666f756e64656420696e203230313620627920666f726d657220506f6c697469636f207374616666657273204a696d2056616e64654865692c204d696b6520416c6c656e2c20616e6420526f7920536368776172747a2e204974206f6666696369616c6c79206c61756e6368656420696e20323031372e2054686520736974652773206e616d65206973206261736564206f6e2074686520477265656b3a202623373934303b26233935383b26233935333b26233935393b26233936323b2c206d65616e696e672022776f72746879222e203c7370616e20636c6173733d22424e65617765223e3c6120687265663d222f75726c3f713d68747470733a2f2f656e2e77696b6970656469612e6f72672f77696b692f4178696f735f28776562736974652926616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673516d684d77456e6f4543416b51427726616d703b7573673d414f76566177306a76706f32364a65425a684a7638496157384e7163223e3c7370616e20636c6173733d22584c6c6f586520415037576e64223e57696b6970656469613c2f7370616e3e3c2f613e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c64697620636c6173733d22766253684f65206b43725954223e3c64697620636c6173733d22415673657066223e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c7370616e3e3c7370616e20636c6173733d22424e656177652073337639726420415037576e64223e4f776e65723c2f7370616e3e3c2f7370616e3e3a203c7370616e3e3c7370616e20636c6173733d22424e6561776520744164384420415037576e64223e4178696f73204d6564696120496e633c2f7370616e3e3c2f7370616e3e3c2f6469763e3c2f6469763e3c64697620636c6173733d2241567365706620753278314f64223e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c7370616e3e3c7370616e20636c6173733d22424e656177652073337639726420415037576e64223e4c61756e636865643c2f7370616e3e3c2f7370616e3e3a203c7370616e3e3c7370616e20636c6173733d22424e6561776520744164384420415037576e64223e323031373c2f7370616e3e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c64697620636c6173733d226b43725954223e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e3c7370616e20636c6173733d224643557030632072514d516f64223e50656f706c6520616c736f2073656172636820666f723c2f7370616e3e3c2f6469763e3c2f7370616e3e3c2f6469763e3c64697620636c6173733d22787063223e3c64697620636c6173733d22783534677466223e3c2f6469763e3c6469763e3c64697620636c6173733d226b43725954223e3c7370616e20636c6173733d2270756e657a223e3c64697620636c6173733d22424e656177652077797277586320415037576e64223e4c69626572616c206e65777320736f7572636573206c6973743c2f6469763e3c2f7370616e3e3c2f6469763e3c6469763e3c6469763e3c64697620636c6173733d2258646c723064223e3c64697620636c6173733d22696467386265223e3c6120636c6173733d22425647304e622220687265663d222f7365617263683f69653d5554462d3826616d703b713d434e4e26616d703b737469636b3d483473494141414141414141414f4f5155654c517a3955335343394f4c7a4b537a4d6c4d5369314b7a4648495379307656696a4f4c79314b54693157794d6b734c6f6c6954617a497a43382d7851685866497152433851304d6f38334b726545636a4a79697772536b36434b6b67324b633644693859565a35686e707678676c6658435a333844437549695632646e504477435833376e476b414141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735173396f424d425a3642416749454149223e3c6469763e3c646976207374796c653d2277696474683a3131327078223e3c64697620636c6173733d2253374a647a6522207374796c653d2277696474683a31313270783b6865696768743a3131327078223e3c696d6720636c6173733d2245594f736c642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a31313270783b6d61782d6865696768743a3131327078222069643d2264696d675f332220646174612d64656665727265643d2231223e3c2f6469763e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e434e4e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f7365617263683f69653d5554462d3826616d703b713d506f6c697469636f26616d703b737469636b3d483473494141414141414141414f4f5155654c537a3955334d444b504e7971334e4a4c4d7955784b4c55724d5563684c4c5339574b4d34764c55704f4c5662497953777569574a4e724d6a4d4c7a3746794146536e6c3663586e534b45556b6e6c4a4f52573153516e6752566c4778516e414d566a795f4d4d7339495f38556f3659504c5f415957786b5773484148354f5a6b6c6d636e3541484749687547584141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735173396f424d425a364241674945414d223e3c6469763e3c646976207374796c653d2277696474683a3131327078223e3c64697620636c6173733d2253374a647a6522207374796c653d2277696474683a31313270783b6865696768743a3131327078223e3c696d6720636c6173733d2245594f736c642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a31313270783b6d61782d6865696768743a3131327078222069643d2264696d675f352220646174612d64656665727265643d2231223e3c2f6469763e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e506f6c697469636f3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f7365617263683f69653d5554462d3826616d703b713d48756666506f737426616d703b737469636b3d483473494141414141414141414f4f5155654c537a395533794d67744b6b68504d704c4d7955784b4c55724d5563684c4c5339574b4d34764c55704f4c5662497953777569574a4e724d6a4d4c7a3746794146536e6c3663586e534b45617a5479447a65714e7753796f45594131575562464363417857504c3877797a306a5f78536a7067387638426862475261776348715670615148357853554175376a463070634141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735173396f424d425a3642416749454151223e3c6469763e3c646976207374796c653d2277696474683a3131327078223e3c64697620636c6173733d2253374a647a6522207374796c653d2277696474683a31313270783b6865696768743a3131327078223e3c696d6720636c6173733d2245594f736c642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a31313270783b6d61782d6865696768743a3131327078222069643d2264696d675f372220646174612d64656665727265643d2231223e3c2f6469763e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e48756666506f73743c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f7365617263683f69653d5554462d3826616d703b713d4e505226616d703b737469636b3d483473494141414141414141414f4f5155654c517a3955335344596f7a6a47537a4d6c4d5369314b7a4648495379307656696a4f4c79314b54693157794d6b734c6f6c6954617a497a43382d785168576e463663586e534b6b5176454e444b504e79713368484979636f734b30704f67696b416d5173586a4337504d4d394a5f4d55723634444b5f675956784553757a583041514142514b584c32514141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735173396f424d425a3642416749454155223e3c6469763e3c646976207374796c653d2277696474683a3131327078223e3c64697620636c6173733d2253374a647a6522207374796c653d2277696474683a31313270783b6865696768743a3131327078223e3c696d6720636c6173733d2245594f736c642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a31313270783b6d61782d6865696768743a3131327078222069643d2264696d675f392220646174612d64656665727265643d2231223e3c2f6469763e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e4e6174696f6e616c205075626c69632052612e2e2e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d226d4868796c66223e3c64697620636c6173733d22575a35474a66223e3c6120636c6173733d22714e394b65642220687265663d222f7365617263683f69653d5554462d3826616d703b713d4c69626572616c2b6e6577732b736f75726365732b6c69737426616d703b737469636b3d483473494141414141414141414f4f514d5a4c4d7955784b4c55724d5563684c4c5339574b4d34764c55704f4c5662497953777569574a4e724d6a4d4c7a3746794b476671322d515870786564497152433851304d6f38334b726545636a4a79697772536b36434b6b67324b633644693859565a35686e707678676c6658435a3338444375496756747a514146695a305070774141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673517a4f30424d425a3642416749454159223e3c627574746f6e20636c6173733d2244586b354d65205169394664223e3c696d6720636c6173733d22685748754a2220616c743d224172726f7722207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a323470783b6d61782d6865696768743a32347078222069643d2264696d675f31312220646174612d64656665727265643d2231223e3c2f627574746f6e3e3c64697620636c6173733d22424e65617765206a69356a706620744164384420415037576e64223e4d6f726520726573756c74733c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c64697620636c6173733d22687763223e3c64697620636c6173733d226b43725954223e3c7370616e20636c6173733d2270756e657a223e3c64697620636c6173733d22424e656177652077797277586320415037576e64223e4c69626572616c20706f6c69746963616c2077656273697465733c2f6469763e3c2f7370616e3e3c2f6469763e3c6469763e3c6469763e3c64697620636c6173733d2258646c723064223e3c64697620636c6173733d22696467386265223e3c6120636c6173733d22425647304e622220687265663d222f7365617263683f69653d5554462d3826616d703b713d536c6174652b4d6167617a696e6526616d703b737469636b3d483473494141414141414141414f4f5155654c557a3955334d4449724e7259776b73724a54456f74537378524b4d6a5079537a4a54416179796c4f54696a4e4c556f756a57424d724d764f4c547a4679675a5762787875565730493547626c4642656c4a70786752426b456c7a4d334b636f70796f524c47795a55705656433253564a4b5354715562576c55626c7a796931484b423666564453794d69316a35676e4d5353314956664250544536737938314942452d4b766d72674141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735173396f424d425a3642416749454167223e3c6469763e3c646976207374796c653d2277696474683a3131327078223e3c64697620636c6173733d2253374a647a6522207374796c653d2277696474683a31313270783b6865696768743a3131327078223e3c696d6720636c6173733d2245594f736c642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a31313270783b6d61782d6865696768743a3131327078222069643d2264696d675f31332220646174612d64656665727265643d2231223e3c2f6469763e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e536c617465204d6167617a696e653c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f7365617263683f69653d5554462d3826616d703b713d42757a7a4665656426616d703b737469636b3d483473494141414141414141414f4f5155654c537a3955334d44637279796e4b4e5a4c4b7955784b4c55724d55536a497a386b7379557747737370546b346f7a53314b4c6f3167544b7a4c7a69303878677455626d6363626c5674434f526d3552515870536163594f6345795a735847466c414a694b6c514365506b797051714b4e736b4b61556b486371324e436f334c766e464b4f5744302d6f4746735a467242784f7056565662716d704b5144564e44327173774141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735173396f424d425a364241674945416b223e3c6469763e3c646976207374796c653d2277696474683a3131327078223e3c64697620636c6173733d2253374a647a6522207374796c653d2277696474683a31313270783b6865696768743a3131327078223e3c696d6720636c6173733d2245594f736c642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a31313270783b6d61782d6865696768743a3131327078222069643d2264696d675f31352220646174612d64656665727265643d2231223e3c2f6469763e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e42757a7a466565643c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f7365617263683f69653d5554462d3826616d703b713d4461696c792b4b6f7326616d703b737469636b3d483473494141414141414141414f4f5155654c557a3955334d453675544b6b796b73724a54456f74537378524b4d6a5079537a4a54416179796c4f54696a4e4c556f756a57424d724d764f4c547a4679675a51626d6363626c5674434f526d3552515870536163597751595a6d5255625730416c7a4d334b636f70796f524a67473642736b3653556b6e516f32394b6f334c6a6b46364f554430367247316759463746797569526d356c5171654f635841774447505a396673774141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735173396f424d425a364241674945416f223e3c6469763e3c646976207374796c653d2277696474683a3131327078223e3c64697620636c6173733d2253374a647a6522207374796c653d2277696474683a31313270783b6865696768743a3131327078223e3c696d6720636c6173733d2245594f736c642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a31313270783b6d61782d6865696768743a3131327078222069643d2264696d675f31372220646174612d64656665727265643d2231223e3c2f6469763e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e4461696c79204b6f733c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c6120636c6173733d22425647304e622220687265663d222f7365617263683f69653d5554462d3826616d703b713d54616c6b696e672b506f696e74732b4d656d6f26616d703b737469636b3d483473494141414141414141414f4f5155654c557a3955334d456c4b4b556b336b73724a54456f74537378524b4d6a5079537a4a54416179796c4f54696a4e4c556f756a57424d724d764f4c547a4679675a51626d6363626c5674434f526d3552515870536163597751595a6d5255625730416c7a4d334b636f70796f524c47795a5570565641323244596f32394b6f334c6a6b46364f5544303672473167594637454b6879546d5a47666d705373453547666d6c5251722d4b626d35674d415637446f6d62304141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735173396f424d425a3642416749454173223e3c6469763e3c646976207374796c653d2277696474683a3131327078223e3c64697620636c6173733d2253374a647a6522207374796c653d2277696474683a31313270783b6865696768743a3131327078223e3c696d6720636c6173733d2245594f736c642220616c743d2222207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a31313270783b6d61782d6865696768743a3131327078222069643d2264696d675f31392220646174612d64656665727265643d2231223e3c2f6469763e3c64697620636c6173733d22525775676763206b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e54616c6b696e6720506f696e7473204d652e2e2e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d226d4868796c66223e3c64697620636c6173733d22575a35474a66223e3c6120636c6173733d22714e394b65642220687265663d222f7365617263683f69653d5554462d3826616d703b713d4c69626572616c2b706f6c69746963616c2b776562736974657326616d703b737469636b3d483473494141414141414141414f4f514d5a4c4b7955784b4c55724d55536a497a386b7379557747737370546b346f7a53314b4c6f3167544b7a4c7a6930387863756e6e366873596d6363626c5674434f526d3552515870536163594f6345795a735847466c414a63374f796e4b4a6371495278636d564b465a52746b7052536b67356c57787156473566385970547977576c314177766a496c5938386744456f63564e75514141414126616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673517a4f30424d425a3642416749454177223e3c627574746f6e20636c6173733d2244586b354d65205169394664223e3c696d6720636c6173733d22685748754a2220616c743d224172726f7722207372633d22646174613a696d6167652f6769663b6261736536342c52306c474f446c68415141424149414141502f2f2f2f2f2f2f7948354241454b414145414c414141414141424141454141414943544145414f773d3d22207374796c653d226d61782d77696474683a323470783b6d61782d6865696768743a32347078222069643d2264696d675f32312220646174612d64656665727265643d2231223e3c2f627574746f6e3e3c64697620636c6173733d22424e65617765206a69356a706620744164384420415037576e64223e4d6f726520726573756c74733c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c68723e3c64697620636c6173733d226475662d68223e3c64697620636c6173733d22664c74587363206949576d346222206f6e636c69636b3d227870287468697329223e3c64697620636c6173733d224c796d3857223e3c64697620636c6173733d2241655151756220687763223e3c2f6469763e3c64697620636c6173733d2259435537656220687763223e3c2f6469763e3c64697620636c6173733d2249795961456420687778223e3c2f6469763e3c64697620636c6173733d2245435548516520687778223e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c64697620636c6173733d226b43725954223e3c6120687265663d222f75726c3f713d68747470733a2f2f656e2e77696b6970656469612e6f72672f77696b692f4178696f735f28776562736974652926616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351466a4159656751494252414226616d703b7573673d414f76566177333867573437455f39394a326a683746384149573439223e3c64697620636c6173733d22424e656177652076766a774a6220415037576e64223e4178696f7320287765627369746529202d2057696b6970656469613c2f6469763e3c64697620636c6173733d22424e656177652055506d697420415037576e64223e68747470733a2f2f656e2e77696b6970656469612e6f7267202623383235303b2077696b69202623383235303b204178696f735f2877656273697465293c2f6469763e3c2f613e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d226b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c6469763e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e4178696f7320287374796c697a6564206173204158494f532920697320616e20416d65726963616e206e65777320616e6420696e666f726d6174696f6e207765627369746520666f756e64656420696e203230313620627920666f726d657220506f6c697469636f207374616666657273204a696d2056616e64654865692c204d696b6520416c6c656e2c20616e6420526f7920536368776172747a2e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c64697620636c6173733d226b43725954223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e66616365626f6f6b2e636f6d2f6178696f736e6577732f26616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351466a415a656751494178414226616d703b7573673d414f76566177306e6d4a4834444273432d317679714158357832346a223e3c64697620636c6173733d22424e656177652076766a774a6220415037576e64223e4178696f73202d20486f6d65207c2046616365626f6f6b3c2f6469763e3c64697620636c6173733d22424e656177652055506d697420415037576e64223e68747470733a2f2f7777772e66616365626f6f6b2e636f6d202623383235303b202e2e2e202623383235303b204272616e64202623383235303b2057656273697465202623383235303b204e6577732026616d703b204d6564696120576562736974653c2f6469763e3c2f613e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d226b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c6469763e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e5468657265206973206e6f207761792052657075626c6963616e732063616e206368616e6765206269727468207261746573206f7220637572622074686973207472656e64202623383231323b20616e642074686572652773206e6f7420612073696e676c652064656d6f67726170686963206d6567617472656e642074686174206661766f72732052657075626c6963616e732e206178696f732e636f6d2e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c64697620636c6173733d226b43725954223e3c6120687265663d222f75726c3f713d68747470733a2f2f7777772e68626f2e636f6d2f6178696f7326616d703b73613d5526616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d43467351466a4161656751494168414226616d703b7573673d414f7656617730314959735a763273624653415f745778396f6a6850223e3c64697620636c6173733d22424e656177652076766a774a6220415037576e64223e4178696f73202d204f6666696369616c205765627369746520666f72207468652048424f20536572696573202d2048424f2e636f6d3c2f6469763e3c64697620636c6173733d22424e656177652055506d697420415037576e64223e68747470733a2f2f7777772e68626f2e636f6d202623383235303b206178696f733c2f6469763e3c2f613e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d226b43725954223e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e3c6469763e3c6469763e3c64697620636c6173733d22424e656177652073337639726420415037576e64223e4b6e6f776e20666f722064656c69766572696e67206e6577732c20636f7665726167652c20616e6420696e7369676874207769746820612064697374696e6374697665206272616e64206f6620736d61727420627265766974792c204178696f73206f6e2048424f2068656c707320766965776572732062657474657220756e6465727374616e642074686520626967207472656e6473a02e2e2e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c6469763e3c64697620636c6173733d225a494e62626320787064204f3967356363207555504769223e3c64697620636c6173733d226b43725954223e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e3c7370616e20636c6173733d224643557030632072514d516f64223e52656c617465642073656172636865733c2f7370616e3e3c2f6469763e3c2f7370616e3e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b6a7326616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735131514a3642416742454145223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e6178696f73206a733c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d224842544d366420585337794764223e3c6120687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b6a73223e3c64697620636c6173733d22424e65617765206d41646a516320754565633320415037576e64223e2667743b3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b68626f26616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735131514a3642416742454149223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e6178696f732068626f3c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d224842544d366420585337794764223e3c6120687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b68626f223e3c64697620636c6173733d22424e65617765206d41646a516320754565633320415037576e64223e2667743b3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b6d65616e696e6726616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735131514a364241674245414d223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e6178696f73206d65616e696e673c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d224842544d366420585337794764223e3c6120687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b6d65616e696e67223e3c64697620636c6173733d22424e65617765206d41646a516320754565633320415037576e64223e2667743b3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b76756526616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735131514a3642416742454151223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e6178696f73207675653c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d224842544d366420585337794764223e3c6120687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b767565223e3c64697620636c6173733d22424e65617765206d41646a516320754565633320415037576e64223e2667743b3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b76732b666574636826616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735131514a3642416742454155223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e6178696f732076732066657463683c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d224842544d366420585337794764223e3c6120687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b76732b6665746368223e3c64697620636c6173733d22424e65617765206d41646a516320754565633320415037576e64223e2667743b3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b72656163742b6e617469766526616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735131514a3642416742454159223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e6178696f73207265616374206e61746976653c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d224842544d366420585337794764223e3c6120687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b72656163742b6e6174697665223e3c64697620636c6173733d22424e65617765206d41646a516320754565633320415037576e64223e2667743b3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b74762b73686f7726616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735131514a3642416742454163223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e6178696f732074762073686f773c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d224842544d366420585337794764223e3c6120687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b74762b73686f77223e3c64697620636c6173733d22424e65617765206d41646a516320754565633320415037576e64223e2667743b3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c64697620636c6173733d22783534677466223e3c2f6469763e3c64697620636c6173733d2258374e545665223e3c6120636c6173733d2274486d6651652220687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b63646e26616d703b73613d5826616d703b7665643d326168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735131514a3642416742454167223e3c64697620636c6173733d22616d33514266223e3c6469763e3c7370616e3e3c64697620636c6173733d22424e656177652064654976436220415037576e64223e6178696f732063646e3c2f6469763e3c2f7370616e3e3c2f6469763e3c2f6469763e3c2f613e3c64697620636c6173733d224842544d366420585337794764223e3c6120687265663d222f7365617263683f69653d5554462d3826616d703b713d6178696f732b63646e223e3c64697620636c6173733d22424e65617765206d41646a516320754565633320415037576e64223e2667743b3c2f6469763e3c2f613e3c2f6469763e3c2f6469763e3c2f6469763e3c2f6469763e3c666f6f7465723e203c6469763e20203c64697620636c6173733d225a494e62626320787064204f396735636320755550476920426d50357466223e3c64697620636c6173733d226e4d796d6566204d5578476264206c794c776c63223e3c6120636c6173733d226e424445316220473565466c662220687265663d222f7365617263683f713d6178696f7326616d703b69653d5554462d3826616d703b65693d704d524f586250624472505339414f426d4b5059425126616d703b73746172743d313026616d703b73613d4e2220617269612d6c6162656c3d224e6578742070616765223e4e657874202667743b3c2f613e3c2f6469763e3c2f6469763e203c2f6469763e2020203c6469762069643d226d436c6a6f62223e3c6469763e3c6120687265663d222f75726c3f713d68747470733a2f2f6163636f756e74732e676f6f676c652e636f6d2f536572766963654c6f67696e253346636f6e74696e7565253344687474703a2f2f7777772e676f6f676c652e636f6d2f73656172636825323533467125323533446178696f7325323532366f7125323533446178696f73253235323661717325323533446368726f6d652e302e36396935396c326a306c336a36396936302e3831316a306a372532353236736f75726365696425323533446368726f6d652532353236696525323533445554462d38253236686c253344656e26616d703b73613d5526616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d434673517873384343475126616d703b7573673d414f765661773338654a695f306b32366743644b726d315057417979223e5369676e20696e3c2f613e3c2f6469763e3c6469763e3c6120636c6173733d226b73545534632220687265663d22687474703a2f2f7777772e676f6f676c652e636f6d2f707265666572656e6365733f686c3d656e2d434126616d703b66673d3126616d703b73613d5826616d703b7665643d306168554b4577697a76597a4173766a6a4168557a4b58304b4851484d4346735135665543434755223e53657474696e67733c2f613e3c6120636c6173733d226b73545534632220687265663d222f2f7777772e676f6f676c652e636f6d2f696e746c2f656e5f63612f706f6c69636965732f707269766163792f3f66673d31223e507269766163793c2f613e3c6120636c6173733d226b73545534632220687265663d222f2f7777772e676f6f676c652e636f6d2f696e746c2f656e5f63612f706f6c69636965732f7465726d732f3f66673d31223e5465726d733c2f613e3c2f6469763e3c2f6469763e20203c2f666f6f7465723e3c736372697074206e6f6e63653d224f77774e4a497141533843544d31352f4351595531513d3d223e2866756e6374696f6e28297b76617220686c3d27656e2d4341273b2866756e6374696f6e28297b76617220623d746869737c7c73656c662c643d2f5e5b5c772b2f5f2d5d2b5b3d5d7b302c327d242f2c653d6e756c6c3b76617220663d646f63756d656e742e717565727953656c6563746f7228222e6c22292c673d646f63756d656e742e717565727953656c6563746f72282223736622292c6b3d672e717565727953656c6563746f7228222e73626322292c6c3d672e717565727953656c6563746f7228225b747970653d746578745d22292c6d3d672e717565727953656c6563746f7228225b747970653d7375626d69745d22292c6e3d672e717565727953656c6563746f7228222e736322292c703d672e717565727953656c6563746f7228222e7822292c713d6c2e76616c75652c723d5b5d2c743d2d312c753d712c772c782c793b717c7c2870262628702e7374796c652e646973706c61793d226e6f6e6522292c7a28213129293b66756e6374696f6e207a2861297b6966286b2e636c6173734c6973742e636f6e7461696e732822657362632229297b76617220633d6b2e636c6173734c6973742e636f6e7461696e732822636873626322292c683d6b2e636c6173734c6973742e636f6e7461696e73282272746c73626322293b612626286e2e7374796c652e646973706c61793d22626c6f636b222c633f28672e7374796c652e626f726465725261646975733d2232307078203230707820302030222c6e2e7374796c652e626f72646572426f74746f6d3d2231707820736f6c69642023444645314535222c6d2e7374796c652e626f726465725261646975733d683f2232307078203020302030223a223020323070782030203022293a6b2e7374796c652e626f726465725261646975733d683f22302038707820302030223a2238707820302030203022293b617c7c286e2e7374796c652e646973706c61793d226e6f6e65222c633f28672e7374796c652e626f726465725261646975733d2232307078222c6e2e7374796c652e626f72646572426f74746f6d3d226e6f6e65222c6d2e7374796c652e626f726465725261646975733d683f2232307078203020302032307078223a223020323070782032307078203022293a6b2e7374796c652e626f726465725261646975733d683f223020387078203870782030223a22387078203020302038707822297d7d66756e6374696f6e204128297b672e717565727953656c6563746f7228225b6e616d653d6f715d22292e76616c75653d753b672e717565727953656c6563746f7228225b6e616d653d6171735d22292e76616c75653d22686569726c6f6f6d2d7372702e222b28303c3d743f743a2222292b222e222b28303c722e6c656e6774683f22306c222b722e6c656e6774683a2222297d0a66756e6374696f6e204328297b773d6e756c6c3b69662878297b76617220613d222f636f6d706c6574652f7365617263683f636c69656e743d686569726c6f6f6d2d73727026686c3d222b686c2b22266a736f6e3d742663616c6c6261636b3d685326713d222b656e636f6465555249436f6d706f6e656e742878293b22756e646566696e656422213d3d747970656f6620647326266473262628612b3d222664733d222b6473293b76617220633d646f63756d656e742e637265617465456c656d656e74282273637269707422293b632e7372633d613b6966286e756c6c3d3d3d6529613a7b613d622e646f63756d656e743b69662828613d612e717565727953656c6563746f722626612e717565727953656c6563746f7228227363726970745b6e6f6e63655d222929262628613d612e6e6f6e63657c7c612e67657441747472696275746528226e6f6e63652229292626642e74657374286129297b653d613b627265616b20617d653d22227d28613d65292626632e73657441747472696275746528226e6f6e6365222c61293b646f63756d656e742e626f64792e617070656e644368696c642863293b783d6e756c6c3b773d73657454696d656f757428432c353030297d7d0a66756e6374696f6e204428297b666f72283b6e2e66697273744368696c643b296e2e72656d6f76654368696c64286e2e66697273744368696c64293b723d5b5d3b743d2d313b7a282131297d66756e6374696f6e204528297b76617220613d6e2e717565727953656c6563746f7228222e73637322293b61262628612e636c6173734e616d653d2222293b303c3d743f28613d6e2e6368696c644e6f6465735b745d2c612e636c6173734e616d653d22736373222c713d612e74657874436f6e74656e74293a713d753b6c2e76616c75653d717d6c2e6164644576656e744c697374656e65722822666f637573222c66756e6374696f6e28297b66262628662e7374796c652e646973706c61793d226e6f6e6522297d2c2131293b6c2e6164644576656e744c697374656e65722822626c7572222c66756e6374696f6e28297b4428293b66262628662e7374796c652e646973706c61793d2222297d2c2131293b6c2e6164644576656e744c697374656e657228226b65797570222c66756e6374696f6e2861297b713d6c2e76616c75653b793d21313b31333d3d612e77686963683f4128293a32373d3d612e77686963683f284428292c66262628662e7374796c652e646973706c61793d2222292c713d752c6c2e76616c75653d71293a34303d3d612e77686963683f28742b2b2c743e3d722e6c656e677468262628743d2d31292c452829293a33383d3d612e77686963683f28742d2d2c2d313e74262628743d722e6c656e6774682d31292c452829293a28613d71293f2870262628702e7374796c652e646973706c61793d2222292c783d612c777c7c4328292c753d61293a2870262628702e7374796c652e646973706c61793d226e6f6e6522292c7a282131292c4428292c753d22222c793d2130297d2c2131293b6d2e6164644576656e744c697374656e65722822636c69636b222c412c2131293b702e6164644576656e744c697374656e65722822636c69636b222c66756e6374696f6e28297b6c2e76616c75653d22223b702e7374796c652e646973706c61793d226e6f6e65223b7a282131297d2c2131293b6b2e6164644576656e744c697374656e65722822636c69636b222c66756e6374696f6e28297b6c2e666f63757328297d2c2131293b77696e646f772e68533d66756e6374696f6e2861297b6966282179297b4428293b303d3d615b315d2e6c656e67746826267a282131293b666f722876617220633d303b633c615b315d2e6c656e6774683b632b2b297b76617220683d615b315d5b635d5b305d2c763d646f63756d656e742e637265617465456c656d656e74282264697622293b762e696e6e657248544d4c3d683b762e6164644576656e744c697374656e657228226d6f757365646f776e222c66756e6374696f6e2842297b422e70726576656e7444656661756c7428293b72657475726e21317d2c2131293b683d682e7265706c616365282f3c5c2f3f623e2f672c2222293b762e6164644576656e744c697374656e65722822636c69636b222c66756e6374696f6e2842297b72657475726e2066756e6374696f6e28297b743d423b4128293b4528293b4428293b672e7375626d697428297d7d2863292c2131293b6e2e617070656e644368696c642876293b7a282130293b722e707573682868297d7d7d3b7d292e63616c6c2874686973293b7d2928293b2866756e6374696f6e28297b66756e6374696f6e20622861297b666f7228613d612e7461726765747c7c612e737263456c656d656e743b612626224122213d612e6e6f64654e616d653b29613d612e706172656e74456c656d656e743b61262628612e687265667c7c2222292e6d61746368282f5c2f7365617263682e2a5b3f265d74626d3d697363682f29262628612e687265662b3d22266269773d222b646f63756d656e742e646f63756d656e74456c656d656e742e636c69656e7457696474682c612e687265662b3d22266269683d222b646f63756d656e742e646f63756d656e74456c656d656e742e636c69656e74486569676874297d646f63756d656e742e6164644576656e744c697374656e65722822636c69636b222c622c2131293b646f63756d656e742e6164644576656e744c697374656e65722822746f7563685374617274222c622c2131293b7d292e63616c6c2874686973293b3c2f7363726970743e3c2f6469763e3c212d2d206363746c636d2035206363746c636d202d2d3e3c746578746172656120636c6173733d2263736922206e616d653d2263736922207374796c653d22646973706c61793a6e6f6e65223e3c2f74657874617265613e3c736372697074206e6f6e63653d224f77774e4a497141533843544d31352f4351595531513d3d223e2866756e6374696f6e28297b76617220653d27704d524f586250624472505339414f426d4b50594251273b76617220736e3d27776562273b2866756e6374696f6e28297b76617220713d66756e6374696f6e2861297b76617220633d77696e646f772c643d646f63756d656e743b69662821617c7c226e6f6e65223d3d612e7374796c652e646973706c61792972657475726e20303b696628642e64656661756c74566965772626642e64656661756c74566965772e676574436f6d70757465645374796c65297b76617220623d642e64656661756c74566965772e676574436f6d70757465645374796c652861293b696628622626282268696464656e223d3d622e7669736962696c6974797c7c22307078223d3d622e686569676874262622307078223d3d622e7769647468292972657475726e20307d69662821612e676574426f756e64696e67436c69656e74526563742972657475726e20313b76617220663d612e676574426f756e64696e67436c69656e745265637428293b613d662e6c6566742b632e70616765584f66667365743b623d662e746f702b632e70616765594f66667365743b766172206b3d662e77696474683b663d662e6865696768743b72657475726e20303e3d662626303e3d6b3f303a303e622b663f323a623e3d28632e696e6e65724865696768747c7c642e646f63756d656e74456c656d656e742e636c69656e74486569676874293f333a303e612b6b7c7c613e3d28632e696e6e657257696474687c7c642e646f63756d656e74456c656d656e742e636c69656e745769647468293f343a317d3b76617220723d652c763d736e3b66756e6374696f6e207728612c632c64297b613d222f67656e5f3230343f617479703d63736926733d222b28767c7c2277656222292b2226743d222b612b2822266c6974653d312665693d222b722b2226636f6e6e3d222b2877696e646f772e6e6176696761746f72262677696e646f772e6e6176696761746f722e636f6e6e656374696f6e3f77696e646f772e6e6176696761746f722e636f6e6e656374696f6e2e747970653a2d31292b63293b633d222672743d223b666f7228766172206220696e206429612b3d632b622b222e222b645b625d2c633d222c223b72657475726e20617d66756e6374696f6e20782861297b613d7b7072743a617d3b77696e646f772e77737274262628612e777372743d77696e646f772e77737274293b72657475726e20617d66756e6374696f6e20792861297b77696e646f772e70696e673f77696e646f772e70696e672861293a286e657720496d616765292e7372633d617d0a2866756e6374696f6e28297b666f722876617220613d2b6e657720446174652d77696e646f772e73746172742c633d782861292c643d302c623d302c663d302c6b3d646f63756d656e742e676574456c656d656e747342795461674e616d652822696d6722292c6d3d2226696d6e3d222b6b2e6c656e6774682b22266269773d222b77696e646f772e696e6e657257696474682b22266269683d222b77696e646f772e696e6e65724865696768742c413d66756e6374696f6e28682c7a297b682e6f6e6c6f61643d66756e6374696f6e28297b623d2b6e657720446174652d77696e646f772e73746172743b7a26262b2b6e3d3d66262628643d622c742829293b682e6f6e6c6f61643d6e756c6c7d7d2c743d66756e6374696f6e28297b6d2b3d2226696d613d222b663b632e6166743d643b7928772822616674222c6d2c6329297d2c6e3d302c423d302c673d766f696420303b673d6b5b422b2b5d3b297b766172206c3d313d3d712867293b6c26262b2b663b76617220753d672e636f6d706c657465262621672e6765744174747269627574652822646174612d646566657272656422292c703d7526264e756d62657228672e6765744174747269627574652822646174612d696d6c2229297c7c303b752626703f286c26262b2b6e2c70262628673d702d77696e646f772e73746172742c6c262628643d4d6174682e6d617828642c6729292c623d4d6174682e6d617828622c672929293a4128672c6c297d647c7c28643d61293b627c7c28623d64293b6e3d3d6626267428293b77696e646f772e6164644576656e744c697374656e657228226c6f6164222c66756e6374696f6e28297b77696e646f772e73657454696d656f75742866756e6374696f6e28297b632e6f6c3d2b6e657720446174652d77696e646f772e73746172743b632e696d6c3d623b76617220683d77696e646f772e706572666f726d616e6365262677696e646f772e706572666f726d616e63652e74696d696e673b68262628632e727173743d682e726573706f6e7365456e642d682e7265717565737453746172742c632e727370743d682e726573706f6e7365456e642d682e726573706f6e73655374617274293b7928772822616c6c222c6d2c6329297d2c30297d2c2131297d2928293b7d292e63616c6c2874686973293b7d2928293b3c2f7363726970743e3c736372697074206e6f6e63653d224f77774e4a497141533843544d31352f4351595531513d3d223e66756e6374696f6e205f736574496d6167657353726328652c63297b66756e6374696f6e20662861297b612e6f6e6572726f723d66756e6374696f6e28297b612e7374796c652e646973706c61793d226e6f6e65227d3b612e7372633d637d666f722876617220673d302c623d766f696420303b623d655b672b2b5d3b297b76617220643d646f63756d656e742e676574456c656d656e74427949642862293b643f662864293a2877696e646f772e676f6f676c653d77696e646f772e676f6f676c657c7c7b7d2c676f6f676c652e6969723d676f6f676c652e6969727c7c7b7d2c676f6f676c652e6969725b625d3d63297d7d3b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f6a7065673b6261736536342c2f396a2f34414151536b5a4a5267414241514141415141424141442f3277434541416b474277674842676b494277674b43676b4c445259504451774d445273554652415749423069496941644878386b4b4451734a4359784a7838664c5430744d5455334f6a6f364979732f52443834517a51354f6a634243676f4b4451774e47673850476a636c487955334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e7a63334e2f2f4141424549414367414b414d4249674143455145444551482f7841415a4141414441514542414141414141414141414141414141414267634243414c2f784141734541414241774d434241554541774141414141414141414241674d454141555242694548456a4642453147426b61456959634852464256782f3851414751454141674d424141414141414141414141414141414141514944424155412f385141485245414177454141674d424141414141414141414141414141454341784a4242424d7842662f61414177444151414345514d5241443841754e655843416e4a3644725735706631746450362b794f6870584b2b2b5044622b3265703971664f48644b56325236364c4b4862364632343858744c57366139456b6d64346a4b696c5254487944397763394b66597237556d4d3149595746744f6f43304c4232556b6a4949394b3566313559333151303374686e4d6474774d504c386c455a542b765565645666675271495858536f746a7a67564a7468434d5a33384d377039747836553232667274794a342b767479566c506f7242306f71496e4d5053705a7275354766656a486279576f333044486452362f67565272784a6369323139356c70546a6752394345444a4a376256504e4c574b5a4c76726239776a504962625558564b6451527a4b37664e58764334787931726f797630656438635a58305a4470526d586f682b7879414f61537965645248527737672b687837564265476c346530667841615a6d6b7474716456436d4950626647663841516f44357271554441785541343161496e6a564b62725a59456951334f547a752f7741644255554f4a786b37644d6a487a564f716455365a705a776f68536a6f45644b4b576548647a6e584c53634a64316a5078357a4b664265513867704a4b647562667a4739464b4f4d7441414859555555414734464742525252434742355555555678782f2f32515c7833645c783364273b76617220693d5b2764696d675f31275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414945414141424943414d414141446d6d575965414141416f6c424d5645556949694c2f2f2f38414141415a47526b644852337537753751304e41534568494b6d654156465256675947414f4467352b666e3757317462382f507a643364314c533073784d5447506a342b656e703675727134714b69705a57566d30744c544477384e766232382f507a2f303950546a342b4f38764c783364336547686f596a484255556572416545414137676138416d657133312f496a46674d586135704570654d416b64375336506f61585952666b376f4f6a63346453474d476e2b6f564141416a445141664f6b77684c6a636b4141416256485a4d6b39507941414143636b6c455156526f6765325732334c694d41794748646b4a635135415349434568435464412b774a324c624c2b372f61536e4b67334f4f5a6e646e526431484c78575039746e384a6c42494551524145515241455152442b41347a57326a794d48476a3661397a6e4d3044306662314e30356e784b7142496b6d53706c55356f744e4d2f6d7655475a355149564c614e366e49444c6d74716d374c4d647542527761665051524345566b474634787a504452454743326944494d633855474a414c48597036656e647450497034637458334447624b656877334550615577616c777943495145456533476c5342634e747376636d3458434d762b47474853697a6f353033674b6d4441517772674a7254686537675331764d6359676958756a4c43324d63662f394247326f464b787a7a4d7544444f77554e35616f4c6f2f6f35793654374b5145324e4b536542507938785045766c395135674e78677a4b5341336d434c486a5441463751636e414b6f4275767246635a54484d66734144532b556650626730384b5345374231773137537235685439617235554e3150696e6766496b764c37387a50697157576e4d33475374594f3463517475645046704d5238384b54684665386773766261454e33566e594350346854554842644f67556b726f5a37626379564879652b6f51744f72316a30624331584456514a44362f51756c54634c6b704949616c797168612b732b6435666345724f492f4b724e45414c62416670764d354252326e785a57574c5a4c73686d67414d4d6e48367a7a4a4f39727739416344324f4b6550643345766e2b73526d35417051565930726c4436685768776c7277706542775242396552347a342f6b4f36434e634673395170534e6c355965344d6d5042547446585a6565764c65415778693236746f48646d6d4538393052547452316465676448686255493934326d7747313075787750486d6973647a613563515554544e354f355351766168507870706e4c73646a3671635478667239663361574c364c4d75474e5956416b634a3551316c67557932365254335947532f4457716a7262652b704934334966574a6e6941733577726e4c6f6748774e387273746b3754377858724a623867434949674349496743494c77442f6b4c6b327368546d377038716741414141415355564f524b35435949495c783364273b76617220693d5b2764696d675f3233275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414841414141427743414d414141447850675235414141416231424d5645584d4141442f2f2f2f4b4141444c4141442b2b2f763435655879304e44514e7a666f72713754516b4c72744c54696b5a484d4a53584b44772f34346548484141446f7036663939766235367572383866483031396676784d54676834664f49694c61626d37636433666b6d4a6a78793876575631666e6f4b445453456a55546b3759596d4c4e4768726666332f514d444475766231676d3047614141414758306c455156526f6762576234614b714b68434662636a4b4c44484e637165563758722f5a37774774484d424b70347238394f5154324341575441464732584868635757595855706e337661373571345341363249734e325349713432584861503875344374385642467a5a31564a36382f50675243774941736149732f4b6154634e6c61636d346546395538506a5a484261424d6b714e316d31755442622b4b30507265446d68645a63316f2b373762585852706864346a446a51354376385562676969783033336d387236414e7548695a4f764c452f4f65475746374a5830414d383952656e327346356c67333176573844486b365733756751387a46653367785659414b76773539487a566a37546d507451324131396e6e384d6779386a725950674e6c75654c686265367947654b45447277754d4866716a48484b636376534441626a614f78546e6d333765316f585841526f443369354b6e477654684c4665337145304b6e67766e58385039394c3442376a55696a4d57586174566358706f79317a762f452f3043756965726c61766e346436486b704c506e4e7269794e4f74304a3953486f4734726e50627a51586f4470524c592f6c34715758787736682b727442464d38756b637165525256396e48362b377255395734444c573763384f3364724c634364714c44796374776736753576477a4b4279647053616162325876416e74724d3273514967443858444d4a466c617a4c47734470333637794c702b6b39716f2f79363646544c58753261735633524d537a2b4261565969535037592f616a722f64473156754f574e734a346862614f4c5446705a63756b5834567644614375695734612b6665516856306e74364c392f393041372b7531507942707234612b6c5563464b7133674d6933494c452b68762b6a64676645476f5577456738496a454e30472b3470596b4958416d497245414f6a304d4c4a5a44747865386c64756f556f4278505a3241673136495170366b5265765544412f3661426d5272345767704e44454b3359464d5241727577486235464835547779676253326f2f55506252424744417a4b6b52554f494f4446672b445569524b464a446b667345494a585467476f65342f354457336467384b696d41646b394d2b70557a39794172446c4d416759557638746b734b507732423359397047786c6734436c593973594a506d6f5475513751496c44394f6a47314175463276636974324241656e37345168512b6367524e3732584f394459674d654163726e4157423745787478413556304a646d7271455269516d41596f48336a75452f676a436d4b3038657352474f78465a495842307237774347534e384a73664b4e6b632f41454445744e673159337631444c72436368494e43666d38477a704436693233514d63644644734561695544473746753556486f4654417141537057666f444b744652595a5261654151714266794c3673596a554a3263354f673346342f4134476d4a55742f507641476c6b74485554656b524b4a58526f734374654f5552794e6269425467325a48655051435658387a31757852642f514b566b674d42714f506561462f694a55753851382b424a78377a41674552732b656f2f444a77644b4b505565793978426941396f454b706276712f3650384464316563427161366d526e347a4c4842596d7145634877316377767871456b706d5733506f664d73774d5664587a34586d72715a47336a456f4e73537063344d314134564c65706d6271424e795352506a304474356b5a47716462626e4c6d416d674b5736755a70385a765a674158347a554e737862596c6454626745734931715752513363774d58427a4252355477683256325a714332664d717a31462b6a69544d434678685a53415673584f504f436453556a4e694b4e336f5435775271536b61637752386172596c7a416e55464c4657785436436d5a4f5235757a5931484941702b4d4967634d6b4d4a625049344a6b445542755a51534436694f304d33674749303273456d454f594c34562f6a74757a425a68324c483571517a414d314f3970684d6948726467473542335438794847674f676a37436165645a74744177375a4b44416a773866674c4856326f4f59335574313074754c3567526e653030683138333168667544696851366852616b65674b69413154334e3766504d4278447661526775715436416d674a4764654d46695063303671723336524734534347456b3165393667782b4b6c43635347493269415834395248426b506c4c612f6f486f4c7a4777767966737757493236343862792f454d3345496b4a783731314c4e784257734a6c335746694157495855477a786e6a49764249554479477653595073554f494a5042693632505961536f314a3637726b336c6b79347a304673304f6d4f4245396e77767a572f6b6c554b75726d74776f6f34416c31712b45612f7335664463464c6f427732506a4f67397871316f54373953547449634b5746374179797069644645654a4c3157706555443239664b335a37304d6b334a554b507550784d74556d58314a776e4c596b5941454e6853495a536875676b5933654c5836334c5455305a356f566335624e594d476d6b587266506679576e4746374d6f6d38537a547772567158754839615231386d6c4150512b696177574e767339752b5351672f78336757525777586b47786d414a5561553239466f356c62516f56366335546556674474724b6b4d5864355a5434467950686754716d777a5a446a55435136794a56483535354644657a566b7a76397275416d46796e48356c467035434c5a652f566d397878474a3756477565484f71577565666e62694669522f76443672367a694d7a6c45364957462b6357783257696f7165386266377833613856746a55586c7847547845786e64362f366d674e5349656c476c334f4437704c5262626271746a6b76334466783765365a7176754978327a3374357262522f54667748444f747262744e7643415541414141415355564f524b35435949495c783364273b76617220693d5b2764696d675f33275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414841414141427743414d414141447850675235414141416456424d5645584e4742332f2f2f2f4a4141444d414144373850444d44685856556c4c4d4642726e73612f37382f505556316a76794d62504953583032747276797372464141446a6b35585150554c4d42512f787a39444d41416a36362b765351304c2b2b76726e724b7a32354f5471754c6661654866585a4754737672375a6348445961576e6669597664666f48504d54446a6f364854536b765350446e6a6d70765a704856754141414461306c455156526f67653258323361714d42424177345159735642705141533543496a2b2f79636553494a415346307439727a4e6672484e5a546245535449536769414967694149676941496769414973676b6d597141514338614e446a3567616541324c4d4f48475749646e43625876413772706a6f5145504f78784f395a7a423461664b342b5448547a776769304c664a4742743852594c6f314f6a6b4b4c3768387a5a5277433370536d466169692f71474c49597957484d5a65353947547545615a57636433413175766c59366337496948683845396b5044783079343834615747414a6e545342554c7832466774646e5938776c595376685949692f46626f7668653563534c6c6c56456774517363723645736832495673496153352b58707a59526e6d6564366b6d57342b4a2f53566b454a6d695a5574684841596661653036594f48355578344261423938736230324f37564d4f386758676e4a555350484a7570766d43397058476a6635533644437771515a3072495366794d4b4b4251442b2f434b2b4759467141656a7131377756566871696e704f6656444b61526b546e794d354e413966555049495657724449766748496f6d4a72526343416e7a737a484d5a69487235462b6e75316a47376e644b502b70734e677048657a594c395174326c4b7942306c6b3133527731636276514d535a4f634f7173684379525231314e7477726a5175367473583042726464434168394b7446564935596f477134746a6a4c312b696c784f3844634c5a614b6e31685674413475513357574f4a5275467a4a636e5832564c476461644c4549563646787346616f63574f304a4f6162772f6c346f4f696b6b7470775277354833763453326e486b6c394137764c656d5864556b50746955563356386b5457354e6d7353574e46444c6262483561465062596d2f64467235745736674a356561545268326c67572f37456d6c704f646f365479334a357150744d4877366865314c48413456737731554e66444f625347664f4c4f7436544448664962453053665464754644686d6a57526e6c314c5a766952463265795273584d4c2f4c532f78636d5562654d706134792f654c56526e2b654b6645494e4449494e357559525351317054514734466e4f674856785849774c364b456876326969464a35344e7734544c70447049756f422b744c4f646f586370422f716e48756e5432466e2b32587068552f466e4b69612b5567684b4d4d666b777535366b517a737048474b625065746f564d714f5663474950507859534e6c586e575271476a314c566a665a5350306a55446a4b46743138492b2b71764e4f4e2b49335276342b2b3474345345482b755459394c454b3245354658656d4d41586a42356b7058505953656a63435a4a5536376937425365616d6d333355414e4f39435858304f534f7149536d48542f45554476316c4e77714e3371485568766f6a6b2b2f746e614a39432f72484b764375714b7271756d75424c6737634958466e304434564271597838742f6e453571394b735239642b324446776d647651706e61702f5a6a7665335563475a72654241454152424541524245415242454154354466384152776f2b4948597561487741414141415355564f524b35435949495c783364273b76617220693d5b2764696d675f35275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414555414141424643414d414141417255397362414141415946424d564555414141442f2f2f2b4b696f724f7a733553556c4c613274716e70366672362b7648783866382f507a7a382f4e61576c712b7672342b506a373239765a7763484376723639396658316b5a47524d544579676f4b41774d44415044772b31746257616d706f6749434156465255714b6971546b354e455245527061576b354f546d4d703246774141414239456c455156525968613257366136434d4243465757565241554655334f3737762b55464539716d6338704d577336764575424c4d3265324b4e354455534c53536631775171386a6d5470467959522f494f574b4d7670446e73554b4b57372b6c4c4f3653756f506958704675516451486f7079436143552b317255424667304e53736c44376a4b50686131697649496f4f6a3862774d6f2b31696b382f2f7044376d70713452594e4f6a6d456b424a464157336f3965677a3333716b67704c2f4957554d74626e613879712b6f4f55334b436b504b57476b4c6b2b39455042553742466333336f454e55383551417072554535386843485258654430764951522f366e4275584151796f4969577144346d39526246427946754c492f3936676a41304c69612b51636a556f47513978744b695451526d366736334b707277677054446a516a5353752b447633473857585777497a7639736d2f4b774b5868456a2b30734e3657304b572f3374323652477666702f792b5351443558365730497a6e3947695530706653696454656c384b4b51366662616f4737486f374548356b43727947644666472b49316f742b37574554794837636f527353696e76306c79596c4963486d4c79505770696f6d6c43495972586c476e2f766a54373447484f4b624932672b587336542f347847394474526663415555624e487170456e636b474f4c576f764e4a47366f674a447a6d672f4c67324334596f745550357a5049386b774b707a2f4b71435262502f424c55706c7139436934365a46432b584f513271592f35504b2b5569306f6d4b4c50757239334745465659547a58776430336c7745467545527259654e7a434b38464f6946456d775956425675555471676f686256514d696b5034674769555766444d6859746951646974632f7555735465446a676a616741414141415355564f524b35435949495c783364273b76617220693d5b2764696d675f37275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414841414141427743414d414141447850675235414141417146424d56455541414141795a737a2f4d77372f2f2f2f304e42466246776f70544a497a614e424f546b374c793873725973744f6564497156616f735a644258614a442f4141442f4c77436d57452f4e4b51757071616e2f61463956664e4c4d31753854574d6b2b506a372f7a4d725330744b4868346663334e79337437664377734a3565586d546b355072362b76464b51314b45676767505855794d6a4b656e7035746257332f3239556348427a2b764c622f66572f2f6548442b7070372b366555724b7973524552466a5932502f5755784663744259574668634477426c5555395759486b6636744c75414141427a456c455156526f67653359363036444d42694134627036574f64685536596955416271594e4e4e6e4b4c652f35324a5543627041637570662f7a65784d515136324f686443424345415242454152424550516675314a302f3944686c7a36654b554d6a525964484863446a4532564467516571414151515141414e67444e6a344351506a615a7955674358646c365366657453516f676678633341367a794556334f704b49414f7a725057464f2f7a466d746463484a614445485a31354e736b674a496967472b673674353478596758756e4d6b474235555173776d324e37454339616750685a4f4b6e36494259587a392b6765464a72514e393141387572584d6357494e37775531534374447751684d6f70616f4262585a442b486c6d57496d6b437672427a796938624657685644746c4d444e4d47344a62396c573141564f34422f454b74417a64647741524c447634427a727541355172674c2b4a77494c7335516d4f677a30596241796d41413131446334754762545838396a333466656962416c3032324455466c7338334f304f677863593672326241674133464166656a76594c59693232453176624f33582f67383364687a324232317a6d4f56336d6b6b5479323951787943536430594a434b337042674b486b4d48684b6b306c654c57764474726b67586a464c693565737a3942782b67366b4633322f79304778614e4e4945732f737753654d343371574a696c4f384839376d36623979537a66764a6d4152674141434347414e474a47663150746e3732435461762b62714f716a43336975444632712b687933372b74435759647051424145515241455152436b33546552454559374c6f71374d6741414141424a52553545726b4a6767675c7833645c783364273b76617220693d5b2764696d675f39275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414267414141415941674d414141436447645672414141414446424d5645564d615846436866524368665243686654307443505a4141414141335253546c4d4167464a456b47784e414141414c306c455156523441575041447867647742543342544446394155697568644336574e4b2f762f2f2f792b55676772436c53413037455756676c6d4546774141356559534578654377696741414141415355564f524b35435949495c783364273b76617220693d5b2764696d675f3131272c2764696d675f3231275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414649414141425343414d4141414477386e4f70414141415946424d5645582f2f2f3933476b6c3247556a6d3074794b4e324a39496c43494e46393548557a31376648372b5071424b46584f714c7a392b2f796556337672322b547a36652f446c61336276383678655a6152516d75565358436a59594c77342b7135686143454c6c716f615972486e62505875636d2b6a71657262343751726344697939636a4536764241414144756b6c455156525968653259323961794b6853474253564555544655464866336635632f696943536c562b4e64624a4737306b6a6841636d7a41306142442f39394e4e502f325052754e434b36616345767948764d7130752f7778597a6a744c2f34513371425746487848374b72562f73746b6777614c506b48534b3049344531667731556847686939544d6235416b5662766d496d4753666f636b556f31316b62426c38566449496c7331314558654a416d2b51524c5772754d63354b526439464d6b59516e306b5a76546634714d4d7742385a504157535568634e49304b56584953713339486b727958517356715656565a5051705a357551724a4733536a69634951794f4d576a354f78546262784a53475349384475474e61386755796c686b43436752324c662b5362467058576c52346b5832474e3758506b666d59484841574331717865484a546e5478556a354f6e794c7a475a3043396277767a7a38696965307063686b6e365a79526c79424b6833696667626750506734622f44526c794f7835463958325154497856757739454d696a71534f6d4754464d626166467a4a4758324a4b4d68584632636b714c5039715633684f616855726d5a44394551626a7048787257317348644370686e4e544a446e76717450723130394e33596a6565686c323245372b38673330564f3257377a775937326b6432774f6f6677724d754e61776774706153782f6853526e534a4a764b72795a552f5157535949317543376d53784b4f3731665a424568634b68516b7a7675686a717758506b655767537151394356534f5754597933764745777a322b486d4f6c4d486d5975644930737970364b70494a553177544574506b56536f7177456536436d53354f6c59335235676235426b564568594e536449476772755a2b424c794870424a76306a4d7062384c4c764261386a566367385a6938514871765769396e595243556269752f7141344a4547634d493756724b337271365273493739674c7a74524c6a517370475644626b53506152626b566c78524d616473385132593331596248483748726b3630534e79746f7545714376644634494c4d533431306a4f63375847337051416a2b52355a726b6a76654d686f7265364f784743346b6a62555879794f546d544c78455032337964376c64793267533679794179796e59394957796865707542742b3836525a71425a774f367572354771674236527074755755586269644873616b4b72726a6f544a736d47483437467543626d7a7a4a6a5a63465449336b50434b6a54544131415848704b4b2f5972484a33336d744f68724e2b794e6e5935466e4d3135735742687443376b344a64393469776e45326d355a48576e446577466e745237473235357661547a684433576e6959376844684b45504c7a4d4759615365394f493453334a554f63565568716738523042623677324459756462747531352f6745626d376b53396b64376b6a767176613063455a4d706a504c342b524d49652b567066566375636d2b684a4a792b7168546b436339633439793768584d54713332794e53767849413078694f4c58424e676a67532b584c412b72304249476263734243746d663641624d52646139674d436b672f526e627263464b4a6d5377426444656162474352307654554a3237626a5a796d63424c7256355436727371454a6c44623778437171756459582f7a596f74346634394f58783865656139645050776e39394e4e502f34332b41627851504c6a33385835594141414141456c46546b5375516d4343273b76617220693d5b2764696d675f3133275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414841414141427743414d41414144785067523541414141636c424d564558754d794c2f2f2f2f74494144744a513334747250754d522f74496762306a346a326f3537744b5250744667442b2b666a326e706a796533502b39765838337433754c5272764f536a765244627550433334757258765545583936756e316c354c734141443731644c3372367636794d587861562f30683444367a63727858564c796347667857457a76537a2f35774c3378596c6a796447355a6e6968724141414745456c455156526f67653261375a616a4b424347465270554e49716d51324b4d332b6e37763855315569674b5a6e706d31396b357537352f2b675331486f47694b4d70326e454f4844683036644f6a51763667414c555578333564583970384c3362324f426a7343306164724b446b6873682f7777775336626848735272514433527a395a714162375558634173624f546f4f71674c6e3030534a577848496e563158413634324e776a3041377a754e71514a475750346d5349436a4d6b4a6677765051347247424f774531684231754e744c76414231306c773039612f315271627156583866664433727944586e3461546236747069314366545a616677626e6c555873657837644c50456970795646732b7a64644563306b51324e45674334786d5977477a2f493844726252783168487a3557374267583241566a614e2b682f36355462414e74437a646e7765754644486e4454434d6c6454394869766e786841612b2b3843512f48466e4733676241592f775061647a52594a675957564d46757773674772376e58724779435234686d597a6c2b4c46526f4a79313134304c715432346530542f6d506530696f4d70317070686c346e6476674e65766448496f7a2f7947517759703148357070656f4c474b31756a6c6b42526a6371564779546f5230436b2f4e4c546f69362f774b7757477a776a654163656d436e5a6532445167756c65343545415a6c56737067784770474641544735663734416b5662366f6d326146624179377a645450694b57457931454e7337644156494844364b5a5a424d507a5a5863594b3942526f394b73675651445472366f6d38594e4e50706245326a74495947352b57706764496936494e2f4577356f7636716b507a38446a716a65384751692b4e3832683237547937776c65686176667765434c594c725148515a47774256763036485a53386445483538564c2b7a4f7371733534764a4e594a4f6f4f6546674f694663785a64686c4e574b767543356352736f45716b7048417547495a44305a34596f493272664967524233684d323355574a6f437338654d326d7876514e304642457356725a636546485062695336794f734273454e703775465535754e626d745a484674414d517957476a6c646354614e37614b5a322f62446e7743477a79457444657251754843697a68354155592b75695a3969325236576731762b54614435644a686348556937672f4f6e33736d71485265394252682b47386766336c4c6c5634766f354e4345645634685242774c6b5565315843444749362b6e5347733258732b3268634878537348797451686d2b4a786c35344252767658496f4f452b55373938487470617849634f2f5235685a74567535536a734a315a39375558633269333271696e386a34476e2f3177502b334b3574625237526573705457544c6e555775437349483264446a685639614f57617150346b6a31443261356e6c6d36343074594b52746d73636c6d4f494430594d47665664743351515364726d4c4d55754e63382f5279774d42505658696c586d4534764d686b53544c5a2f582b56376f64716261415161706e4d364b634377536f3062504871734d6a634a6e667858317472536873413347397974632b564a366a4b6b655437596161774e634447314f385067484c7542303854515079334b4a4b6362504352324144756b6e36746f7152464570567a6164366944746b61387255654a536654684375454f6f6563535932344d617832374c7748787a4279546c76686b51764b36586c49636d666177623353307253467037395241434d723145552b644d78704c6564453231412f46536a474c7a794e5a724b5939516471544e384f446a6e717a44453543485a37626745436f516f5259796f3038377a65366e2b4138784f70537334326f717067314f684241373150674d67684176326b4a597153786e62426954796b466a53715a6f6c5061566863676146616963384857394e71414c434b366f536743564b32594279524d504f5355466348756d6a6d79786446456864534e4534326d4732416849755454586d6d453666456536544d706943574a4d4533615137684e6f464f63623165516c30626e4c614c51467a64635a2f69526c4c445a52547358486c7441624367662f446e45524c704448584e696a68573841795851484278766541314e38774b394157304673447764463973795a734151627962426d577035556171506a6e7a66704b6c36336e554d36365a2b594e4675425541574c4255687a4f766d4934524b36757249415949734a6c323073314949477053725449784d645868524c716f6b4936426b7979424b7276536647626861383773504b612b3030744b33595a69336845726e4d337270556c7a737258755a726f6b5959674469556c7978526167535144582b6a54327a4234394f5a4559546a577464566d474a654d3451437a573132356e327747336c366258486546636c5a6f32793673477a425664632b77754a616c583732736a5846782b6b5467697274586576667831394235745433705879324743375a553037376a30386f314e4e5a4a6739713845472f7368354731696d6b48456d34533551524e33306230563845326f4765766d6d376b4e4d52592f5155555866426c5659454c5332784a4d61724f2b726c794145497063753151424c57465a7252345476394c77476d70495957665973334e704a4a3769376279524e4c566f7a4c446f54684b7934387153664c4362357a4638356932555a4550562f7072693255326c7a346d3156324b4e33474f537472742b587a7753686b6f4d6a4e706a7448347458482b527847753653676948547030364e436851332b492f674a2f6948506855784b5a387741414141424a52553545726b4a6767675c7833645c783364273b76617220693d5b2764696d675f3135275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414841414141427743414d414141447850675235414141413156424d5645582f2f2f2f6a6641446a6567442b2f2f33696467442b2f6672686367447267514438367450366a774436376433392f5066376c41447468674431696744326a67443335395037394f763738655437307148706a525866625144716b437a3133735432343833686677446d6b555838324c50366f4450316c532f766d5437726b68623030375478794a767477492f737549626967436636776f58346e682f76304b726c6a546a6f6744583236642f77785a37686642726e706d3369677833747670546c6e316e6c70574c7172336a767a4c54706c32546e6a457a7670337a757771586f754a6278313862696e573770716f44706c46726b63786e707359336b6479626d6d555030723176347a4a4c367533583272453331707a6e796641416c4e7748774141414e4f6b6c455156526f67653161435865627568494749555143735931494d4574365578616e45474e6a77456e644e4c6b70625872372f332f536d7846657744464f2b3972653838343731546c3144466f2b7a66624e534b346b2f576c2f6d6954706a764c76417159732f6e634259325a6e36722b45705a704f6b6b526c3551572f46555a426d366e635361496f4468304c704d75383548666953524a33677a444b484175526c314f4657776c3166694f674730616861786e71536a47347362793965372f696269616276774e4b6b525365784a4659652f7268357537526c5054567a6430484b6244693334496f57566b5563496c7a2b4d72763339383833457553385948666378377965577239636a676c695630442f6b342f6d737370574f2b653636725264446d426c4e37785834786e785948424a5555314832372b2f6d67595169442b734a713647495768593651706c33346c366269527a706533793655424b452f4c6577752b54766e7138655a6d4b5a416a79357a4d6a562b495a325a63637039752f6e34414b62677133542f64664870554662363865586f537174526a3157585672304a554a434d7955484f3371395855584b71414b443038725a61415051554f554666674e30346b42617871614f46486d3272736b614d6942655953564b644d5665582b34656239453677364258633137724558465875376c494b466c4c487352385341565254446955732f54564f2f7142795541324d503133534d3230635142722f6533393138584973396658783457454a77726a37646656436c49414d6d2f333545574a6f48706363596f35544370383238644747756a6349742f75456a35782f76707841504b78302b5631504f7265586a337a667751706f7570616b687157456d56665148614e55745a45706b797168334d523566654d7932575435763071756c532b7239394f6e75426f4d4e325852353933674c2b6c773933433978543462705a4658783643564759627666475278475261684d364b514954554f465a7568426d524a477834474b4b6f5552717252386646677041516362547a393875734e34554131754a6d57614531414c4a64544f72474c6d66422b694a594e3065597955714268476f306f6556446c6a4261416c316e513152564c6a397838665079355269397930544366326852474954414253546f73713043635439377438315a68545670677730417a6e5256484d4936665256557a735757796f7a764c6d41373551514c59555a654d414e674555496f4e654743506a4b6a41744c756d754b6166485357367a47576353414942546778454a7754586b306855305855337373614d75507a6f636542706b57306d75365251455147515a4a5a756c5253783242794670756645386d666d39524f3434727237656a6d4c6f7742636c6f374973316b46513667665971382f424f4a6c6a38506550364a4f4b75386852697a434b73627859424d337975684e557151632b586c5330364b4e564d70756b666855366131413342654667475a764b6e6b647444424130494d696432677755646676707653475a3551785641426133535a3370584b774d6476426e464e2b4467744f616c54324151686a302f334673416c63795849616d6c646941345559467247485861466770385364317348726931687832497850687a346e516f384b64796f4d6c7945597a6146453250317a4a4351386a73676544714a2f46384a336d6d623774567430596c4a64487770526d4465487470417a5870537864514671436e5668426e446236625a7948356e355a5a566e655536334738364b2b386968722f47514738685549703570516b6f574e2f536f3070616952696b5179557970304d733461492b6978543344487370444c5330736f347a6a475645425a654241526d77484c627979775142724a636c7330576d656754726541674335637a6c4f663179674c545350554a444968452f71464c55304b384952326367706d374768394448566d56736f32556d39513037576e676a337a79704855424879504641587a4f5a71496f5249555351304b4559516757516f6875423936536b624a41566f3158644f4569712f784b46555059567255474754394151717264456b76684d6f4c6e734c6e584e6a4f4c46435473435566597643414b416f75394c4a6142616468784b766e433064764a716d7863417268414c5278426b595758457053384c7859435169424f67495330344b674c6d6b6148366d415935496e69643539742b594b614f6b3852426f4e476d334b746c66456933672b526a6f68794b62365975794471424e427a55614a5157446e432f306730726f5a637a7333793636755a6449347449416c525741347775666f4c425231764b71363155526f452b5253515a57576a7a7970566a614d6b6550584b444f344b6f776f366567625a524f71417852505a737a50664d51726d7a4a61444c55574535446172674e3834503745456d6f67496c526579516e6f536c62534b617a4b4f49497a5546586e744b454b4975646774424c4e704b7933696145497a6b396e4651686b2b5a6a52462f435938316678444e77536a77377241644a6f4261456f484b3851626d68594a684237737a3048765a546c4652786169472b7046534835556573317a635777304d6442377744754c487749725654486f45344b344473354c7a4a584646475a54443343434a59676754716e4e483431772f4b7342713752493538654f65594144356432444869574c33684863456770504e2b7462524763784450564f614842712f717357424942305537314e44326f6a653138437a626b354f766f4545514e365263316d3077457363774e55436b4162707054654634644748754c38594b526770656f564f36422f6a6331683246425357426961644275657472435130677952796e4e4259526c376b4a55554c6f7450335655425756374b755a7a34413861524d4b546e5a6d2f586439504a396977376c336e625253315a4774713230484f49714d3532467649526f51754e6b3463436b3244545a554f6e7544305764794d4356697838656c4e496f4d3932757969456c63456b726e6d377162794578555a556b2b77325359576c4e746a537967327838436d324153615752533132475735325542696232714f6c684249595153344558705355596e6c2f72794b36367277476b716c644f343258474241766f72567459534f344e3563654b496f594c4d696c367661677856537652454c2f73563270625941642b6f4476302f6e5a73556735775563366d48466764494a3868415269574d3246304b61516f556237634555396851466872736f2f43696569486f6e72584d303455374851496178514e7a774b476c68676d4e414a7471716a417450424f71454d5978556a6d6e57744c3159646c755559516d6c43474e6a483072773962364a434b2b7432786f5162736865776e5a55744b6141587550626a72546d584463555a346e41593432553873534459544e33742f756b71442f58496c74646c6339466d712f334c644c594267386a57325433744b35434b4a69686d55465531716d6f4d325235374a6c626c30326174586b6c6c4e5559414136656d36556b4d362f6e4f5661487773316d6564346b764265485954316c45435237784771345765566a535261466a754d45436251677159786d70795a6b4b747155675836727a6e564c767832316f7444304638464c796a596639555a6174524f7a687557416a39554146416175726b50524e747659533958424138453865627972354257706d70464f30424c6d69663439366b4f37624f6d47367934495a4f716362354b6c6f613831596b4174587252337937757370455232687947673673694f33797a6f77514943527751353549597143397a3167716f494f5967513072706a656e454d4d7964647644537a6a755a4a7330724a2b6b5167693978414356535969626d656f70666f664939746d62715153725848756f5630504339543175564d4d516e4f476e4a64786c41494d4b5152747569627265427070447435612f4365746b2f534f314f497542512b554279706c51713276397669754157372f747763457072513337773869756441475541365732356e79694f41614d645a366b4d5431774e6b632f4b43343246777a43497847344f564f3468302f446f6768634e313572696d446f514441524c45425a366e6f486e56676668744e33644771705475696167663878717347576764384937694651684d427a6a47354b2f39504d4342712b7439382f6366384646435173756675423958387a3054436f55647538576b6e754346677a72346a7173575657516a7236745463757a32792f2b357133456a6878716b5342685a6b306254614e3476346b39656a484e303745714a684950765774702f32763676726c4e62737a4754637367776534332f334c72484d562b2b2b71565837582f612f3262375152736648373435632f534d4f6a382f747a70394f727a5a6352322f684d645737776b386e6a514c4e6c39664c4b75494b514a5076317750627072686e702b6230764230654e6d5a64546b38506233657a7462666e5a362b32394b47496e71666d306a344446387644346877636746543849767a35757a30644c6462342f6b4d73553750686c3836347939507a3971414d4f6c646936663432646e5a6c57416e3638335a326274447565766b4c2b6941762f71625578693841377947523967687648767542355432416457764d45316f2f424b6d667a356754365542564b527a4242687541666b376741494a34632f587a6a365041307268734f6c57727358307778494b6c594a4570322b2b624731344d6b4c5a54416c322f4f616b50523441523063415964377050336945523145504d6e38624d4f784d504c7343306237412b70304566446b364b71454651434d41636c476a42354e514737436c6767336735664273314e454d7642683241643932317232472f6e4f7830654868484e5143484c595338516251484936473139327747413362586a6f616451477848397a736e2b466f644c67304f666c724e477741523375414977513865547361646c537a447a6a73416972577851676d5771505273443663764c65417346426270594f52414c7a385042792b365155456c513648585543706867484f35546363645a4339476b4246414c596c58414e2b67666544397333554869444d3367504541654831743948686f4e67434b732b48415a2f4434556872543330464550783041455a4d766f314750655551417236786f756774614c316c5a584d4e57434c416c355a7958675530726f624469355042384f74687641625130595a6f3552616c624143726b7776776d7462345142747062634442384f31652b523343694f543532376c307544574173487a582f5465416f54556544713561485945323048592f34756a6a7757425051736b5a444c546e685051567443642f44515a6a52394d47304671324d7545524152336c737a6277576c346a414c63624f41536f587730474638355833704e685432444b57502f7935545073712b3443347630386c36344249646831584c3471496337346c6a67393578774641532f575579396167466f4443446d66447251575356322b4b7145596374316236514b67686b426d48364144674e4865616c304a3932585259613766653237635347694f4e61304e364347674969574a4b512b3063696645433556714c77417438496a443139685349356c32434642754a4e5249634b567072534e6b6f476d76417372616f502f5132512b6f43554161786c547a646e6544416446494731445458746751414c556a67424e74432b6931415455425346695a554533626364424c77497344456d72392f77477141376a7a4c484d394b556b632f434670393139453967466c4d745a6a614e4675376d7541736a5a6541354979324f775741655653614e4c305a46707570516a7765726b4c364e68346f3746547245586b3434414541546e2b6173593278333246343732744c616a4838676c4a6577454a47627455396d537641796a334137707251476e426d6d7668426c434b6d4f77316c305a4741524a733577633273376658765a492b5933622b516b4a343767384c39784e4d77533971525a6d3975394177596e6755706c4f634d41793363617a44512b74734155384a666f54687a6f5947767579394e75415a39445a6654566971355848773246774a39303374613072727336667a5545632f7a7038542b702f32702f312f742f38412b724a44466c344139755541414141415355564f524b35435949495c783364273b76617220693d5b2764696d675f3137275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220733d27646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414841414141427743414d414141447850675235414141416231424d5645575a4141442f2f2f2b574141435a45684c7a354f535341414474323976713164585772713743686f61765656586e304e432f666e372b2b2f763538764b705330754e414143694f546e66774d446275626d5a4367717153456a323675713965586e52704b53474141446b794d6969516b4b36635847684e44544a6c4a537457566d644b69716149534764485232775947433161576b305454574c4141414376306c455156526f6765325759584f6a4942434764654e714c4e56456a4d6247746b6d622f502f665747424230574454752f4e6d626d37322f59514c374f4d43757842464c42614c78574b7857437757693856697356697366307962425230696d4a73414944544a39335a59374848434e416d7150554c547a6d785a2f626f787944642f5576734f6f7a63784759386859426d486c654d755945324f326b6c5654477744454d36547765304b77446a756c5074714f756e7369466a2f45664170334b465763427068584476484d4c5758693843796c764a6b42376c6d6a7057553076374f53526c623636626f34564c4c4f7445687044546e772f4a793353316f5746624c4c53774278517675477a744f4e63316135684168496e6b734b745838644f76566f65725a622f562f584d6c794a4d2b5971625a384a64767a486b4d42456a446252324158554a30736175626143566a677a767549552f32424769695177685947434f5a487a3065374a6148776667324975563331616744754f7a4a56777a2b30384c7761454b357545776367374f6959644b5a667539726962774450433043374f3855594964684e31716b495a6933376878466d716954495759537471545233533270505462735a675743585761556943724f2f6a34415267436d524870427331476d4250514b364c5978504f414b6a4379574f536b57546e506c6a6f4f5036774e46716765393533746b7a47736533385a53717543516470452f5535366634694659425469534e4a77654547316d504a6b4d6b7267394d71617734594252524b6c494e754d4c6177474a3767436b514b52564e5257763159566746574769565a646f3177385867674e434d4662746243316a6365715871416a6834475a63557831586f56774d3264374e486f4574464e64643872674c6366514e307165694b30393848326c5455536668546f436f6a376a355556364f37783754563359657a797733785252394f6f63786a4b756f455656507339625230487872506653704552754e4b49635357526a6171535365777949536f76542b2b6e49526f615843717468636f46585553536945536370514963566f737037506e556f6f42612b4c4e39743830543242547354334d58306a46556f687a59425943706a3751383675416b626b5664524a4f6763464831426f52556970576438446c434a736b3955585076706c56657341334d5a695478747939535772324749552f4a666a794a754a4d49665053684f453734476d4a78324b78574377576938566973566773466f7646597630332b67492b6453615a504562466d6741414141424a52553545726b4a6767675c7833645c783364273b76617220693d5b2764696d675f3139275d3b5f736574496d6167657353726328692c73293b7d2928293b2866756e6374696f6e28297b76617220653d27704d524f586250624472505339414f426d4b50594251273b2866756e6374696f6e28297b76617220613d652c623d77696e646f772e706572666f726d616e6365262677696e646f772e706572666f726d616e63652e6e617669676174696f6e3b622626323d3d622e74797065262677696e646f772e70696e6728222f67656e5f3230343f63743d6261636b627574746f6e2665693d222b61293b7d292e63616c6c2874686973293b7d2928293b2866756e6374696f6e28297b2866756e6374696f6e28297b676f6f676c652e637363743d7b7d3b676f6f676c652e637363742e70733d27414f765661773144704144365477704f5055674e344e6f72467065555c7832367573745c78336431353635353239363336323834373838273b7d2928293b7d2928293b2866756e6374696f6e28297b2866756e6374696f6e28297b676f6f676c652e637363742e72643d747275653b7d2928293b7d2928293b2866756e6374696f6e28297b77696e646f772e78703d66756e6374696f6e2861297b66756e6374696f6e206528682c662c67297b72657475726e227870222b282278223d3d663f2263223a227822292b677d666f722876617220623d2f5c62787028787c6329285c643f295c622f3b613b297b76617220633d612e636c6173734e616d652c643d632e6d617463682862293b69662864297b612e636c6173734e616d653d632e7265706c61636528622c65293b6966282263223d3d645b315d29666f7228613d612e676574456c656d656e747342795461674e616d652822696d6722292c623d303b623c612e6c656e6774683b2b2b6229696628633d615b625d2c643d632e6765744174747269627574652822646174612d6c6c222929632e7372633d642c632e72656d6f76654174747269627574652822646174612d6c6c22293b627265616b7d613d612e706172656e74456c656d656e747d7d3b7d2928293b676f6f676c652e647274792626676f6f676c652e6472747928293b3c2f7363726970743e3c2f626f64793e3c2f68746d6c3e", + "rawHeaders": [ + "Content-Type", + "text/html; charset=ISO-8859-1", + "Date", + "Sat, 10 Aug 2019 01:21:31 GMT", + "Expires", + "-1", + "Cache-Control", + "private, max-age=0", + "P3P", + "CP=\"This is not a P3P policy! See g.co/p3phelp for more info.\"", + "Server", + "gws", + "X-XSS-Protection", + "0", + "X-Frame-Options", + "SAMEORIGIN", + "Set-Cookie", + "1P_JAR=2019-08-10-01; expires=Mon, 09-Sep-2019 01:21:31 GMT; path=/; domain=.google.com", + "Set-Cookie", + "CGIC=IiFhcHBsaWNhdGlvbi9qc29uLCB0ZXh0L3BsYWluLCAqLyo; expires=Thu, 06-Feb-2020 01:21:31 GMT; path=/complete/search; domain=.google.com; HttpOnly", + "Set-Cookie", + "CGIC=IiFhcHBsaWNhdGlvbi9qc29uLCB0ZXh0L3BsYWluLCAqLyo; expires=Thu, 06-Feb-2020 01:21:31 GMT; path=/search; domain=.google.com; HttpOnly", + "Set-Cookie", + "NID=188=vTMutucOBO-Yl5bpVtVnzkN1voOukQ24RkD0wuuzeNL_BDPMEB90MqBF06HFaILh_fs-PO8JGLhIjkSb3nxl9Rzf8L7CxJtk_yJF0aEgi2znY0rMT_dQr6_5tYfVNKU9u0d2BoXOVOWHEN3ZzaD7q6yRUb44yH3vjL0kue6Ki0s; expires=Sun, 09-Feb-2020 01:21:31 GMT; path=/; domain=.google.com; HttpOnly", + "Accept-Ranges", + "none", + "Vary", + "Accept-Encoding", + "Connection", + "close" + ], + "responseIsBinary": true + } +] diff --git a/packages/opentelemetry-instrumentation-http/test/fixtures/server-cert.pem b/packages/opentelemetry-instrumentation-http/test/fixtures/server-cert.pem new file mode 100644 index 0000000000..e2b79024da --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/test/fixtures/server-cert.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBqzCCARQCCQDLcUeJsLDL5jANBgkqhkiG9w0BAQUFADAaMQswCQYDVQQGEwJD +QTELMAkGA1UECAwCUUMwHhcNMTkwOTI5MjIwMDI2WhcNMTkxMDI5MjIwMDI2WjAa +MQswCQYDVQQGEwJDQTELMAkGA1UECAwCUUMwgZ8wDQYJKoZIhvcNAQEBBQADgY0A +MIGJAoGBALhfi1dwIyC1Jha4N/j/VtlPPi+j+SZQGZqLNVVgzzGY7+cc3VkCySZD +yXh3Z+/ftp9DDKdHRutJQE0R4peSDussC/IQDJKzuKN/O9S6tnNlgUr5YZLRENxL +FSJIY5cIkty50IrEhlN5QeDJP8p4yrYq9J6M0yzyfdqIWI3CBqbzAgMBAAEwDQYJ +KoZIhvcNAQEFBQADgYEArnOeXmXXJTK39Ma25elHxlYUZiYOBu/truy5zmx4umyS +GyehAv+jRIanoCRWtOBnrjS5CY/6cC64aIVLMoqXEFIL7q/GD0wEM/DS8rN7KTcp +w+nIX98srYaAFeQZScPioS6WpXz5AjbTVhvAwkIm2/s6dOlX31+1zu6Zu6ASSuQ= +-----END CERTIFICATE----- diff --git a/packages/opentelemetry-instrumentation-http/test/fixtures/server-key.pem b/packages/opentelemetry-instrumentation-http/test/fixtures/server-key.pem new file mode 100644 index 0000000000..405c5fa0d7 --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/test/fixtures/server-key.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQC4X4tXcCMgtSYWuDf4/1bZTz4vo/kmUBmaizVVYM8xmO/nHN1Z +AskmQ8l4d2fv37afQwynR0brSUBNEeKXkg7rLAvyEAySs7ijfzvUurZzZYFK+WGS +0RDcSxUiSGOXCJLcudCKxIZTeUHgyT/KeMq2KvSejNMs8n3aiFiNwgam8wIDAQAB +AoGBAKBztcYQduGeBFm9VCjDvgc8KTg4kTlAeCfAglec+nOFTzJoMlGmVPuR/qFx ++OgOXtXW+goRw6w7gVQQ/os9tvCCp7awSC5UCfPejHh6bW2B0BF2lZJ6B9y+u5Fa +/p8oKoJGcC4eagVnDojuoYJHSqWBf7d7V/U54NpxwgBTsHAhAkEA8PJROgWzjMl2 +Gs5j8oBldEqzrC/d4K1uMEvCTb4RJ+t6jWq+Ug/vqvCfIcLfxHbOmTbOHTfhpv/d +NUf9eDyBGwJBAMPkZaHP5vPDd900MqypLVasollzxgPnMUg35EEQJLAbb/5xG3X9 +ZbaVDTRtLQYNFvDZLlTpRpCPxZCgrn9hJwkCQQDPEVChLrkpqxFm5CydAZ8vG+vh +dJmYNzPVKaZorYmM5yBBXJUHbU6pd3UqzJEGBJx0q9bi4V156bYvzhiVNlo1AkBu +1hbvFCwPtoRmg3c8nEhL50fApzHd2XzX6M/cRF8Nyah3ZdXsz6AyS2l6RV+ZMeTO +B4QghRDpEH/vUgsJhZXJAkB5GQZPJh6/kozc5+Ffc60ThN/58SX0KEFeKnWRlzfr +vfBXwcmaz1oNXN+kcWdLnKbr/tx+3UQ6weRRmeYX/hOi +-----END RSA PRIVATE KEY----- diff --git a/packages/opentelemetry-instrumentation-http/test/functionals/http-disable.test.ts b/packages/opentelemetry-instrumentation-http/test/functionals/http-disable.test.ts new file mode 100644 index 0000000000..7a962a56e3 --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/test/functionals/http-disable.test.ts @@ -0,0 +1,91 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { NoopTracerProvider, NOOP_TRACER } from '@opentelemetry/api'; +import { NoopLogger } from '@opentelemetry/core'; +import * as assert from 'assert'; +import { HttpInstrumentation } from '../../src/http'; +import { AddressInfo } from 'net'; +import * as nock from 'nock'; +import * as sinon from 'sinon'; +import { httpRequest } from '../utils/httpRequest'; +import { isWrapped } from '@opentelemetry/instrumentation'; + +const logger = new NoopLogger(); +const instrumentation = new HttpInstrumentation({ logger }); +instrumentation.enable(); +instrumentation.disable(); + +import * as http from 'http'; + +describe('HttpInstrumentation', () => { + let server: http.Server; + let serverPort = 0; + + describe('disable()', () => { + const provider = new NoopTracerProvider(); + before(() => { + nock.cleanAll(); + nock.enableNetConnect(); + instrumentation.enable(); + assert.strictEqual(isWrapped(http.Server.prototype.emit), true); + instrumentation.setTracerProvider(provider); + + server = http.createServer((request, response) => { + response.end('Test Server Response'); + }); + + server.listen(serverPort); + server.once('listening', () => { + serverPort = (server.address() as AddressInfo).port; + }); + }); + + beforeEach(() => { + NOOP_TRACER.startSpan = sinon.spy(); + NOOP_TRACER.withSpan = sinon.spy(); + }); + + afterEach(() => { + sinon.restore(); + }); + + after(() => { + server.close(); + }); + describe('unpatch()', () => { + it('should not call provider methods for creating span', async () => { + instrumentation.disable(); + assert.strictEqual(isWrapped(http.Server.prototype.emit), false); + + const testPath = '/incoming/unpatch/'; + + const options = { host: 'localhost', path: testPath, port: serverPort }; + + await httpRequest.get(options).then(result => { + assert.strictEqual( + (NOOP_TRACER.startSpan as sinon.SinonSpy).called, + false + ); + + assert.strictEqual( + (NOOP_TRACER.withSpan as sinon.SinonSpy).called, + false + ); + }); + }); + }); + }); +}); diff --git a/packages/opentelemetry-instrumentation-http/test/functionals/http-enable.test.ts b/packages/opentelemetry-instrumentation-http/test/functionals/http-enable.test.ts new file mode 100644 index 0000000000..73d37ea79f --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/test/functionals/http-enable.test.ts @@ -0,0 +1,827 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + StatusCode, + context, + propagation, + Span as ISpan, + SpanKind, + getActiveSpan, +} from '@opentelemetry/api'; +import { NoopLogger } from '@opentelemetry/core'; +import { NodeTracerProvider } from '@opentelemetry/node'; +import { + InMemorySpanExporter, + SimpleSpanProcessor, +} from '@opentelemetry/tracing'; +import { + HttpAttribute, + GeneralAttribute, +} from '@opentelemetry/semantic-conventions'; +import * as assert from 'assert'; +import * as nock from 'nock'; +import * as path from 'path'; +import { HttpInstrumentation } from '../../src/http'; +import { HttpInstrumentationConfig } from '../../src/types'; +import { assertSpan } from '../utils/assertSpan'; +import { DummyPropagation } from '../utils/DummyPropagation'; +import { httpRequest } from '../utils/httpRequest'; +import { ContextManager } from '@opentelemetry/context-base'; +import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; +import type { ClientRequest, IncomingMessage, ServerResponse } from 'http'; +import { isWrapped } from '@opentelemetry/instrumentation'; + +const logger = new NoopLogger(); +const instrumentation = new HttpInstrumentation({ logger }); +instrumentation.enable(); +instrumentation.disable(); + +import * as http from 'http'; + +const applyCustomAttributesOnSpanErrorMessage = + 'bad applyCustomAttributesOnSpan function'; + +let server: http.Server; +const serverPort = 22345; +const protocol = 'http'; +const hostname = 'localhost'; +const pathname = '/test'; +const serverName = 'my.server.name'; +const memoryExporter = new InMemorySpanExporter(); +const provider = new NodeTracerProvider({ + logger, +}); +provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); +instrumentation.setTracerProvider(provider); +propagation.setGlobalPropagator(new DummyPropagation()); + +function doNock( + hostname: string, + path: string, + httpCode: number, + respBody: string, + times?: number +) { + const i = times || 1; + nock(`${protocol}://${hostname}`) + .get(path) + .times(i) + .reply(httpCode, respBody); +} + +export const customAttributeFunction = (span: ISpan): void => { + span.setAttribute('span kind', SpanKind.CLIENT); +}; + +export const requestHookFunction = ( + span: ISpan, + request: ClientRequest | IncomingMessage +): void => { + span.setAttribute('custom request hook attribute', 'request'); +}; + +export const responseHookFunction = ( + span: ISpan, + response: IncomingMessage | ServerResponse +): void => { + span.setAttribute('custom response hook attribute', 'response'); +}; + +describe('HttpInstrumentation', () => { + let contextManager: ContextManager; + + beforeEach(() => { + contextManager = new AsyncHooksContextManager().enable(); + context.setGlobalContextManager(contextManager); + }); + + afterEach(() => { + context.disable(); + }); + + describe('enable()', () => { + describe('with bad instrumentation options', () => { + beforeEach(() => { + memoryExporter.reset(); + }); + + before(() => { + const config: HttpInstrumentationConfig = { + ignoreIncomingPaths: [ + (url: string) => { + throw new Error('bad ignoreIncomingPaths function'); + }, + ], + ignoreOutgoingUrls: [ + (url: string) => { + throw new Error('bad ignoreOutgoingUrls function'); + }, + ], + applyCustomAttributesOnSpan: () => { + throw new Error(applyCustomAttributesOnSpanErrorMessage); + }, + }; + instrumentation.setConfig(config); + instrumentation.enable(); + server = http.createServer((request, response) => { + response.end('Test Server Response'); + }); + + server.listen(serverPort); + }); + + after(() => { + server.close(); + instrumentation.disable(); + }); + + it('should generate valid spans (client side and server side)', async () => { + const result = await httpRequest.get( + `${protocol}://${hostname}:${serverPort}${pathname}` + ); + const spans = memoryExporter.getFinishedSpans(); + const [incomingSpan, outgoingSpan] = spans; + const validations = { + hostname, + httpStatusCode: result.statusCode!, + httpMethod: result.method!, + pathname, + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + component: 'http', + }; + + assert.strictEqual(spans.length, 2); + assertSpan(incomingSpan, SpanKind.SERVER, validations); + assertSpan(outgoingSpan, SpanKind.CLIENT, validations); + assert.strictEqual( + incomingSpan.attributes[GeneralAttribute.NET_HOST_PORT], + serverPort + ); + assert.strictEqual( + outgoingSpan.attributes[GeneralAttribute.NET_PEER_PORT], + serverPort + ); + }); + }); + describe('with good instrumentation options', () => { + beforeEach(() => { + memoryExporter.reset(); + }); + + before(() => { + instrumentation.setConfig({ + ignoreIncomingPaths: [ + '/ignored/string', + /\/ignored\/regexp$/i, + (url: string) => url.endsWith('/ignored/function'), + ], + ignoreOutgoingUrls: [ + `${protocol}://${hostname}:${serverPort}/ignored/string`, + /\/ignored\/regexp$/i, + (url: string) => url.endsWith('/ignored/function'), + ], + applyCustomAttributesOnSpan: customAttributeFunction, + requestHook: requestHookFunction, + responseHook: responseHookFunction, + serverName, + }); + instrumentation.enable(); + server = http.createServer((request, response) => { + response.end('Test Server Response'); + }); + + server.listen(serverPort); + }); + + after(() => { + server.close(); + instrumentation.disable(); + }); + + it(`${protocol} module should be patched`, () => { + assert.strictEqual(isWrapped(http.Server.prototype.emit), true); + }); + + it('should generate valid spans (client side and server side)', async () => { + const result = await httpRequest.get( + `${protocol}://${hostname}:${serverPort}${pathname}`, + { + headers: { + 'x-forwarded-for': ', , ', + 'user-agent': 'chrome', + }, + } + ); + const spans = memoryExporter.getFinishedSpans(); + const [incomingSpan, outgoingSpan] = spans; + const validations = { + hostname, + httpStatusCode: result.statusCode!, + httpMethod: result.method!, + pathname, + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + component: 'http', + serverName, + }; + + assert.strictEqual(spans.length, 2); + assert.strictEqual( + incomingSpan.attributes[HttpAttribute.HTTP_CLIENT_IP], + '' + ); + assert.strictEqual( + incomingSpan.attributes[GeneralAttribute.NET_HOST_PORT], + serverPort + ); + assert.strictEqual( + outgoingSpan.attributes[GeneralAttribute.NET_PEER_PORT], + serverPort + ); + [ + { span: incomingSpan, kind: SpanKind.SERVER }, + { span: outgoingSpan, kind: SpanKind.CLIENT }, + ].forEach(({ span, kind }) => { + assert.strictEqual(span.attributes[HttpAttribute.HTTP_FLAVOR], '1.1'); + assert.strictEqual( + span.attributes[GeneralAttribute.NET_TRANSPORT], + GeneralAttribute.IP_TCP + ); + assertSpan(span, kind, validations); + }); + }); + + const httpErrorCodes = [ + 400, + 401, + 403, + 404, + 429, + 501, + 503, + 504, + 500, + 505, + 597, + ]; + + for (let i = 0; i < httpErrorCodes.length; i++) { + it(`should test span for GET requests with http error ${httpErrorCodes[i]}`, async () => { + const testPath = '/outgoing/rootSpan/1'; + + doNock( + hostname, + testPath, + httpErrorCodes[i], + httpErrorCodes[i].toString() + ); + + const isReset = memoryExporter.getFinishedSpans().length === 0; + assert.ok(isReset); + + const result = await httpRequest.get( + `${protocol}://${hostname}${testPath}` + ); + const spans = memoryExporter.getFinishedSpans(); + const reqSpan = spans[0]; + + assert.strictEqual(result.data, httpErrorCodes[i].toString()); + assert.strictEqual(spans.length, 1); + + const validations = { + hostname, + httpStatusCode: result.statusCode!, + httpMethod: 'GET', + pathname: testPath, + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + component: 'http', + }; + + assertSpan(reqSpan, SpanKind.CLIENT, validations); + }); + } + + it('should create a child span for GET requests', async () => { + const testPath = '/outgoing/rootSpan/childs/1'; + doNock(hostname, testPath, 200, 'Ok'); + const name = 'TestRootSpan'; + const span = provider.getTracer('default').startSpan(name); + return provider.getTracer('default').withSpan(span, async () => { + const result = await httpRequest.get( + `${protocol}://${hostname}${testPath}` + ); + span.end(); + const spans = memoryExporter.getFinishedSpans(); + const [reqSpan, localSpan] = spans; + const validations = { + hostname, + httpStatusCode: result.statusCode!, + httpMethod: 'GET', + pathname: testPath, + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + component: 'http', + }; + + assert.ok(localSpan.name.indexOf('TestRootSpan') >= 0); + assert.strictEqual(spans.length, 2); + assert.strictEqual(reqSpan.name, 'HTTP GET'); + assert.strictEqual( + localSpan.spanContext.traceId, + reqSpan.spanContext.traceId + ); + assertSpan(reqSpan, SpanKind.CLIENT, validations); + assert.notStrictEqual( + localSpan.spanContext.spanId, + reqSpan.spanContext.spanId + ); + }); + }); + + for (let i = 0; i < httpErrorCodes.length; i++) { + it(`should test child spans for GET requests with http error ${httpErrorCodes[i]}`, async () => { + const testPath = '/outgoing/rootSpan/childs/1'; + doNock( + hostname, + testPath, + httpErrorCodes[i], + httpErrorCodes[i].toString() + ); + const name = 'TestRootSpan'; + const span = provider.getTracer('default').startSpan(name); + return provider.getTracer('default').withSpan(span, async () => { + const result = await httpRequest.get( + `${protocol}://${hostname}${testPath}` + ); + span.end(); + const spans = memoryExporter.getFinishedSpans(); + const [reqSpan, localSpan] = spans; + const validations = { + hostname, + httpStatusCode: result.statusCode!, + httpMethod: 'GET', + pathname: testPath, + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + component: 'http', + }; + + assert.ok(localSpan.name.indexOf('TestRootSpan') >= 0); + assert.strictEqual(spans.length, 2); + assert.strictEqual(reqSpan.name, 'HTTP GET'); + assert.strictEqual( + localSpan.spanContext.traceId, + reqSpan.spanContext.traceId + ); + assertSpan(reqSpan, SpanKind.CLIENT, validations); + assert.notStrictEqual( + localSpan.spanContext.spanId, + reqSpan.spanContext.spanId + ); + }); + }); + } + + it('should create multiple child spans for GET requests', async () => { + const testPath = '/outgoing/rootSpan/childs'; + const num = 5; + doNock(hostname, testPath, 200, 'Ok', num); + const name = 'TestRootSpan'; + const span = provider.getTracer('default').startSpan(name); + await provider.getTracer('default').withSpan(span, async () => { + for (let i = 0; i < num; i++) { + await httpRequest.get(`${protocol}://${hostname}${testPath}`); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans[i].name, 'HTTP GET'); + assert.strictEqual( + span.context().traceId, + spans[i].spanContext.traceId + ); + } + span.end(); + const spans = memoryExporter.getFinishedSpans(); + // 5 child spans ended + 1 span (root) + assert.strictEqual(spans.length, 6); + }); + }); + + for (const ignored of ['string', 'function', 'regexp']) { + it(`should not trace ignored requests (client and server side) with type ${ignored}`, async () => { + const testPath = `/ignored/${ignored}`; + + await httpRequest.get( + `${protocol}://${hostname}:${serverPort}${testPath}` + ); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + }); + } + + for (const arg of ['string', {}, new Date()]) { + it(`should be tracable and not throw exception in ${protocol} instrumentation when passing the following argument ${JSON.stringify( + arg + )}`, async () => { + try { + await httpRequest.get(arg); + } catch (error) { + // request has been made + // nock throw + assert.ok(error.message.startsWith('Nock: No match for request')); + } + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + }); + } + + for (const arg of [true, 1, false, 0, '']) { + it(`should not throw exception in ${protocol} instrumentation when passing the following argument ${JSON.stringify( + arg + )}`, async () => { + try { + await httpRequest.get(arg as any); + } catch (error) { + // request has been made + // nock throw + assert.ok( + error.stack.indexOf( + path.normalize('/node_modules/nock/lib/intercept.js') + ) > 0 + ); + } + const spans = memoryExporter.getFinishedSpans(); + // for this arg with don't provide trace. We pass arg to original method (http.get) + assert.strictEqual(spans.length, 0); + }); + } + + it('should have 1 ended span when request throw on bad "options" object', () => { + try { + http.request({ protocol: 'telnet' }); + } catch (error) { + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + } + }); + + it('should have 1 ended span when response.end throw an exception', async () => { + const testPath = '/outgoing/rootSpan/childs/1'; + doNock(hostname, testPath, 400, 'Not Ok'); + + const promiseRequest = new Promise((resolve, reject) => { + const req = http.request( + `${protocol}://${hostname}${testPath}`, + (resp: http.IncomingMessage) => { + let data = ''; + resp.on('data', chunk => { + data += chunk; + }); + resp.on('end', () => { + reject(new Error(data)); + }); + } + ); + return req.end(); + }); + + try { + await promiseRequest; + assert.fail(); + } catch (error) { + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + } + }); + + it('should have 1 ended span when request throw on bad "options" object', () => { + nock.cleanAll(); + nock.enableNetConnect(); + try { + http.request({ protocol: 'telnet' }); + assert.fail(); + } catch (error) { + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + } + }); + + it('should have 1 ended span when response.end throw an exception', async () => { + const testPath = '/outgoing/rootSpan/childs/1'; + doNock(hostname, testPath, 400, 'Not Ok'); + + const promiseRequest = new Promise((resolve, reject) => { + const req = http.request( + `${protocol}://${hostname}${testPath}`, + (resp: http.IncomingMessage) => { + let data = ''; + resp.on('data', chunk => { + data += chunk; + }); + resp.on('end', () => { + reject(new Error(data)); + }); + } + ); + return req.end(); + }); + + try { + await promiseRequest; + assert.fail(); + } catch (error) { + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + } + }); + + it('should have 1 ended span when request is aborted', async () => { + nock(`${protocol}://my.server.com`) + .get('/') + .socketDelay(50) + .reply(200, ''); + + const promiseRequest = new Promise((resolve, reject) => { + const req = http.request( + `${protocol}://my.server.com`, + (resp: http.IncomingMessage) => { + let data = ''; + resp.on('data', chunk => { + data += chunk; + }); + resp.on('end', () => { + resolve(data); + }); + } + ); + req.setTimeout(10, () => { + req.abort(); + reject('timeout'); + }); + return req.end(); + }); + + try { + await promiseRequest; + assert.fail(); + } catch (error) { + const spans = memoryExporter.getFinishedSpans(); + const [span] = spans; + assert.strictEqual(spans.length, 1); + assert.strictEqual(span.status.code, StatusCode.ERROR); + assert.ok(Object.keys(span.attributes).length >= 6); + } + }); + + it('should have 1 ended span when request is aborted after receiving response', async () => { + nock(`${protocol}://my.server.com`) + .get('/') + .delay({ + body: 50, + }) + .replyWithFile(200, `${process.cwd()}/package.json`); + + const promiseRequest = new Promise((resolve, reject) => { + const req = http.request( + `${protocol}://my.server.com`, + (resp: http.IncomingMessage) => { + let data = ''; + resp.on('data', chunk => { + req.abort(); + data += chunk; + }); + resp.on('end', () => { + resolve(data); + }); + } + ); + + return req.end(); + }); + + try { + await promiseRequest; + assert.fail(); + } catch (error) { + const spans = memoryExporter.getFinishedSpans(); + const [span] = spans; + assert.strictEqual(spans.length, 1); + assert.strictEqual(span.status.code, StatusCode.ERROR); + assert.ok(Object.keys(span.attributes).length > 7); + } + }); + + it("should have 1 ended span when request doesn't listening response", done => { + nock.cleanAll(); + nock.enableNetConnect(); + const req = http.request(`${protocol}://${hostname}/`); + req.on('close', () => { + const spans = memoryExporter.getFinishedSpans(); + const [span] = spans; + assert.strictEqual(spans.length, 1); + assert.ok(Object.keys(span.attributes).length > 6); + done(); + }); + req.end(); + }); + + it("should have 1 ended span when response is listened by using req.on('response')", done => { + const host = `${protocol}://${hostname}`; + nock(host).get('/').reply(404); + const req = http.request(`${host}/`); + req.on('response', response => { + response.on('data', () => {}); + response.on('end', () => { + const spans = memoryExporter.getFinishedSpans(); + const [span] = spans; + assert.strictEqual(spans.length, 1); + assert.ok(Object.keys(span.attributes).length > 6); + assert.strictEqual( + span.attributes[HttpAttribute.HTTP_STATUS_CODE], + 404 + ); + assert.strictEqual(span.status.code, StatusCode.ERROR); + done(); + }); + }); + req.end(); + }); + + it('custom attributes should show up on client and server spans', async () => { + await httpRequest.get( + `${protocol}://${hostname}:${serverPort}${pathname}` + ); + const spans = memoryExporter.getFinishedSpans(); + const [incomingSpan, outgoingSpan] = spans; + + assert.strictEqual( + incomingSpan.attributes['custom request hook attribute'], + 'request' + ); + assert.strictEqual( + incomingSpan.attributes['custom response hook attribute'], + 'response' + ); + assert.strictEqual( + incomingSpan.attributes['span kind'], + SpanKind.CLIENT + ); + + assert.strictEqual( + outgoingSpan.attributes['custom request hook attribute'], + 'request' + ); + assert.strictEqual( + outgoingSpan.attributes['custom response hook attribute'], + 'response' + ); + assert.strictEqual( + outgoingSpan.attributes['span kind'], + SpanKind.CLIENT + ); + }); + + it('should not set span as active in context for outgoing request', done => { + assert.deepStrictEqual(getActiveSpan(context.active()), undefined); + http.get(`${protocol}://${hostname}:${serverPort}/test`, res => { + assert.deepStrictEqual(getActiveSpan(context.active()), undefined); + done(); + }); + }); + }); + + describe('with require parent span', () => { + beforeEach(done => { + memoryExporter.reset(); + instrumentation.setConfig({}); + instrumentation.enable(); + server = http.createServer((request, response) => { + response.end('Test Server Response'); + }); + server.listen(serverPort, done); + }); + + afterEach(() => { + server.close(); + instrumentation.disable(); + }); + + it('should not trace without parent with options enabled (both client & server)', async () => { + instrumentation.disable(); + instrumentation.setConfig({ + requireParentforIncomingSpans: true, + requireParentforOutgoingSpans: true, + }); + instrumentation.enable(); + const testPath = '/test/test'; + await httpRequest.get( + `${protocol}://${hostname}:${serverPort}${testPath}` + ); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + }); + + it('should not trace without parent with options enabled (client only)', async () => { + instrumentation.disable(); + instrumentation.setConfig({ + requireParentforOutgoingSpans: true, + }); + instrumentation.enable(); + const testPath = '/test/test'; + const result = await httpRequest.get( + `${protocol}://${hostname}:${serverPort}${testPath}` + ); + assert( + result.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY] !== undefined + ); + assert( + result.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY] !== undefined + ); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual( + spans.every(span => span.kind === SpanKind.SERVER), + true + ); + }); + + it('should not trace without parent with options enabled (server only)', async () => { + instrumentation.disable(); + instrumentation.setConfig({ + requireParentforIncomingSpans: true, + }); + instrumentation.enable(); + const testPath = '/test/test'; + const result = await httpRequest.get( + `${protocol}://${hostname}:${serverPort}${testPath}` + ); + assert( + result.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY] !== undefined + ); + assert( + result.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY] !== undefined + ); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual( + spans.every(span => span.kind === SpanKind.CLIENT), + true + ); + }); + + it('should trace with parent with both requireParent options enabled', done => { + instrumentation.disable(); + instrumentation.setConfig({ + requireParentforIncomingSpans: true, + requireParentforOutgoingSpans: true, + }); + instrumentation.enable(); + const testPath = '/test/test'; + const tracer = provider.getTracer('default'); + const span = tracer.startSpan('parentSpan', { + kind: SpanKind.INTERNAL, + }); + tracer.withSpan(span, () => { + httpRequest + .get(`${protocol}://${hostname}:${serverPort}${testPath}`) + .then(result => { + span.end(); + assert( + result.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY] !== + undefined + ); + assert( + result.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY] !== + undefined + ); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 2); + assert.strictEqual( + spans.filter(span => span.kind === SpanKind.CLIENT).length, + 1 + ); + assert.strictEqual( + spans.filter(span => span.kind === SpanKind.INTERNAL).length, + 1 + ); + return done(); + }) + .catch(done); + }); + }); + }); + }); +}); diff --git a/packages/opentelemetry-instrumentation-http/test/functionals/http-package.test.ts b/packages/opentelemetry-instrumentation-http/test/functionals/http-package.test.ts new file mode 100644 index 0000000000..44f6468ec5 --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/test/functionals/http-package.test.ts @@ -0,0 +1,153 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { context, SpanKind, Span, propagation } from '@opentelemetry/api'; +import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; +import { NoopLogger } from '@opentelemetry/core'; +import { NodeTracerProvider } from '@opentelemetry/node'; +import { + InMemorySpanExporter, + SimpleSpanProcessor, +} from '@opentelemetry/tracing'; +import * as assert from 'assert'; +import * as path from 'path'; +import * as url from 'url'; +import { HttpInstrumentation } from '../../src/http'; +import { assertSpan } from '../utils/assertSpan'; +import { DummyPropagation } from '../utils/DummyPropagation'; + +const logger = new NoopLogger(); +const instrumentation = new HttpInstrumentation({ logger }); +instrumentation.enable(); +instrumentation.disable(); + +import * as http from 'http'; +import * as request from 'request-promise-native'; +import * as superagent from 'superagent'; +import * as got from 'got'; +import * as nock from 'nock'; +import axios, { AxiosResponse } from 'axios'; + +const memoryExporter = new InMemorySpanExporter(); +const protocol = 'http'; +const customAttributeFunction = (span: Span): void => { + span.setAttribute('span kind', SpanKind.CLIENT); +}; + +describe('Packages', () => { + beforeEach(() => { + context.setGlobalContextManager(new AsyncHooksContextManager().enable()); + }); + + afterEach(() => { + context.disable(); + }); + describe('get', () => { + const logger = new NoopLogger(); + const provider = new NodeTracerProvider({ + logger, + }); + provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); + instrumentation.setTracerProvider(provider); + propagation.setGlobalPropagator(new DummyPropagation()); + beforeEach(() => { + memoryExporter.reset(); + }); + + before(() => { + instrumentation.setConfig({ + applyCustomAttributesOnSpan: customAttributeFunction, + }); + instrumentation.enable(); + }); + + after(() => { + // back to normal + nock.cleanAll(); + nock.enableNetConnect(); + }); + + let resHeaders: http.IncomingHttpHeaders; + [ + { name: 'axios', httpPackage: axios }, //keep first + { name: 'superagent', httpPackage: superagent }, + { name: 'got', httpPackage: { get: (url: string) => got(url) } }, + { + name: 'request', + httpPackage: { get: (url: string) => request(url) }, + }, + ].forEach(({ name, httpPackage }) => { + it(`should create a span for GET requests and add propagation headers by using ${name} package`, async () => { + if (process.versions.node.startsWith('12') && name === 'got') { + // got complains with nock and node version 12+ + // > RequestError: The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type function + // so let's make a real call + nock.cleanAll(); + nock.enableNetConnect(); + } else { + nock.load(path.join(__dirname, '../', '/fixtures/google-http.json')); + } + + const urlparsed = url.parse( + name === 'got' && process.versions.node.startsWith('12') + ? // there is an issue with got 9.6 version and node 12 when redirecting so url above will not work + // https://github.com/nock/nock/pull/1551 + // https://github.com/sindresorhus/got/commit/bf1aa5492ae2bc78cbbec6b7d764906fb156e6c2#diff-707a4781d57c42085155dcb27edb9ccbR258 + // TODO: check if this is still the case when new version + `${protocol}://info.cern.ch/` + : `${protocol}://www.google.com/search?q=axios&oq=axios&aqs=chrome.0.69i59l2j0l3j69i60.811j0j7&sourceid=chrome&ie=UTF-8` + ); + const result = await httpPackage.get(urlparsed.href!); + if (!resHeaders) { + const res = result as AxiosResponse<{}>; + resHeaders = res.headers; + } + const spans = memoryExporter.getFinishedSpans(); + const span = spans[0]; + const validations = { + hostname: urlparsed.hostname!, + httpStatusCode: 200, + httpMethod: 'GET', + pathname: urlparsed.pathname!, + path: urlparsed.path, + resHeaders, + component: 'http', + }; + + assert.strictEqual(spans.length, 1); + assert.strictEqual(span.name, 'HTTP GET'); + + switch (name) { + case 'axios': + assert.ok( + result.request._headers[DummyPropagation.TRACE_CONTEXT_KEY] + ); + assert.ok( + result.request._headers[DummyPropagation.SPAN_CONTEXT_KEY] + ); + break; + case 'got': + case 'superagent': + break; + default: + break; + } + assert.strictEqual(span.attributes['span kind'], SpanKind.CLIENT); + assertSpan(span, SpanKind.CLIENT, validations); + }); + }); + }); +}); diff --git a/packages/opentelemetry-instrumentation-http/test/functionals/https-disable.test.ts b/packages/opentelemetry-instrumentation-http/test/functionals/https-disable.test.ts new file mode 100644 index 0000000000..f0eb60b38f --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/test/functionals/https-disable.test.ts @@ -0,0 +1,99 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { NoopTracerProvider, NOOP_TRACER } from '@opentelemetry/api'; +import { NoopLogger } from '@opentelemetry/core'; +import * as assert from 'assert'; +import * as fs from 'fs'; +import type { AddressInfo } from 'net'; +import * as nock from 'nock'; +import * as sinon from 'sinon'; +import { HttpInstrumentation } from '../../src'; +import { isWrapped } from '@opentelemetry/instrumentation'; + +const logger = new NoopLogger(); +const instrumentation = new HttpInstrumentation({ logger }); +instrumentation.enable(); +instrumentation.disable(); + +import * as https from 'https'; +import { httpsRequest } from '../utils/httpsRequest'; + +describe('HttpsInstrumentation', () => { + let server: https.Server; + let serverPort = 0; + + describe('disable()', () => { + const provider = new NoopTracerProvider(); + before(() => { + nock.cleanAll(); + nock.enableNetConnect(); + + instrumentation.enable(); + assert.strictEqual(isWrapped(https.Server.prototype.emit), true); + instrumentation.setTracerProvider(provider); + + server = https.createServer( + { + key: fs.readFileSync('test/fixtures/server-key.pem'), + cert: fs.readFileSync('test/fixtures/server-cert.pem'), + }, + (request, response) => { + response.end('Test Server Response'); + } + ); + + server.listen(serverPort); + server.once('listening', () => { + serverPort = (server.address() as AddressInfo).port; + }); + }); + + beforeEach(() => { + NOOP_TRACER.startSpan = sinon.spy(); + NOOP_TRACER.withSpan = sinon.spy(); + }); + + afterEach(() => { + sinon.restore(); + }); + + after(() => { + server.close(); + }); + describe('unpatch()', () => { + it('should not call tracer methods for creating span', async () => { + instrumentation.disable(); + const testPath = '/incoming/unpatch/'; + + const options = { host: 'localhost', path: testPath, port: serverPort }; + + await httpsRequest.get(options).then(result => { + assert.strictEqual( + (NOOP_TRACER.startSpan as sinon.SinonSpy).called, + false + ); + + assert.strictEqual(isWrapped(https.Server.prototype.emit), false); + assert.strictEqual( + (NOOP_TRACER.withSpan as sinon.SinonSpy).called, + false + ); + }); + }); + }); + }); +}); diff --git a/packages/opentelemetry-instrumentation-http/test/functionals/https-enable.test.ts b/packages/opentelemetry-instrumentation-http/test/functionals/https-enable.test.ts new file mode 100644 index 0000000000..e8a7a18655 --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/test/functionals/https-enable.test.ts @@ -0,0 +1,645 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + StatusCode, + context, + propagation, + Span as ISpan, + SpanKind, +} from '@opentelemetry/api'; +import { NoopLogger } from '@opentelemetry/core'; +import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; +import { ContextManager } from '@opentelemetry/context-base'; +import { + BasicTracerProvider, + InMemorySpanExporter, + SimpleSpanProcessor, +} from '@opentelemetry/tracing'; +import { + GeneralAttribute, + HttpAttribute, +} from '@opentelemetry/semantic-conventions'; +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as semver from 'semver'; +import * as nock from 'nock'; +import * as path from 'path'; +import { HttpInstrumentation } from '../../src/http'; +import { assertSpan } from '../utils/assertSpan'; +import { DummyPropagation } from '../utils/DummyPropagation'; +import { isWrapped } from '@opentelemetry/instrumentation'; + +const logger = new NoopLogger(); +const instrumentation = new HttpInstrumentation({ logger }); +instrumentation.enable(); +instrumentation.disable(); + +import * as http from 'http'; +import * as https from 'https'; +import { httpsRequest } from '../utils/httpsRequest'; + +const applyCustomAttributesOnSpanErrorMessage = + 'bad applyCustomAttributesOnSpan function'; + +let server: https.Server; +const serverPort = 32345; +const protocol = 'https'; +const hostname = 'localhost'; +const serverName = 'my.server.name'; +const pathname = '/test'; +const memoryExporter = new InMemorySpanExporter(); +const provider = new BasicTracerProvider({ + logger, +}); +instrumentation.setTracerProvider(provider); +const tracer = provider.getTracer('test-https'); +provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); +propagation.setGlobalPropagator(new DummyPropagation()); + +function doNock( + hostname: string, + path: string, + httpCode: number, + respBody: string, + times?: number +) { + const i = times || 1; + nock(`${protocol}://${hostname}`) + .get(path) + .times(i) + .reply(httpCode, respBody); +} + +export const customAttributeFunction = (span: ISpan): void => { + span.setAttribute('span kind', SpanKind.CLIENT); +}; + +describe('HttpsInstrumentation', () => { + let contextManager: ContextManager; + + beforeEach(() => { + contextManager = new AsyncHooksContextManager().enable(); + context.setGlobalContextManager(contextManager); + }); + + afterEach(() => { + contextManager.disable(); + context.disable(); + }); + + describe('enable()', () => { + describe('with bad instrumentation options', () => { + beforeEach(() => { + memoryExporter.reset(); + }); + + before(() => { + instrumentation.setConfig({ + ignoreIncomingPaths: [ + (url: string) => { + throw new Error('bad ignoreIncomingPaths function'); + }, + ], + ignoreOutgoingUrls: [ + (url: string) => { + throw new Error('bad ignoreOutgoingUrls function'); + }, + ], + applyCustomAttributesOnSpan: () => { + throw new Error(applyCustomAttributesOnSpanErrorMessage); + }, + }); + instrumentation.enable(); + server = https.createServer( + { + key: fs.readFileSync('test/fixtures/server-key.pem'), + cert: fs.readFileSync('test/fixtures/server-cert.pem'), + }, + (request, response) => { + response.end('Test Server Response'); + } + ); + + server.listen(serverPort); + }); + + after(() => { + server.close(); + instrumentation.disable(); + }); + + it('should generate valid spans (client side and server side)', async () => { + const result = await httpsRequest.get( + `${protocol}://${hostname}:${serverPort}${pathname}` + ); + const spans = memoryExporter.getFinishedSpans(); + const [incomingSpan, outgoingSpan] = spans; + const validations = { + hostname, + httpStatusCode: result.statusCode!, + httpMethod: result.method!, + pathname, + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + component: 'https', + }; + + assert.strictEqual(spans.length, 2); + assertSpan(incomingSpan, SpanKind.SERVER, validations); + assertSpan(outgoingSpan, SpanKind.CLIENT, validations); + assert.strictEqual( + incomingSpan.attributes[GeneralAttribute.NET_HOST_PORT], + serverPort + ); + assert.strictEqual( + outgoingSpan.attributes[GeneralAttribute.NET_PEER_PORT], + serverPort + ); + }); + }); + describe('with good instrumentation options', () => { + beforeEach(() => { + memoryExporter.reset(); + }); + + before(() => { + instrumentation.setConfig({ + ignoreIncomingPaths: [ + '/ignored/string', + /\/ignored\/regexp$/i, + (url: string) => url.endsWith('/ignored/function'), + ], + ignoreOutgoingUrls: [ + `${protocol}://${hostname}:${serverPort}/ignored/string`, + /\/ignored\/regexp$/i, + (url: string) => url.endsWith('/ignored/function'), + ], + applyCustomAttributesOnSpan: customAttributeFunction, + serverName, + }); + instrumentation.enable(); + server = https.createServer( + { + key: fs.readFileSync('test/fixtures/server-key.pem'), + cert: fs.readFileSync('test/fixtures/server-cert.pem'), + }, + (request, response) => { + response.end('Test Server Response'); + } + ); + + server.listen(serverPort); + }); + + after(() => { + server.close(); + instrumentation.disable(); + }); + + it(`${protocol} module should be patched`, () => { + assert.strictEqual(isWrapped(https.Server.prototype.emit), true); + }); + + it('should generate valid spans (client side and server side)', async () => { + const result = await httpsRequest.get( + `${protocol}://${hostname}:${serverPort}${pathname}`, + { + headers: { + 'x-forwarded-for': ', , ', + 'user-agent': 'chrome', + }, + } + ); + const spans = memoryExporter.getFinishedSpans(); + const [incomingSpan, outgoingSpan] = spans; + const validations = { + hostname, + httpStatusCode: result.statusCode!, + httpMethod: result.method!, + pathname, + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + component: 'https', + serverName, + }; + + assert.strictEqual(spans.length, 2); + assert.strictEqual( + incomingSpan.attributes[HttpAttribute.HTTP_CLIENT_IP], + '' + ); + assert.strictEqual( + incomingSpan.attributes[GeneralAttribute.NET_HOST_PORT], + serverPort + ); + assert.strictEqual( + outgoingSpan.attributes[GeneralAttribute.NET_PEER_PORT], + serverPort + ); + + [ + { span: incomingSpan, kind: SpanKind.SERVER }, + { span: outgoingSpan, kind: SpanKind.CLIENT }, + ].forEach(({ span, kind }) => { + assert.strictEqual(span.attributes[HttpAttribute.HTTP_FLAVOR], '1.1'); + assert.strictEqual( + span.attributes[GeneralAttribute.NET_TRANSPORT], + GeneralAttribute.IP_TCP + ); + assertSpan(span, kind, validations); + }); + }); + + const httpErrorCodes = [400, 401, 403, 404, 429, 501, 503, 504, 500, 505]; + + for (let i = 0; i < httpErrorCodes.length; i++) { + it(`should test span for GET requests with http error ${httpErrorCodes[i]}`, async () => { + const testPath = '/outgoing/rootSpan/1'; + + doNock( + hostname, + testPath, + httpErrorCodes[i], + httpErrorCodes[i].toString() + ); + + const isReset = memoryExporter.getFinishedSpans().length === 0; + assert.ok(isReset); + + const result = await httpsRequest.get( + `${protocol}://${hostname}${testPath}` + ); + const spans = memoryExporter.getFinishedSpans(); + const reqSpan = spans[0]; + + assert.strictEqual(result.data, httpErrorCodes[i].toString()); + assert.strictEqual(spans.length, 1); + + const validations = { + hostname, + httpStatusCode: result.statusCode!, + httpMethod: 'GET', + pathname: testPath, + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + component: 'https', + }; + + assertSpan(reqSpan, SpanKind.CLIENT, validations); + }); + } + + it('should create a child span for GET requests', async () => { + const testPath = '/outgoing/rootSpan/childs/1'; + doNock(hostname, testPath, 200, 'Ok'); + const name = 'TestRootSpan'; + const span = tracer.startSpan(name); + return tracer.withSpan(span, async () => { + const result = await httpsRequest.get( + `${protocol}://${hostname}${testPath}` + ); + span.end(); + const spans = memoryExporter.getFinishedSpans(); + const [reqSpan, localSpan] = spans; + const validations = { + hostname, + httpStatusCode: result.statusCode!, + httpMethod: 'GET', + pathname: testPath, + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + component: 'https', + }; + + assert.ok(localSpan.name.indexOf('TestRootSpan') >= 0); + assert.strictEqual(spans.length, 2); + assert.strictEqual(reqSpan.name, 'HTTPS GET'); + assert.strictEqual( + localSpan.spanContext.traceId, + reqSpan.spanContext.traceId + ); + assertSpan(reqSpan, SpanKind.CLIENT, validations); + assert.notStrictEqual( + localSpan.spanContext.spanId, + reqSpan.spanContext.spanId + ); + }); + }); + + for (let i = 0; i < httpErrorCodes.length; i++) { + it(`should test child spans for GET requests with http error ${httpErrorCodes[i]}`, async () => { + const testPath = '/outgoing/rootSpan/childs/1'; + doNock( + hostname, + testPath, + httpErrorCodes[i], + httpErrorCodes[i].toString() + ); + const name = 'TestRootSpan'; + const span = tracer.startSpan(name); + return tracer.withSpan(span, async () => { + const result = await httpsRequest.get( + `${protocol}://${hostname}${testPath}` + ); + span.end(); + const spans = memoryExporter.getFinishedSpans(); + const [reqSpan, localSpan] = spans; + const validations = { + hostname, + httpStatusCode: result.statusCode!, + httpMethod: 'GET', + pathname: testPath, + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + component: 'https', + }; + + assert.ok(localSpan.name.indexOf('TestRootSpan') >= 0); + assert.strictEqual(spans.length, 2); + assert.strictEqual(reqSpan.name, 'HTTPS GET'); + assert.strictEqual( + localSpan.spanContext.traceId, + reqSpan.spanContext.traceId + ); + assertSpan(reqSpan, SpanKind.CLIENT, validations); + assert.notStrictEqual( + localSpan.spanContext.spanId, + reqSpan.spanContext.spanId + ); + }); + }); + } + + it('should create multiple child spans for GET requests', async () => { + const testPath = '/outgoing/rootSpan/childs'; + const num = 5; + doNock(hostname, testPath, 200, 'Ok', num); + const name = 'TestRootSpan'; + const span = tracer.startSpan(name); + await tracer.withSpan(span, async () => { + for (let i = 0; i < num; i++) { + await httpsRequest.get(`${protocol}://${hostname}${testPath}`); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans[i].name, 'HTTPS GET'); + assert.strictEqual( + span.context().traceId, + spans[i].spanContext.traceId + ); + } + span.end(); + const spans = memoryExporter.getFinishedSpans(); + // 5 child spans ended + 1 span (root) + assert.strictEqual(spans.length, 6); + }); + }); + + for (const ignored of ['string', 'function', 'regexp']) { + it(`should not trace ignored requests (client and server side) with type ${ignored}`, async () => { + const testPath = `/ignored/${ignored}`; + + await httpsRequest.get( + `${protocol}://${hostname}:${serverPort}${testPath}` + ); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + }); + } + + for (const arg of ['string', {}, new Date()]) { + it(`should be tracable and not throw exception in ${protocol} instrumentation when passing the following argument ${JSON.stringify( + arg + )}`, async () => { + try { + await httpsRequest.get(arg); + } catch (error) { + // request has been made + // nock throw + assert.ok(error.message.startsWith('Nock: No match for request')); + } + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + }); + } + + for (const arg of [true, 1, false, 0, '']) { + it(`should not throw exception in https instrumentation when passing the following argument ${JSON.stringify( + arg + )}`, async () => { + try { + await httpsRequest.get(arg as any); + } catch (error) { + // request has been made + // nock throw + assert.ok( + error.stack.indexOf( + path.normalize('/node_modules/nock/lib/intercept.js') + ) > 0 + ); + } + const spans = memoryExporter.getFinishedSpans(); + // for this arg with don't provide trace. We pass arg to original method (https.get) + assert.strictEqual(spans.length, 0); + }); + } + + it('should have 1 ended span when request throw on bad "options" object', () => { + try { + https.request({ protocol: 'telnet' }); + } catch (error) { + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + } + }); + + it('should have 1 ended span when response.end throw an exception', async () => { + const testPath = '/outgoing/rootSpan/childs/1'; + doNock(hostname, testPath, 400, 'Not Ok'); + + const promiseRequest = new Promise((resolve, reject) => { + const req = https.request( + `${protocol}://${hostname}${testPath}`, + (resp: http.IncomingMessage) => { + let data = ''; + resp.on('data', chunk => { + data += chunk; + }); + resp.on('end', () => { + reject(new Error(data)); + }); + } + ); + return req.end(); + }); + + try { + await promiseRequest; + assert.fail(); + } catch (error) { + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + } + }); + + it('should have 1 ended span when request throw on bad "options" object', () => { + nock.cleanAll(); + nock.enableNetConnect(); + try { + https.request({ protocol: 'telnet' }); + assert.fail(); + } catch (error) { + const spans = memoryExporter.getFinishedSpans(); + /** + * There is an edge case with node 8 because the https module + * just call the http one, resulting in 2 span. The fix only works + * if the protocol is 'https:' resulting in 2 span only for this test. + */ + assert.strictEqual( + spans.length, + semver.gt(process.version, '9.0.0') ? 1 : 2 + ); + } + }); + + it('should have 1 ended span when response.end throw an exception', async () => { + const testPath = '/outgoing/rootSpan/childs/1'; + doNock(hostname, testPath, 400, 'Not Ok'); + + const promiseRequest = new Promise((resolve, reject) => { + const req = https.request( + `${protocol}://${hostname}${testPath}`, + (resp: http.IncomingMessage) => { + let data = ''; + resp.on('data', chunk => { + data += chunk; + }); + resp.on('end', () => { + reject(new Error(data)); + }); + } + ); + return req.end(); + }); + + try { + await promiseRequest; + assert.fail(); + } catch (error) { + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + } + }); + + it('should have 1 ended span when request is aborted', async () => { + nock(`${protocol}://my.server.com`) + .get('/') + .socketDelay(50) + .reply(200, ''); + + const promiseRequest = new Promise((resolve, reject) => { + const req = https.request( + `${protocol}://my.server.com`, + (resp: http.IncomingMessage) => { + let data = ''; + resp.on('data', chunk => { + data += chunk; + }); + resp.on('end', () => { + resolve(data); + }); + } + ); + req.setTimeout(10, () => { + req.abort(); + reject('timeout'); + }); + return req.end(); + }); + + try { + await promiseRequest; + assert.fail(); + } catch (error) { + const spans = memoryExporter.getFinishedSpans(); + const [span] = spans; + assert.strictEqual(spans.length, 1); + assert.strictEqual(span.status.code, StatusCode.ERROR); + assert.ok(Object.keys(span.attributes).length >= 6); + } + }); + + it('should have 1 ended span when request is aborted after receiving response', async () => { + nock(`${protocol}://my.server.com`) + .get('/') + .delay({ + body: 50, + }) + .replyWithFile(200, `${process.cwd()}/package.json`); + + const promiseRequest = new Promise((resolve, reject) => { + const req = https.request( + `${protocol}://my.server.com`, + (resp: http.IncomingMessage) => { + let data = ''; + resp.on('data', chunk => { + req.abort(); + data += chunk; + }); + resp.on('end', () => { + resolve(data); + }); + } + ); + + return req.end(); + }); + + try { + await promiseRequest; + assert.fail(); + } catch (error) { + const spans = memoryExporter.getFinishedSpans(); + const [span] = spans; + assert.strictEqual(spans.length, 1); + assert.strictEqual(span.status.code, StatusCode.ERROR); + assert.ok(Object.keys(span.attributes).length > 7); + } + }); + + it("should have 1 ended span when response is listened by using req.on('response')", done => { + const host = `${protocol}://${hostname}`; + nock(host).get('/').reply(404); + const req = https.request(`${host}/`); + req.on('response', response => { + response.on('data', () => {}); + response.on('end', () => { + const spans = memoryExporter.getFinishedSpans(); + const [span] = spans; + assert.strictEqual(spans.length, 1); + assert.ok(Object.keys(span.attributes).length > 6); + assert.strictEqual( + span.attributes[HttpAttribute.HTTP_STATUS_CODE], + 404 + ); + assert.strictEqual(span.status.code, StatusCode.ERROR); + done(); + }); + }); + req.end(); + }); + }); + }); +}); diff --git a/packages/opentelemetry-instrumentation-http/test/functionals/https-package.test.ts b/packages/opentelemetry-instrumentation-http/test/functionals/https-package.test.ts new file mode 100644 index 0000000000..826792953b --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/test/functionals/https-package.test.ts @@ -0,0 +1,154 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { context, SpanKind, propagation, Span } from '@opentelemetry/api'; +import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; +import { NoopLogger } from '@opentelemetry/core'; +import { NodeTracerProvider } from '@opentelemetry/node'; +import { + InMemorySpanExporter, + SimpleSpanProcessor, +} from '@opentelemetry/tracing'; +import * as assert from 'assert'; +import * as path from 'path'; +import * as url from 'url'; +import { HttpInstrumentation } from '../../src/http'; +import { assertSpan } from '../utils/assertSpan'; +import { DummyPropagation } from '../utils/DummyPropagation'; + +const logger = new NoopLogger(); +const instrumentation = new HttpInstrumentation({ logger }); +instrumentation.enable(); +instrumentation.disable(); + +import * as http from 'http'; +import * as request from 'request-promise-native'; +import * as superagent from 'superagent'; +import * as got from 'got'; +import * as nock from 'nock'; +import axios, { AxiosResponse } from 'axios'; + +const memoryExporter = new InMemorySpanExporter(); +const customAttributeFunction = (span: Span): void => { + span.setAttribute('span kind', SpanKind.CLIENT); +}; + +describe('Packages', () => { + beforeEach(() => { + memoryExporter.reset(); + context.setGlobalContextManager(new AsyncHooksContextManager().enable()); + }); + + afterEach(() => { + context.disable(); + }); + describe('get', () => { + const logger = new NoopLogger(); + + const provider = new NodeTracerProvider({ + logger, + }); + provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); + instrumentation.setTracerProvider(provider); + propagation.setGlobalPropagator(new DummyPropagation()); + beforeEach(() => { + memoryExporter.reset(); + }); + + before(() => { + instrumentation.setConfig({ + applyCustomAttributesOnSpan: customAttributeFunction, + }); + instrumentation.enable(); + }); + + after(() => { + // back to normal + nock.cleanAll(); + nock.enableNetConnect(); + }); + + let resHeaders: http.IncomingHttpHeaders; + [ + { name: 'axios', httpPackage: axios }, //keep first + { name: 'superagent', httpPackage: superagent }, + { name: 'got', httpPackage: { get: (url: string) => got(url) } }, + { + name: 'request', + httpPackage: { get: (url: string) => request(url) }, + }, + ].forEach(({ name, httpPackage }) => { + it(`should create a span for GET requests and add propagation headers by using ${name} package`, async () => { + if (process.versions.node.startsWith('12') && name === 'got') { + // got complains with nock and node version 12+ + // > RequestError: The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type function + // so let's make a real call + nock.cleanAll(); + nock.enableNetConnect(); + } else { + nock.load(path.join(__dirname, '../', '/fixtures/google-https.json')); + } + + const urlparsed = url.parse( + name === 'got' && process.versions.node.startsWith('12') + ? // there is an issue with got 9.6 version and node 12 when redirecting so url above will not work + // https://github.com/nock/nock/pull/1551 + // https://github.com/sindresorhus/got/commit/bf1aa5492ae2bc78cbbec6b7d764906fb156e6c2#diff-707a4781d57c42085155dcb27edb9ccbR258 + // TODO: check if this is still the case when new version + 'https://www.google.com' + : 'https://www.google.com/search?q=axios&oq=axios&aqs=chrome.0.69i59l2j0l3j69i60.811j0j7&sourceid=chrome&ie=UTF-8' + ); + const result = await httpPackage.get(urlparsed.href!); + if (!resHeaders) { + const res = result as AxiosResponse<{}>; + resHeaders = res.headers; + } + const spans = memoryExporter.getFinishedSpans(); + const span = spans[0]; + const validations = { + hostname: urlparsed.hostname!, + httpStatusCode: 200, + httpMethod: 'GET', + pathname: urlparsed.pathname!, + path: urlparsed.path, + resHeaders, + component: 'https', + }; + + assert.strictEqual(spans.length, 1); + assert.strictEqual(span.name, 'HTTPS GET'); + + switch (name) { + case 'axios': + assert.ok( + result.request._headers[DummyPropagation.TRACE_CONTEXT_KEY] + ); + assert.ok( + result.request._headers[DummyPropagation.SPAN_CONTEXT_KEY] + ); + break; + case 'got': + case 'superagent': + break; + default: + break; + } + assert.strictEqual(span.attributes['span kind'], SpanKind.CLIENT); + assertSpan(span, SpanKind.CLIENT, validations); + }); + }); + }); +}); diff --git a/packages/opentelemetry-instrumentation-http/test/functionals/utils.test.ts b/packages/opentelemetry-instrumentation-http/test/functionals/utils.test.ts new file mode 100644 index 0000000000..1a43c057bb --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/test/functionals/utils.test.ts @@ -0,0 +1,311 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + StatusCode, + ROOT_CONTEXT, + SpanKind, + TraceFlags, +} from '@opentelemetry/api'; +import { NoopLogger } from '@opentelemetry/core'; +import { BasicTracerProvider, Span } from '@opentelemetry/tracing'; +import { HttpAttribute } from '@opentelemetry/semantic-conventions'; +import * as assert from 'assert'; +import * as http from 'http'; +import { IncomingMessage, ServerResponse } from 'http'; +import { Socket } from 'net'; +import * as sinon from 'sinon'; +import * as url from 'url'; +import { IgnoreMatcher } from '../../src/types'; +import * as utils from '../../src/utils'; + +describe('Utility', () => { + describe('parseResponseStatus()', () => { + it('should return ERROR code by default', () => { + const status = utils.parseResponseStatus( + (undefined as unknown) as number + ); + assert.deepStrictEqual(status, { code: StatusCode.ERROR }); + }); + + it('should return OK for Success HTTP status code', () => { + for (let index = 100; index < 400; index++) { + const status = utils.parseResponseStatus(index); + assert.deepStrictEqual(status, { code: StatusCode.OK }); + } + }); + + it('should not return OK for Bad HTTP status code', () => { + for (let index = 400; index <= 600; index++) { + const status = utils.parseResponseStatus(index); + assert.notStrictEqual(status.code, StatusCode.OK); + } + }); + }); + describe('hasExpectHeader()', () => { + it('should throw if no option', () => { + try { + utils.hasExpectHeader('' as http.RequestOptions); + assert.fail(); + } catch (ignore) {} + }); + + it('should not throw if no headers', () => { + const result = utils.hasExpectHeader({} as http.RequestOptions); + assert.strictEqual(result, false); + }); + + it('should return true on Expect (no case sensitive)', () => { + for (const headers of [{ Expect: 1 }, { expect: 1 }, { ExPect: 1 }]) { + const result = utils.hasExpectHeader({ + headers, + } as http.RequestOptions); + assert.strictEqual(result, true); + } + }); + }); + + describe('getRequestInfo()', () => { + it('should get options object', () => { + const webUrl = 'http://u:p@google.fr/aPath?qu=ry'; + const urlParsed = url.parse(webUrl); + const urlParsedWithoutPathname = { + ...urlParsed, + pathname: undefined, + }; + const whatWgUrl = new url.URL(webUrl); + for (const param of [ + webUrl, + urlParsed, + urlParsedWithoutPathname, + whatWgUrl, + ]) { + const result = utils.getRequestInfo(param); + assert.strictEqual(result.optionsParsed.hostname, 'google.fr'); + assert.strictEqual(result.optionsParsed.protocol, 'http:'); + assert.strictEqual(result.optionsParsed.path, '/aPath?qu=ry'); + assert.strictEqual(result.pathname, '/aPath'); + assert.strictEqual(result.origin, 'http://google.fr'); + } + }); + }); + + describe('satisfiesPattern()', () => { + it('string pattern', () => { + const answer1 = utils.satisfiesPattern('/test/1', '/test/1'); + assert.strictEqual(answer1, true); + const answer2 = utils.satisfiesPattern('/test/1', '/test/11'); + assert.strictEqual(answer2, false); + }); + + it('regex pattern', () => { + const answer1 = utils.satisfiesPattern('/TeSt/1', /\/test/i); + assert.strictEqual(answer1, true); + const answer2 = utils.satisfiesPattern('/2/tEst/1', /\/test/); + assert.strictEqual(answer2, false); + }); + + it('should throw if type is unknown', () => { + try { + utils.satisfiesPattern('/TeSt/1', (true as unknown) as IgnoreMatcher); + assert.fail(); + } catch (error) { + assert.strictEqual(error instanceof TypeError, true); + } + }); + + it('function pattern', () => { + const answer1 = utils.satisfiesPattern( + '/test/home', + (url: string) => url === '/test/home' + ); + assert.strictEqual(answer1, true); + const answer2 = utils.satisfiesPattern( + '/test/home', + (url: string) => url !== '/test/home' + ); + assert.strictEqual(answer2, false); + }); + }); + + describe('isIgnored()', () => { + let satisfiesPatternStub: sinon.SinonSpy<[string, IgnoreMatcher], boolean>; + beforeEach(() => { + satisfiesPatternStub = sinon.spy(utils, 'satisfiesPattern'); + }); + + afterEach(() => { + satisfiesPatternStub.restore(); + }); + + it('should call isSatisfyPattern, n match', () => { + const answer1 = utils.isIgnored('/test/1', ['/test/11']); + assert.strictEqual(answer1, false); + assert.strictEqual( + (utils.satisfiesPattern as sinon.SinonSpy).callCount, + 1 + ); + }); + + it('should call isSatisfyPattern, match for function', () => { + satisfiesPatternStub.restore(); + const answer1 = utils.isIgnored('/test/1', [ + url => url.endsWith('/test/1'), + ]); + assert.strictEqual(answer1, true); + }); + + it('should not re-throw when function throws an exception', () => { + satisfiesPatternStub.restore(); + const log = new NoopLogger(); + const onException = (e: Error) => { + log.error('error', e); + }; + for (const callback of [undefined, onException]) { + assert.doesNotThrow(() => + utils.isIgnored( + '/test/1', + [ + () => { + throw new Error('test'); + }, + ], + callback + ) + ); + } + }); + + it('should call onException when function throws an exception', () => { + satisfiesPatternStub.restore(); + const onException = sinon.spy(); + assert.doesNotThrow(() => + utils.isIgnored( + '/test/1', + [ + () => { + throw new Error('test'); + }, + ], + onException + ) + ); + assert.strictEqual((onException as sinon.SinonSpy).callCount, 1); + }); + + it('should not call isSatisfyPattern', () => { + utils.isIgnored('/test/1', []); + assert.strictEqual( + (utils.satisfiesPattern as sinon.SinonSpy).callCount, + 0 + ); + }); + + it('should return false on empty list', () => { + const answer1 = utils.isIgnored('/test/1', []); + assert.strictEqual(answer1, false); + }); + + it('should not throw and return false when list is undefined', () => { + const answer2 = utils.isIgnored('/test/1', undefined); + assert.strictEqual(answer2, false); + }); + }); + + describe('getAbsoluteUrl()', () => { + it('should return absolute url with localhost', () => { + const path = '/test/1'; + const result = utils.getAbsoluteUrl(url.parse(path), {}); + assert.strictEqual(result, `http://localhost${path}`); + }); + it('should return absolute url', () => { + const absUrl = 'http://www.google/test/1?query=1'; + const result = utils.getAbsoluteUrl(url.parse(absUrl), {}); + assert.strictEqual(result, absUrl); + }); + it('should return default url', () => { + const result = utils.getAbsoluteUrl(null, {}); + assert.strictEqual(result, 'http://localhost/'); + }); + it("{ path: '/helloworld', port: 8080 } should return http://localhost:8080/helloworld", () => { + const result = utils.getAbsoluteUrl( + { path: '/helloworld', port: 8080 }, + {} + ); + assert.strictEqual(result, 'http://localhost:8080/helloworld'); + }); + }); + + describe('setSpanWithError()', () => { + it('should have error attributes', () => { + const errorMessage = 'test error'; + for (const obj of [undefined, { statusCode: 400 }]) { + const span = new Span( + new BasicTracerProvider().getTracer('default'), + ROOT_CONTEXT, + 'test', + { spanId: '', traceId: '', traceFlags: TraceFlags.SAMPLED }, + SpanKind.INTERNAL + ); + /* tslint:disable-next-line:no-any */ + utils.setSpanWithError(span, new Error(errorMessage), obj as any); + const attributes = span.attributes; + assert.strictEqual( + attributes[HttpAttribute.HTTP_ERROR_MESSAGE], + errorMessage + ); + assert.ok(attributes[HttpAttribute.HTTP_ERROR_NAME]); + } + }); + }); + + describe('isValidOptionsType()', () => { + ['', false, true, 1, 0, []].forEach(options => { + it(`should return false with the following value: ${JSON.stringify( + options + )}`, () => { + assert.strictEqual(utils.isValidOptionsType(options), false); + }); + }); + for (const options of ['url', url.parse('http://url.com'), {}]) { + it(`should return true with the following value: ${JSON.stringify( + options + )}`, () => { + assert.strictEqual(utils.isValidOptionsType(options), true); + }); + } + }); + + describe('getIncomingRequestAttributesOnResponse()', () => { + it('should correctly parse the middleware stack if present', () => { + const request = { + __ot_middlewares: ['/test', '/toto', '/'], + } as IncomingMessage & { __ot_middlewares?: string[] }; + + const attributes = utils.getIncomingRequestAttributesOnResponse(request, { + socket: {}, + } as ServerResponse & { socket: Socket }); + assert.deepEqual(attributes[HttpAttribute.HTTP_ROUTE], '/test/toto'); + }); + + it('should succesfully process without middleware stack', () => { + const request = {} as IncomingMessage; + const attributes = utils.getIncomingRequestAttributesOnResponse(request, { + socket: {}, + } as ServerResponse & { socket: Socket }); + assert.deepEqual(attributes[HttpAttribute.HTTP_ROUTE], undefined); + }); + }); +}); diff --git a/packages/opentelemetry-instrumentation-http/test/integrations/http-enable.test.ts b/packages/opentelemetry-instrumentation-http/test/integrations/http-enable.test.ts new file mode 100644 index 0000000000..86df32f56e --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/test/integrations/http-enable.test.ts @@ -0,0 +1,306 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { NoopLogger } from '@opentelemetry/core'; +import { SpanKind, Span, context, propagation } from '@opentelemetry/api'; +import { + HttpAttribute, + GeneralAttribute, +} from '@opentelemetry/semantic-conventions'; +import * as assert from 'assert'; +import * as url from 'url'; +import { HttpInstrumentation } from '../../src/http'; +import { assertSpan } from '../utils/assertSpan'; +import * as utils from '../utils/utils'; +import { NodeTracerProvider } from '@opentelemetry/node'; +import { + InMemorySpanExporter, + SimpleSpanProcessor, +} from '@opentelemetry/tracing'; +import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; + +const logger = new NoopLogger(); +const instrumentation = new HttpInstrumentation({ logger }); +instrumentation.enable(); +instrumentation.disable(); + +import * as http from 'http'; +import { httpRequest } from '../utils/httpRequest'; +import { DummyPropagation } from '../utils/DummyPropagation'; + +const protocol = 'http'; +const serverPort = 32345; +const hostname = 'localhost'; +const memoryExporter = new InMemorySpanExporter(); + +const customAttributeFunction = (span: Span): void => { + span.setAttribute('span kind', SpanKind.CLIENT); +}; + +describe('HttpInstrumentation Integration tests', () => { + beforeEach(() => { + memoryExporter.reset(); + context.setGlobalContextManager(new AsyncHooksContextManager().enable()); + }); + + afterEach(() => { + context.disable(); + }); + describe('enable()', () => { + before(function (done) { + // mandatory + if (process.env.CI) { + done(); + return; + } + + utils.checkInternet(isConnected => { + if (!isConnected) { + this.skip(); + // don't disturb people + } + done(); + }); + }); + + const logger = new NoopLogger(); + const provider = new NodeTracerProvider({ + logger, + }); + propagation.setGlobalPropagator(new DummyPropagation()); + provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); + instrumentation.setTracerProvider(provider); + beforeEach(() => { + memoryExporter.reset(); + }); + + before(() => { + const ignoreConfig = [ + `${protocol}://${hostname}:${serverPort}/ignored/string`, + /\/ignored\/regexp$/i, + (url: string) => url.endsWith('/ignored/function'), + ]; + instrumentation.setConfig({ + ignoreIncomingPaths: ignoreConfig, + ignoreOutgoingUrls: ignoreConfig, + applyCustomAttributesOnSpan: customAttributeFunction, + }); + instrumentation.enable(); + }); + + after(() => { + instrumentation.disable(); + }); + + it('should create a rootSpan for GET requests and add propagation headers', async () => { + let spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + + const result = await httpRequest.get( + `${protocol}://google.fr/?query=test` + ); + + spans = memoryExporter.getFinishedSpans(); + const span = spans[0]; + const validations = { + hostname: 'google.fr', + httpStatusCode: result.statusCode!, + httpMethod: 'GET', + pathname: '/', + path: '/?query=test', + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + component: 'http', + }; + + assert.strictEqual(spans.length, 1); + assert.strictEqual(span.name, 'HTTP GET'); + assertSpan(span, SpanKind.CLIENT, validations); + }); + + it('should create a rootSpan for GET requests and add propagation headers if URL is used', async () => { + let spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + + const result = await httpRequest.get( + new url.URL(`${protocol}://google.fr/?query=test`) + ); + + spans = memoryExporter.getFinishedSpans(); + const span = spans[0]; + const validations = { + hostname: 'google.fr', + httpStatusCode: result.statusCode!, + httpMethod: 'GET', + pathname: '/', + path: '/?query=test', + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + component: 'http', + }; + + assert.strictEqual(spans.length, 1); + assert.strictEqual(span.name, 'HTTP GET'); + assertSpan(span, SpanKind.CLIENT, validations); + }); + + it('should create a valid rootSpan with propagation headers for GET requests if URL and options are used', async () => { + let spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + + const result = await httpRequest.get( + new url.URL(`${protocol}://google.fr/?query=test`), + { + headers: { 'x-foo': 'foo' }, + } + ); + + spans = memoryExporter.getFinishedSpans(); + const span = spans[0]; + const validations = { + hostname: 'google.fr', + httpStatusCode: result.statusCode!, + httpMethod: 'GET', + pathname: '/', + path: '/?query=test', + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + component: 'http', + }; + + assert.strictEqual(spans.length, 1); + assert.strictEqual(span.name, 'HTTP GET'); + assert.strictEqual(result.reqHeaders['x-foo'], 'foo'); + assert.strictEqual(span.attributes[HttpAttribute.HTTP_FLAVOR], '1.1'); + assert.strictEqual( + span.attributes[GeneralAttribute.NET_TRANSPORT], + GeneralAttribute.IP_TCP + ); + assertSpan(span, SpanKind.CLIENT, validations); + }); + + it('custom attributes should show up on client spans', async () => { + const result = await httpRequest.get(`${protocol}://google.fr/`); + const spans = memoryExporter.getFinishedSpans(); + const span = spans[0]; + const validations = { + hostname: 'google.fr', + httpStatusCode: result.statusCode!, + httpMethod: 'GET', + pathname: '/', + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + component: 'http', + }; + + assert.strictEqual(spans.length, 1); + assert.strictEqual(span.name, 'HTTP GET'); + assert.strictEqual(span.attributes['span kind'], SpanKind.CLIENT); + assertSpan(span, SpanKind.CLIENT, validations); + }); + + it('should create a span for GET requests and add propagation headers with Expect headers', async () => { + let spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + const options = Object.assign( + { headers: { Expect: '100-continue' } }, + url.parse(`${protocol}://google.fr/`) + ); + + const result = await httpRequest.get(options); + spans = memoryExporter.getFinishedSpans(); + const span = spans[0]; + const validations = { + hostname: 'google.fr', + httpStatusCode: 301, + httpMethod: 'GET', + pathname: '/', + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + component: 'http', + }; + + assert.strictEqual(spans.length, 1); + assert.strictEqual(span.name, 'HTTP GET'); + + try { + assertSpan(span, SpanKind.CLIENT, validations); + } catch (error) { + // temporary redirect is also correct + validations.httpStatusCode = 307; + assertSpan(span, SpanKind.CLIENT, validations); + } + }); + for (const headers of [ + { Expect: '100-continue', 'user-agent': 'http-instrumentation-test' }, + { 'user-agent': 'http-instrumentation-test' }, + ]) { + it(`should create a span for GET requests and add propagation when using the following signature: get(url, options, callback) and following headers: ${JSON.stringify( + headers + )}`, done => { + let validations: { + hostname: string; + httpStatusCode: number; + httpMethod: string; + pathname: string; + reqHeaders: http.OutgoingHttpHeaders; + resHeaders: http.IncomingHttpHeaders; + }; + let data = ''; + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + const options = { headers }; + const req = http.get( + `${protocol}://google.fr/`, + options, + (resp: http.IncomingMessage) => { + const res = (resp as unknown) as http.IncomingMessage & { + req: http.IncomingMessage; + }; + + resp.on('data', chunk => { + data += chunk; + }); + resp.on('end', () => { + validations = { + hostname: 'google.fr', + httpStatusCode: 301, + httpMethod: 'GET', + pathname: '/', + resHeaders: resp.headers, + /* tslint:disable:no-any */ + reqHeaders: (res.req as any).getHeaders + ? (res.req as any).getHeaders() + : (res.req as any)._headers, + /* tslint:enable:no-any */ + }; + }); + } + ); + + req.on('close', () => { + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].name, 'HTTP GET'); + assert.ok(data); + assert.ok(validations.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY]); + assert.ok(validations.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY]); + done(); + }); + }); + } + }); +}); diff --git a/packages/opentelemetry-instrumentation-http/test/integrations/https-enable.test.ts b/packages/opentelemetry-instrumentation-http/test/integrations/https-enable.test.ts new file mode 100644 index 0000000000..b9d68ea8d1 --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/test/integrations/https-enable.test.ts @@ -0,0 +1,305 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { NoopLogger } from '@opentelemetry/core'; +import { SpanKind, Span, context, propagation } from '@opentelemetry/api'; +import { + HttpAttribute, + GeneralAttribute, +} from '@opentelemetry/semantic-conventions'; +import * as assert from 'assert'; +import * as http from 'http'; +import { assertSpan } from '../utils/assertSpan'; +import * as url from 'url'; +import * as utils from '../utils/utils'; +import { NodeTracerProvider } from '@opentelemetry/node'; +import { + InMemorySpanExporter, + SimpleSpanProcessor, +} from '@opentelemetry/tracing'; +import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; +import { HttpInstrumentation } from '../../src'; + +const logger = new NoopLogger(); +const instrumentation = new HttpInstrumentation({ logger }); +instrumentation.enable(); +instrumentation.disable(); + +import * as https from 'https'; +import { httpsRequest } from '../utils/httpsRequest'; +import { DummyPropagation } from '../utils/DummyPropagation'; + +const protocol = 'https'; +const serverPort = 42345; +const hostname = 'localhost'; +const memoryExporter = new InMemorySpanExporter(); + +export const customAttributeFunction = (span: Span): void => { + span.setAttribute('span kind', SpanKind.CLIENT); +}; + +describe('HttpsInstrumentation Integration tests', () => { + beforeEach(() => { + memoryExporter.reset(); + context.setGlobalContextManager(new AsyncHooksContextManager().enable()); + }); + + afterEach(() => { + context.disable(); + }); + + describe('enable()', () => { + before(function (done) { + // mandatory + if (process.env.CI) { + done(); + return; + } + + utils.checkInternet(isConnected => { + if (!isConnected) { + this.skip(); + // don't disturb people + } + done(); + }); + }); + const logger = new NoopLogger(); + const provider = new NodeTracerProvider({ + logger, + }); + propagation.setGlobalPropagator(new DummyPropagation()); + provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); + instrumentation.setTracerProvider(provider); + beforeEach(() => { + memoryExporter.reset(); + }); + + before(() => { + const ignoreConfig = [ + `${protocol}://${hostname}:${serverPort}/ignored/string`, + /\/ignored\/regexp$/i, + (url: string) => url.endsWith('/ignored/function'), + ]; + instrumentation.setConfig({ + ignoreIncomingPaths: ignoreConfig, + ignoreOutgoingUrls: ignoreConfig, + applyCustomAttributesOnSpan: customAttributeFunction, + }); + instrumentation.enable(); + }); + + after(() => { + instrumentation.disable(); + }); + + it('should create a rootSpan for GET requests and add propagation headers', async () => { + let spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + + const result = await httpsRequest.get( + `${protocol}://google.fr/?query=test` + ); + + spans = memoryExporter.getFinishedSpans(); + const span = spans[0]; + const validations = { + hostname: 'google.fr', + httpStatusCode: result.statusCode!, + httpMethod: 'GET', + pathname: '/', + path: '/?query=test', + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + component: 'https', + }; + + assert.strictEqual(spans.length, 1); + assert.strictEqual(span.name, 'HTTPS GET'); + assertSpan(span, SpanKind.CLIENT, validations); + }); + + it('should create a rootSpan for GET requests and add propagation headers if URL is used', async () => { + let spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + + const result = await httpsRequest.get( + new url.URL(`${protocol}://google.fr/?query=test`) + ); + + spans = memoryExporter.getFinishedSpans(); + const span = spans[0]; + const validations = { + hostname: 'google.fr', + httpStatusCode: result.statusCode!, + httpMethod: 'GET', + pathname: '/', + path: '/?query=test', + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + component: 'https', + }; + + assert.strictEqual(spans.length, 1); + assert.strictEqual(span.name, 'HTTPS GET'); + assertSpan(span, SpanKind.CLIENT, validations); + }); + + it('should create a valid rootSpan with propagation headers for GET requests if URL and options are used', async () => { + let spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + + const result = await httpsRequest.get( + new url.URL(`${protocol}://google.fr/?query=test`), + { headers: { 'x-foo': 'foo' } } + ); + + spans = memoryExporter.getFinishedSpans(); + const span = spans[0]; + const validations = { + hostname: 'google.fr', + httpStatusCode: result.statusCode!, + httpMethod: 'GET', + pathname: '/', + path: '/?query=test', + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + component: 'https', + }; + + assert.strictEqual(spans.length, 1); + assert.strictEqual(span.name, 'HTTPS GET'); + assert.strictEqual(result.reqHeaders['x-foo'], 'foo'); + assert.strictEqual(span.attributes[HttpAttribute.HTTP_FLAVOR], '1.1'); + assert.strictEqual( + span.attributes[GeneralAttribute.NET_TRANSPORT], + GeneralAttribute.IP_TCP + ); + assertSpan(span, SpanKind.CLIENT, validations); + }); + + it('custom attributes should show up on client spans', async () => { + const result = await httpsRequest.get(`${protocol}://google.fr/`); + const spans = memoryExporter.getFinishedSpans(); + const span = spans[0]; + const validations = { + hostname: 'google.fr', + httpStatusCode: result.statusCode!, + httpMethod: 'GET', + pathname: '/', + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + component: 'https', + }; + + assert.strictEqual(spans.length, 1); + assert.strictEqual(span.name, 'HTTPS GET'); + assert.strictEqual(span.attributes['span kind'], SpanKind.CLIENT); + assertSpan(span, SpanKind.CLIENT, validations); + }); + + it('should create a span for GET requests and add propagation headers with Expect headers', async () => { + let spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + const options = Object.assign( + { headers: { Expect: '100-continue' } }, + url.parse(`${protocol}://google.fr/`) + ); + + const result = await httpsRequest.get(options); + spans = memoryExporter.getFinishedSpans(); + const span = spans[0]; + const validations = { + hostname: 'google.fr', + httpStatusCode: 301, + httpMethod: 'GET', + pathname: '/', + resHeaders: result.resHeaders, + reqHeaders: result.reqHeaders, + component: 'https', + }; + + assert.strictEqual(spans.length, 1); + assert.strictEqual(span.name, 'HTTPS GET'); + + try { + assertSpan(span, SpanKind.CLIENT, validations); + } catch (error) { + // temporary redirect is also correct + validations.httpStatusCode = 307; + assertSpan(span, SpanKind.CLIENT, validations); + } + }); + for (const headers of [ + { Expect: '100-continue', 'user-agent': 'https-instrumentation-test' }, + { 'user-agent': 'https-instrumentation-test' }, + ]) { + it(`should create a span for GET requests and add propagation when using the following signature: get(url, options, callback) and following headers: ${JSON.stringify( + headers + )}`, done => { + let validations: { + hostname: string; + httpStatusCode: number; + httpMethod: string; + pathname: string; + reqHeaders: http.OutgoingHttpHeaders; + resHeaders: http.IncomingHttpHeaders; + }; + let data = ''; + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + const options = { headers }; + const req = https.get( + `${protocol}://google.fr/`, + options, + (resp: http.IncomingMessage) => { + const res = (resp as unknown) as http.IncomingMessage & { + req: http.IncomingMessage; + }; + + resp.on('data', chunk => { + data += chunk; + }); + resp.on('end', () => { + validations = { + hostname: 'google.fr', + httpStatusCode: 301, + httpMethod: 'GET', + pathname: '/', + resHeaders: resp.headers, + /* tslint:disable:no-any */ + reqHeaders: (res.req as any).getHeaders + ? (res.req as any).getHeaders() + : (res.req as any)._headers, + /* tslint:enable:no-any */ + }; + }); + } + ); + + req.on('close', () => { + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].name, 'HTTPS GET'); + assert.ok(data); + assert.ok(validations.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY]); + assert.ok(validations.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY]); + done(); + }); + }); + } + }); +}); diff --git a/packages/opentelemetry-instrumentation-http/test/utils/DummyPropagation.ts b/packages/opentelemetry-instrumentation-http/test/utils/DummyPropagation.ts new file mode 100644 index 0000000000..0944e1e199 --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/test/utils/DummyPropagation.ts @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + Context, + TextMapPropagator, + TraceFlags, + getParentSpanContext, + setExtractedSpanContext, +} from '@opentelemetry/api'; +import type * as http from 'http'; + +export class DummyPropagation implements TextMapPropagator { + static TRACE_CONTEXT_KEY = 'x-dummy-trace-id'; + static SPAN_CONTEXT_KEY = 'x-dummy-span-id'; + extract(context: Context, carrier: http.OutgoingHttpHeaders) { + const extractedSpanContext = { + traceId: carrier[DummyPropagation.TRACE_CONTEXT_KEY] as string, + spanId: DummyPropagation.SPAN_CONTEXT_KEY, + traceFlags: TraceFlags.SAMPLED, + isRemote: true, + }; + if (extractedSpanContext.traceId && extractedSpanContext.spanId) { + return setExtractedSpanContext(context, extractedSpanContext); + } + return context; + } + inject(context: Context, headers: { [custom: string]: string }): void { + const spanContext = getParentSpanContext(context); + if (!spanContext) return; + headers[DummyPropagation.TRACE_CONTEXT_KEY] = spanContext.traceId; + headers[DummyPropagation.SPAN_CONTEXT_KEY] = spanContext.spanId; + } + fields(): string[] { + return [ + DummyPropagation.TRACE_CONTEXT_KEY, + DummyPropagation.SPAN_CONTEXT_KEY, + ]; + } +} diff --git a/packages/opentelemetry-instrumentation-http/test/utils/assertSpan.ts b/packages/opentelemetry-instrumentation-http/test/utils/assertSpan.ts new file mode 100644 index 0000000000..68ea829cb4 --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/test/utils/assertSpan.ts @@ -0,0 +1,131 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { SpanKind, Status } from '@opentelemetry/api'; +import { hrTimeToNanoseconds } from '@opentelemetry/core'; +import { ReadableSpan } from '@opentelemetry/tracing'; +import { + GeneralAttribute, + HttpAttribute, +} from '@opentelemetry/semantic-conventions'; +import * as assert from 'assert'; +import * as http from 'http'; +import * as utils from '../../src/utils'; +import { DummyPropagation } from './DummyPropagation'; + +export const assertSpan = ( + span: ReadableSpan, + kind: SpanKind, + validations: { + httpStatusCode: number; + httpMethod: string; + resHeaders: http.IncomingHttpHeaders; + hostname: string; + pathname: string; + reqHeaders?: http.OutgoingHttpHeaders; + path?: string | null; + forceStatus?: Status; + serverName?: string; + component: string; + } +) => { + assert.strictEqual(span.spanContext.traceId.length, 32); + assert.strictEqual(span.spanContext.spanId.length, 16); + assert.strictEqual(span.kind, kind); + assert.strictEqual( + span.name, + `${validations.component.toUpperCase()} ${validations.httpMethod}` + ); + assert.strictEqual( + span.attributes[HttpAttribute.HTTP_ERROR_MESSAGE], + span.status.message + ); + assert.strictEqual( + span.attributes[HttpAttribute.HTTP_METHOD], + validations.httpMethod + ); + assert.strictEqual( + span.attributes[HttpAttribute.HTTP_TARGET], + validations.path || validations.pathname + ); + assert.strictEqual( + span.attributes[HttpAttribute.HTTP_STATUS_CODE], + validations.httpStatusCode + ); + + assert.strictEqual(span.links.length, 0); + assert.strictEqual(span.events.length, 0); + + assert.deepStrictEqual( + span.status, + validations.forceStatus || + utils.parseResponseStatus(validations.httpStatusCode) + ); + + assert.ok(span.endTime, 'must be finished'); + assert.ok(hrTimeToNanoseconds(span.duration), 'must have positive duration'); + + if (validations.reqHeaders) { + const userAgent = validations.reqHeaders['user-agent']; + if (userAgent) { + assert.strictEqual( + span.attributes[HttpAttribute.HTTP_USER_AGENT], + userAgent + ); + } + } + if (span.kind === SpanKind.CLIENT) { + assert.strictEqual( + span.attributes[GeneralAttribute.NET_PEER_NAME], + validations.hostname, + 'must be consistent (PEER_NAME and hostname)' + ); + assert.ok( + span.attributes[GeneralAttribute.NET_PEER_IP], + 'must have PEER_IP' + ); + assert.ok( + span.attributes[GeneralAttribute.NET_PEER_PORT], + 'must have PEER_PORT' + ); + assert.ok( + (span.attributes[HttpAttribute.HTTP_URL] as string).indexOf( + span.attributes[GeneralAttribute.NET_PEER_NAME] as string + ) > -1, + 'must be consistent' + ); + } + if (span.kind === SpanKind.SERVER) { + if (validations.serverName) { + assert.strictEqual( + span.attributes[HttpAttribute.HTTP_SERVER_NAME], + validations.serverName, + ' must have serverName attribute' + ); + assert.ok( + span.attributes[GeneralAttribute.NET_HOST_PORT], + 'must have HOST_PORT' + ); + assert.ok( + span.attributes[GeneralAttribute.NET_HOST_IP], + 'must have HOST_IP' + ); + } + assert.strictEqual(span.parentSpanId, DummyPropagation.SPAN_CONTEXT_KEY); + } else if (validations.reqHeaders) { + assert.ok(validations.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY]); + assert.ok(validations.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY]); + } +}; diff --git a/packages/opentelemetry-instrumentation-http/test/utils/httpRequest.ts b/packages/opentelemetry-instrumentation-http/test/utils/httpRequest.ts new file mode 100644 index 0000000000..f507b7f1f1 --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/test/utils/httpRequest.ts @@ -0,0 +1,68 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as http from 'http'; +import { URL } from 'url'; + +type GetResult = Promise<{ + data: string; + statusCode: number | undefined; + resHeaders: http.IncomingHttpHeaders; + reqHeaders: http.OutgoingHttpHeaders; + method: string | undefined; +}>; + +function get(input: string | URL, options?: http.RequestOptions): GetResult; +function get(input: http.RequestOptions): GetResult; +function get(input: any, options?: any): GetResult { + return new Promise((resolve, reject) => { + // eslint-disable-next-line prefer-const + let req: http.ClientRequest; + + function onGetResponseCb(resp: http.IncomingMessage): void { + const res = (resp as unknown) as http.IncomingMessage & { + req: http.IncomingMessage; + }; + let data = ''; + resp.on('data', chunk => { + data += chunk; + }); + resp.on('end', () => { + resolve({ + data, + statusCode: res.statusCode, + reqHeaders: req.getHeaders ? req.getHeaders() : (req as any)._headers, + resHeaders: res.headers, + method: res.req.method, + }); + }); + resp.on('error', err => { + reject(err); + }); + } + req = + options != null + ? http.get(input, options, onGetResponseCb) + : http.get(input, onGetResponseCb); + req.on('error', err => { + reject(err); + }); + return req; + }); +} + +export const httpRequest = { + get, +}; diff --git a/packages/opentelemetry-instrumentation-http/test/utils/httpsRequest.ts b/packages/opentelemetry-instrumentation-http/test/utils/httpsRequest.ts new file mode 100644 index 0000000000..f75cf1e566 --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/test/utils/httpsRequest.ts @@ -0,0 +1,72 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as http from 'http'; +import * as https from 'https'; +import { URL } from 'url'; + +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + +type GetResult = Promise<{ + data: string; + statusCode: number | undefined; + resHeaders: http.IncomingHttpHeaders; + reqHeaders: http.OutgoingHttpHeaders; + method: string | undefined; +}>; + +function get(input: string | URL, options?: https.RequestOptions): GetResult; +function get(input: https.RequestOptions): GetResult; +function get(input: any, options?: any): GetResult { + return new Promise((resolve, reject) => { + // eslint-disable-next-line prefer-const + let req: http.ClientRequest; + + function onGetResponseCb(resp: http.IncomingMessage): void { + const res = (resp as unknown) as http.IncomingMessage & { + req: http.IncomingMessage; + }; + let data = ''; + resp.on('data', chunk => { + data += chunk; + }); + resp.on('end', () => { + resolve({ + data, + statusCode: res.statusCode, + reqHeaders: req.getHeaders ? req.getHeaders() : (req as any)._headers, + resHeaders: res.headers, + method: res.req.method, + }); + }); + resp.on('error', err => { + reject(err); + }); + } + req = + options != null + ? https.get(input, options, onGetResponseCb) + : https.get(input, onGetResponseCb); + req.on('error', err => { + reject(err); + }); + return req; + }); +} + +export const httpsRequest = { + get, +}; diff --git a/packages/opentelemetry-instrumentation-http/test/utils/utils.ts b/packages/opentelemetry-instrumentation-http/test/utils/utils.ts new file mode 100644 index 0000000000..5d2e5cb3bc --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/test/utils/utils.ts @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as dns from 'dns'; + +export const checkInternet = (cb: (isConnected: boolean) => void) => { + dns.lookup('google.com', err => { + if (err && err.code === 'ENOTFOUND') { + cb(false); + } else { + cb(true); + } + }); +}; diff --git a/packages/opentelemetry-instrumentation-http/tsconfig.json b/packages/opentelemetry-instrumentation-http/tsconfig.json new file mode 100644 index 0000000000..a2042cd68b --- /dev/null +++ b/packages/opentelemetry-instrumentation-http/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.base", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +} diff --git a/packages/opentelemetry-instrumentation/src/platform/node/instrumentation.ts b/packages/opentelemetry-instrumentation/src/platform/node/instrumentation.ts index d4acd2c7bb..badf5dd5d3 100644 --- a/packages/opentelemetry-instrumentation/src/platform/node/instrumentation.ts +++ b/packages/opentelemetry-instrumentation/src/platform/node/instrumentation.ts @@ -87,6 +87,7 @@ export abstract class InstrumentationBase ): T { if (!baseDir) { if (typeof module.patch === 'function') { + module.moduleExports = exports; return module.patch(exports); } return exports; diff --git a/packages/opentelemetry-instrumentation/src/platform/node/instrumentationNodeModuleDefinition.ts b/packages/opentelemetry-instrumentation/src/platform/node/instrumentationNodeModuleDefinition.ts index 9dc7d01dbc..5ae8868cab 100644 --- a/packages/opentelemetry-instrumentation/src/platform/node/instrumentationNodeModuleDefinition.ts +++ b/packages/opentelemetry-instrumentation/src/platform/node/instrumentationNodeModuleDefinition.ts @@ -26,7 +26,7 @@ export class InstrumentationNodeModuleDefinition public name: string, public supportedVersions: string[], public patch?: (exports: T) => T, - public unpatch?: () => void, + public unpatch?: (exports: T) => void, files?: InstrumentationModuleFile[] ) { this.files = files || []; diff --git a/packages/opentelemetry-instrumentation/src/utils.ts b/packages/opentelemetry-instrumentation/src/utils.ts index 8f770451f1..d4406e3a85 100644 --- a/packages/opentelemetry-instrumentation/src/utils.ts +++ b/packages/opentelemetry-instrumentation/src/utils.ts @@ -23,7 +23,8 @@ import { ShimWrapped } from './types'; */ export function safeExecuteInTheMiddle( execute: () => T, - onFinish: (e: Error | undefined, result: T | undefined) => void + onFinish: (e: Error | undefined, result: T | undefined) => void, + preventThrowingError?: boolean ): T { let error: Error | undefined; let result: T | undefined; @@ -33,7 +34,7 @@ export function safeExecuteInTheMiddle( error = e; } finally { onFinish(error, result); - if (error) { + if (error && !preventThrowingError) { // eslint-disable-next-line no-unsafe-finally throw error; } diff --git a/packages/opentelemetry-instrumentation/test/common/utils.test.ts b/packages/opentelemetry-instrumentation/test/common/utils.test.ts index a3e7439870..805490ea15 100644 --- a/packages/opentelemetry-instrumentation/test/common/utils.test.ts +++ b/packages/opentelemetry-instrumentation/test/common/utils.test.ts @@ -15,7 +15,7 @@ */ import * as assert from 'assert'; -import { isWrapped } from '../../src'; +import { isWrapped, safeExecuteInTheMiddle } from '../../src'; describe('isWrapped', () => { describe('when function is wrapped', () => { @@ -42,3 +42,45 @@ describe('isWrapped', () => { }); }); }); + +describe('safeExecuteInTheMiddle', () => { + it('should not throw error', () => { + const error = new Error('test'); + safeExecuteInTheMiddle( + () => { + throw error; + }, + err => { + assert.deepStrictEqual(error, err); + }, + true + ); + }); + it('should throw error', () => { + const error = new Error('test'); + try { + safeExecuteInTheMiddle( + () => { + throw error; + }, + err => { + assert.deepStrictEqual(error, err); + } + ); + } catch (err) { + assert.deepStrictEqual(error, err); + } + }); + it('should return result', () => { + const result = safeExecuteInTheMiddle( + () => { + return 1; + }, + (err, result) => { + assert.deepStrictEqual(err, undefined); + assert.deepStrictEqual(result, 1); + } + ); + assert.deepStrictEqual(result, 1); + }); +}); From 1a24f40266c32028e12dceaae70723a969f14cc8 Mon Sep 17 00:00:00 2001 From: Shovnik Bhattacharya Date: Wed, 9 Dec 2020 15:52:46 -0500 Subject: [PATCH 4/4] Migrate CircleCI unit test workflow to Github Actions (#1711) --- .circleci/checksum.sh | 27 ---- .circleci/config.yml | 155 --------------------- .github/workflows/backcompat.yml | 5 +- .github/workflows/lint.yml | 5 +- .github/workflows/unit-test.yml | 64 +++++++++ .github/workflows/w3c-integration-test.yml | 5 +- 6 files changed, 76 insertions(+), 185 deletions(-) delete mode 100644 .circleci/checksum.sh delete mode 100644 .circleci/config.yml create mode 100644 .github/workflows/unit-test.yml diff --git a/.circleci/checksum.sh b/.circleci/checksum.sh deleted file mode 100644 index fa7cab9ae9..0000000000 --- a/.circleci/checksum.sh +++ /dev/null @@ -1,27 +0,0 @@ -#! /bin/sh -# -# Usage: checksum.sh filename -# -# checksum.sh computes the checksum of the repo's top level `package.json` -# and `package.json` files in package/, putting the hashes into a file in -# alphabetical order. Must be run at the top level of the repository. - - -if [ -z $1 ]; then - echo "Usage: checksum.sh filename" - exit 1 -fi - -FILE=$1 - -# remove existing file -if [ -f $FILE ]; then - rm $FILE -fi - -openssl md5 package.json >> $FILE - -find packages/*/package.json | xargs -I{} openssl md5 {} >> $FILE -find metapackages/*/package.json | xargs -I{} openssl md5 {} >> $FILE - -sort -o $FILE $FILE diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 3fd0472dc2..0000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,155 +0,0 @@ -version: 2 - -node_test_env: &node_test_env - NPM_CONFIG_UNSAFE_PERM: true - - -cache_1: &cache_1 - key: npm-cache-01-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-20B74F85 - paths: - - ./node_modules - - ./package-lock.json - - packages/opentelemetry-api/node_modules - - packages/opentelemetry-context-async-hooks/node_modules - - packages/opentelemetry-context-base/node_modules - - packages/opentelemetry-context-zone/node_modules - - packages/opentelemetry-context-zone-peer-dep/node_modules - - packages/opentelemetry-core/node_modules - - packages/opentelemetry-exporter-jaeger/node_modules - - packages/opentelemetry-exporter-prometheus/node_modules - - packages/opentelemetry-exporter-zipkin/node_modules - - packages/opentelemetry-metrics/node_modules - - packages/opentelemetry-node/node_modules - - packages/opentelemetry-shim-opentracing/node_modules - - packages/opentelemetry-tracing/node_modules - - packages/opentelemetry-web/node_modules - -cache_2: &cache_2 - key: npm-cache-02-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-20B74F85 - paths: - - packages/opentelemetry-plugin-grpc/node_modules - - packages/opentelemetry-plugin-http/node_modules - - packages/opentelemetry-plugin-https/node_modules - - packages/opentelemetry-exporter-collector/node_modules - - packages/opentelemetry-instrumentation-http/node_modules - - packages/opentelemetry-instrumentation-xml-http-request/node_modules - - packages/opentelemetry-resource-detector-aws/node_modules - - packages/opentelemetry-resource-detector-gcp/node_modules - - packages/opentelemetry-resources/node_modules - -node_unit_tests: &node_unit_tests - resource_class: large - steps: - - checkout - - run: - name: Create Checksum - command: sh .circleci/checksum.sh /tmp/checksums.txt - - run: - name: Setup environment variables - command: | - echo "export CIRCLE_NODE_VERSION=\$(node --version | grep -oE 'v[0-9]+')" >> $BASH_ENV - source $BASH_ENV - - run: - name: Log out node.js version - command: | - node --version - echo "CIRCLE_NODE_VERSION=${CIRCLE_NODE_VERSION}" - - restore_cache: - keys: - - npm-cache-01-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-20B74F85 - - restore_cache: - keys: - - npm-cache-02-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-20B74F85 - - run: - name: Install Root Dependencies - command: npm install --ignore-scripts - - run: - name: Boostrap dependencies - command: npx lerna bootstrap --no-ci - - save_cache: - <<: *cache_1 - - save_cache: - <<: *cache_2 - - run: - name: Unit tests - command: npm run test - - run: - name: report coverage - command: if [ "${CIRCLE_NODE_VERSION}" = "v12" ]; then npm run codecov; fi - -browsers_unit_tests: &browsers_unit_tests - resource_class: large - steps: - - checkout - - run: - name: Create Checksum - command: sh .circleci/checksum.sh /tmp/checksums.txt - - run: - name: Setup environment variables - command: | - echo "export CIRCLE_NODE_VERSION=\$(node --version | grep -oE 'v[0-9]+')" >> $BASH_ENV - source $BASH_ENV - - run: - name: Log out node.js version - command: | - node --version - echo "CIRCLE_NODE_VERSION=${CIRCLE_NODE_VERSION}" - - restore_cache: - keys: - - npm-cache-01-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-20B74F85 - - restore_cache: - keys: - - npm-cache-02-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-20B74F85 - - run: - name: Install Root Dependencies - command: npm install --ignore-scripts - - run: - name: Boostrap dependencies - command: npx lerna bootstrap --no-ci - - save_cache: - <<: *cache_1 - - save_cache: - <<: *cache_2 - - run: - name: Unit tests - command: npm run test:browser - - run: - name: report coverage - command: if [ "$CIRCLE_NODE_VERSION" = "v12" ]; then npm run codecov:browser; fi - -jobs: - node8: - docker: - - image: node:8 - environment: *node_test_env - <<: *node_unit_tests - node10: - docker: - - image: node:10 - environment: *node_test_env - <<: *node_unit_tests - node12: - docker: - - image: node:12 - environment: *node_test_env - <<: *node_unit_tests - node14: - docker: - - image: node:14 - environment: *node_test_env - <<: *node_unit_tests - node12-browsers: - docker: - - image: circleci/node:12-browsers - <<: *browsers_unit_tests - -workflows: - version: 2 - build: - jobs: - - node8 - - node10 - - node12 - - node14 - - node12-browsers - diff --git a/.github/workflows/backcompat.yml b/.github/workflows/backcompat.yml index 58121302e9..93ef7428e2 100644 --- a/.github/workflows/backcompat.yml +++ b/.github/workflows/backcompat.yml @@ -1,6 +1,9 @@ name: Backwards Compatability -on: [push, pull_request] +on: + push: + branches: [master] + pull_request: jobs: types-node: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index de1c99048d..f24f2dd2c5 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,6 +1,9 @@ name: Lint -on: [push, pull_request] +on: + push: + branches: [master] + pull_request: jobs: build: diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml new file mode 100644 index 0000000000..a9e94981c4 --- /dev/null +++ b/.github/workflows/unit-test.yml @@ -0,0 +1,64 @@ +name: Unit Tests +on: + push: + branches: [master] + pull_request: + +jobs: + unit-test: + strategy: + fail-fast: false + matrix: + container: ["node:8", "node:10", "node:12", "node:14"] + runs-on: ubuntu-latest + container: + image: ${{ matrix.container }} + env: + NPM_CONFIG_UNSAFE_PERM: true + steps: + - name: Checkout + uses: actions/checkout@v1 + - name: Cache Dependencies + uses: actions/cache@v2 + with: + path: | + node_modules + package-lock.json + packages/*/node_modules + key: ${{ runner.os }}-${{ matrix.container }}-${{ hashFiles('**/package.json') }} + - name: Install Root Dependencies + run: npm install --ignore-scripts + - name: Boostrap Dependencies + run: npx lerna bootstrap --no-ci + - name: Unit tests + run: npm run test + - name: Report Coverage + run: npm run codecov + if: ${{ matrix.container }} == 'node:12' + browser-tests: + runs-on: ubuntu-latest + container: + image: circleci/node:12-browsers + env: + NPM_CONFIG_UNSAFE_PERM: true + steps: + - name: Checkout + uses: actions/checkout@v1 + - name: Permission Setup + run: sudo chmod -R 777 /github /__w + - name: Cache Dependencies + uses: actions/cache@v2 + with: + path: | + node_modules + package-lock.json + packages/*/node_modules + key: ${{ runner.os }}-node:12-${{ hashFiles('**/package.json') }} + - name: Install Root Dependencies + run: npm install --ignore-scripts + - name: Boostrap Dependencies + run: npx lerna bootstrap --no-ci + - name: Unit tests + run: npm run test:browser + - name: Report Coverage + run: npm run codecov:browser diff --git a/.github/workflows/w3c-integration-test.yml b/.github/workflows/w3c-integration-test.yml index c62ec8a0b7..4a1864d93e 100644 --- a/.github/workflows/w3c-integration-test.yml +++ b/.github/workflows/w3c-integration-test.yml @@ -1,6 +1,9 @@ name: Run w3c tests on push -on: [push] +on: + push: + branches: [master] + pull_request: jobs: build-and-deploy: