├── .DS_Store ├── .bumpversion.cfg ├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .travis.yml ├── @types ├── event-pubsub │ └── index.d.ts ├── fastest-validator │ └── index.d.ts ├── find-my-way │ └── index.d.ts └── node-fetch │ └── index.d.ts ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── config ├── client.yml └── config.yml ├── docs ├── ipc.md └── requests.http ├── gulpfile.js ├── makefile ├── media └── request_lifecycle.png ├── package-lock.json ├── package.json ├── src ├── AppServer.ts ├── Dispatcher.ts ├── StaticData.ts ├── bus │ ├── index.ts │ ├── interfaces.ts │ ├── tree.ts │ └── tree_name.ts ├── constants │ ├── common.ts │ ├── errors.ts │ ├── http.ts │ └── index.ts ├── enrichers │ ├── fingerprint.ts │ ├── index.ts │ └── userdata.ts ├── errors.ts ├── handlers │ ├── hello.ts │ ├── index.ts │ ├── pixel.ts │ ├── redirect.ts │ ├── static.ts │ └── track.ts ├── helpers │ ├── auto-domain.ts │ ├── class.ts │ ├── common.ts │ ├── cybr53.ts │ ├── file-loader.ts │ ├── getprop.ts │ ├── http.ts │ ├── index.ts │ ├── remove-www.ts │ ├── simpleHash.ts │ ├── stringTemplate.ts │ └── validation.ts ├── http │ ├── http_server.ts │ ├── index.ts │ └── ws_server.ts ├── start.ts ├── stores │ └── StubStore.ts ├── transformers │ ├── common.ts │ ├── index.ts │ ├── mp_android.ts │ ├── mp_ios.ts │ ├── mp_ios_native.ts │ ├── ph_native.ts │ └── track_batch.ts └── types │ ├── base.ts │ ├── config.ts │ ├── http.ts │ ├── index.ts │ └── msg.ts ├── test └── url_safe.js ├── tsconfig.json ├── tslint.json ├── var └── .keep └── web-sdk-dist └── .keep /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rockstat/front/d3c456850c5f529e80b8b42a1029b78781eb8833/.DS_Store -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 4.0.1 3 | commit = True 4 | tag = False 5 | 6 | # [bumpversion:file:package.json] 7 | 8 | # [bumpversion:file:Dockerfile] 9 | 10 | # [bumpversion:file:config/config.yml] 11 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules/ 3 | .yarn/ 4 | Dockerfile 5 | .dockerignore 6 | .env 7 | .env.local 8 | dist 9 | /public 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 2 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /node_modules 3 | /yarn-error.log 4 | /deploy 5 | /certs 6 | /static/lib* 7 | /.env* 8 | TextApp* 9 | /build 10 | /dist 11 | .pytest_cache 12 | /public 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | branches: 4 | only: 5 | - master 6 | - dev 7 | env: 8 | global: 9 | - PROJECT_NAME=front 10 | - ORG=rockstat 11 | - IMAGE_NAME=$ORG/$PROJECT_NAME 12 | - TAG="$( [ $TRAVIS_BRANCH == 'master' ] && echo latest || echo $TRAVIS_BRANCH )" 13 | - secure: bB5Y+bokrlZZ33JcVHEBWER19qRT4rDIDdBaInax41Gt+XtHo4lvj+abj5sv92W36XjZs8O0sF4wa0oSKEniBeEjBAAjtPD6dSet+GHzHlledUH/VWA3+nN/mKv0ImeiWTHmC2tPYhxkeRzeREGeCwZMsxk3NSSkLAJh11Z+ikgaJgFq7oB8bUM66RgaYvYlCu22Sx7KR9zdjlrNJWkEsHVYy45KC+pRzi2hzAOKyhlT7mL1hEw8gxkBUayPw1SG10y0ByxdA3T9368oi3oJhluXALI/tRCYPNZRh4uGKtbGfu0W+qx+bbtLHKE5v9itYeRDuoP60w8heMs7MNqyMWMrP1lZy3fojTHyudKI2HKC8NC1/3cv+cYgHNGUITBRRcCnxh826nGdvVkU5IJd8U4PemOdqm/XsyYRsZAXcV3sxD84hJgRPcLJqqCYOyie0BQh8U8vb3MqQoQJifMadDHoFqMBNGihpZECH6FZcEynSsXNo/vJU3DLh+ChAwWlm9xhm0uhD/yzFSnH9+cAUNbX6wM3xLUZeXFXcbJ7NffP80tSIW9wWNJwNLedtzgg0vOpdgCm9lIeu3ZN91MYZxnu/6BdJA4F7zgF7YUkaJXB/8H7RaPrqdhTXfLgIlPhF+CzL3M5sxR/y58DD5VgO8Yuj+Me9JpqRHBzjpFfa5I= 14 | services: 15 | - docker 16 | script: 17 | - echo "$PROJECT_NAME $IMAGE_NAME:$TAG" 18 | - docker build -t $PROJECT_NAME . --build-arg WEB_SDK_CONTAINER=rockstat/web-sdk:$TAG --build-arg BASE_CONTAINER=rockstat/band-base-ts:$TAG 19 | after_script: 20 | - docker images 21 | before_deploy: 22 | - docker tag "$PROJECT_NAME" "$IMAGE_NAME:$TAG" 23 | - docker login -u "$DOCKER_USERNAME" -p "$DOCKER_PASSWORD" 24 | deploy: 25 | provider: script 26 | skip_cleanup: true 27 | script: docker push "$IMAGE_NAME:$TAG" 28 | on: 29 | all_branches: true -------------------------------------------------------------------------------- /@types/event-pubsub/index.d.ts: -------------------------------------------------------------------------------- 1 | 2 | /// 3 | 4 | declare module 'event-pubsub/es6' { 5 | 6 | class EventPubSub { 7 | constructor(scope?: any); 8 | on(type: string, handler: (type: string, ...args: any[]) => void, once?: boolean): void; 9 | once(type: string, handler: (type: string, ...args: any[]) => void): void; 10 | off(type: string, handler: (type: string, ...args: any[]) => void): void; 11 | emit(type: string, ...args: any[]): void; 12 | } 13 | 14 | namespace EventPubSub { } 15 | export = EventPubSub; 16 | 17 | } 18 | 19 | -------------------------------------------------------------------------------- /@types/fastest-validator/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module 'fastest-validator'; 4 | -------------------------------------------------------------------------------- /@types/find-my-way/index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for find my way router 2 | // Project: https://github.com/delvedor/find-my-way 3 | // Definitions by: Dmitry Rodin 4 | // TypeScript Version: 2.7 5 | 6 | /// 7 | 8 | declare module 'find-my-way' { 9 | 10 | import { IncomingMessage, ServerResponse } from 'http'; 11 | 12 | type RouteParams = { [key: string]: string }; 13 | type RouteStore = { [key: string]: any }; 14 | type RouteResult = { [key: string]: any }; 15 | 16 | interface RequestHandler { 17 | // (req: IncomingMessage, res: ServerResponse, params: RouteParams, store: RouteStore): any 18 | (result: RouteResult): any 19 | } 20 | 21 | interface RouterOptions { 22 | defaultRoute: RequestHandler; 23 | ignoreTrailingSlash: boolean 24 | } 25 | 26 | interface RouterResult { 27 | method: string; 28 | path: string; 29 | handler: RequestHandler; 30 | store: RouteStore; 31 | params?: RouteParams; 32 | } 33 | 34 | class Router { 35 | constructor(opts?: RouterOptions); 36 | on(method: string, path: string, handler: RequestHandler, store?: RouteStore): void; 37 | get(path: string, handler: RequestHandler, store?: RouteStore): void; 38 | post(path: string, handler: RequestHandler, store?: RouteStore): void; 39 | options(path: string, handler: RequestHandler, store?: RouteStore): void; 40 | all(path: string, handler: RequestHandler, store?: RouteStore): void; 41 | lookup(req: IncomingMessage, res: ServerResponse): void; 42 | find(method: string, path: string): RouterResult | undefined; 43 | prettyPrint(): string; 44 | } 45 | 46 | namespace Router { } 47 | export = Router; 48 | } 49 | 50 | /** Declaration file generated by dts-gen */ 51 | 52 | // export = find_my_way; 53 | 54 | // declare class find_my_way { 55 | 56 | // constructor(opts: any); 57 | 58 | // acl(path: any, handler: any, store: any): any; 59 | 60 | // all(path: any, handler: any, store: any): void; 61 | 62 | // bind(path: any, handler: any, store: any): any; 63 | 64 | // checkout(path: any, handler: any, store: any): any; 65 | 66 | // connect(path: any, handler: any, store: any): any; 67 | 68 | // copy(path: any, handler: any, store: any): any; 69 | 70 | // delete(path: any, handler: any, store: any): any; 71 | 72 | // find(method: any, path: any): any; 73 | 74 | // get(path: any, handler: any, store: any): any; 75 | 76 | // head(path: any, handler: any, store: any): any; 77 | 78 | // link(path: any, handler: any, store: any): any; 79 | 80 | // lock(path: any, handler: any, store: any): any; 81 | 82 | // lookup(req: any, res: any): any; 83 | 84 | // "m-search"(path: any, handler: any, store: any): any; 85 | 86 | // merge(path: any, handler: any, store: any): any; 87 | 88 | // mkactivity(path: any, handler: any, store: any): any; 89 | 90 | // mkcalendar(path: any, handler: any, store: any): any; 91 | 92 | // mkcol(path: any, handler: any, store: any): any; 93 | 94 | // move(path: any, handler: any, store: any): any; 95 | 96 | // notify(path: any, handler: any, store: any): any; 97 | 98 | // off(method: any, path: any): any; 99 | 100 | // on(method: any, path: any, handler: any, store: any): void; 101 | 102 | // options(path: any, handler: any, store: any): any; 103 | 104 | // patch(path: any, handler: any, store: any): any; 105 | 106 | // post(path: any, handler: any, store: any): any; 107 | 108 | // prettyPrint(): any; 109 | 110 | // propfind(path: any, handler: any, store: any): any; 111 | 112 | // proppatch(path: any, handler: any, store: any): any; 113 | 114 | // purge(path: any, handler: any, store: any): any; 115 | 116 | // put(path: any, handler: any, store: any): any; 117 | 118 | // rebind(path: any, handler: any, store: any): any; 119 | 120 | // report(path: any, handler: any, store: any): any; 121 | 122 | // reset(): void; 123 | 124 | // search(path: any, handler: any, store: any): any; 125 | 126 | // subscribe(path: any, handler: any, store: any): any; 127 | 128 | // trace(path: any, handler: any, store: any): any; 129 | 130 | // unbind(path: any, handler: any, store: any): any; 131 | 132 | // unlink(path: any, handler: any, store: any): any; 133 | 134 | // unlock(path: any, handler: any, store: any): any; 135 | 136 | // unsubscribe(path: any, handler: any, store: any): any; 137 | 138 | // } 139 | 140 | // declare namespace find_my_way { 141 | 142 | // function acl(path: any, handler: any, store: any): any; 143 | 144 | // function all(path: any, handler: any, store: any): void; 145 | 146 | // function bind(path: any, handler: any, store: any): any; 147 | 148 | // function checkout(path: any, handler: any, store: any): any; 149 | 150 | // function connect(path: any, handler: any, store: any): any; 151 | 152 | // function copy(path: any, handler: any, store: any): any; 153 | 154 | // function find(method: any, path: any): any; 155 | 156 | // function get(path: any, handler: any, store: any): any; 157 | 158 | // function head(path: any, handler: any, store: any): any; 159 | 160 | // function link(path: any, handler: any, store: any): any; 161 | 162 | // function lock(path: any, handler: any, store: any): any; 163 | 164 | // function lookup(req: any, res: any): any; 165 | 166 | // function merge(path: any, handler: any, store: any): any; 167 | 168 | // function mkactivity(path: any, handler: any, store: any): any; 169 | 170 | // function mkcalendar(path: any, handler: any, store: any): any; 171 | 172 | // function mkcol(path: any, handler: any, store: any): any; 173 | 174 | // function move(path: any, handler: any, store: any): any; 175 | 176 | // function notify(path: any, handler: any, store: any): any; 177 | 178 | // function off(method: any, path: any): any; 179 | 180 | // function on(method: any, path: any, handler: any, store: any): void; 181 | 182 | // function options(path: any, handler: any, store: any): any; 183 | 184 | // function patch(path: any, handler: any, store: any): any; 185 | 186 | // function post(path: any, handler: any, store: any): any; 187 | 188 | // function prettyPrint(): any; 189 | 190 | // function propfind(path: any, handler: any, store: any): any; 191 | 192 | // function proppatch(path: any, handler: any, store: any): any; 193 | 194 | // function purge(path: any, handler: any, store: any): any; 195 | 196 | // function put(path: any, handler: any, store: any): any; 197 | 198 | // function rebind(path: any, handler: any, store: any): any; 199 | 200 | // function report(path: any, handler: any, store: any): any; 201 | 202 | // function reset(): void; 203 | 204 | // function search(path: any, handler: any, store: any): any; 205 | 206 | // function subscribe(path: any, handler: any, store: any): any; 207 | 208 | // function trace(path: any, handler: any, store: any): any; 209 | 210 | // function unbind(path: any, handler: any, store: any): any; 211 | 212 | // function unlink(path: any, handler: any, store: any): any; 213 | 214 | // function unlock(path: any, handler: any, store: any): any; 215 | 216 | // function unsubscribe(path: any, handler: any, store: any): any; 217 | 218 | // } 219 | 220 | -------------------------------------------------------------------------------- /@types/node-fetch/index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for node-fetch v2.1.1 2 | // Project: https://github.com/bitinn/node-fetch 3 | // Definitions by: Dmitry Rodin 4 | // Based on https://github.com/torstenwerner 5 | /// 6 | 7 | declare module 'node-fetch' { 8 | 9 | import { Agent } from "http"; 10 | 11 | export class Request extends Body { 12 | constructor(input: string | Request, init?: RequestInit); 13 | method: string; 14 | url: string; 15 | headers: Headers; 16 | referrer: string; 17 | redirect: RequestRedirect; 18 | clone: Request; 19 | 20 | //node-fetch extensions to the whatwg/fetch spec 21 | compress: boolean; 22 | agent?: Agent; 23 | counter: number; 24 | follow: number; 25 | 26 | hostname: string; 27 | protocol: string; 28 | port?: number; 29 | timeout: number; 30 | size: number; 31 | } 32 | 33 | interface RequestInit { 34 | //whatwg/fetch standard options 35 | method?: string; 36 | headers?: HeaderInit | { [index: string]: string }; 37 | body?: BodyInit; 38 | redirect?: RequestRedirect; 39 | 40 | //node-fetch extensions 41 | timeout?: number; //=0 req/res timeout in ms, it resets on redirect. 0 to disable (OS limit applies) 42 | compress?: boolean; //=true support gzip/deflate content encoding. false to disable 43 | size?: number; //=0 maximum response body size in bytes. 0 to disable 44 | agent?: Agent; //=null http.Agent instance, allows custom proxy, certificate etc. 45 | follow?: number; //=20 maximum redirect count. 0 to not follow redirect 46 | //node-fetch does not support mode, cache or credentials options 47 | } 48 | 49 | type RequestMode = "same-origin" | "no-cors" | "cors"; 50 | type RequestRedirect = "follow" | "error" | "manual"; 51 | type RequestCredentials = "omit" | "same-origin" | "include"; 52 | type RequestCache = 53 | "default" | "no-store" | "reload" | "no-cache" | 54 | "force-cache" | "only-if-cached"; 55 | 56 | export class Headers { 57 | append(name: string, value: string): void; 58 | delete(name: string): void; 59 | get(name: string): string | undefined | null; 60 | has(name: string): boolean; 61 | set(name: string, value: string): void; 62 | forEach(callback: (value: string, name: string, thisArg?: any) => void, thisArg?: any): void; 63 | raw(): object; 64 | keys: Iterator 65 | values: Iterator 66 | } 67 | 68 | export class Body { 69 | bodyUsed: boolean; 70 | body: NodeJS.ReadableStream; 71 | arrayBuffer: Promise; 72 | blob: Promise 73 | timeout: number; 74 | size: number; 75 | buffer(): Promise; 76 | json(): Promise; 77 | json(): Promise; 78 | text(): Promise; 79 | textConverted: Promise; 80 | } 81 | 82 | export class Response extends Body { 83 | constructor(body?: BodyInit, opts?: ResponseInit); 84 | static error(): Response; 85 | static redirect(url: string, status: number): Response; 86 | type: ResponseType; 87 | url: string; 88 | status: number; 89 | ok: boolean; 90 | size: number; 91 | statusText: string; 92 | timeout: number; 93 | headers: Headers; 94 | clone(): Response; 95 | } 96 | 97 | export class FetchError extends Error { 98 | constructor(message: string, type: string, systemError?: Error) 99 | name: string; 100 | message: string; 101 | type: string; 102 | code: number; 103 | errno: number; 104 | } 105 | 106 | type ResponseType = "basic" | "cors" | "default" | "error" | "opaque" | "opaqueredirect"; 107 | 108 | interface ResponseInit { 109 | status: number; 110 | url: string; 111 | statusText?: string; 112 | headers?: HeaderInit; 113 | } 114 | 115 | type HeaderInit = Headers | Array; 116 | type BodyInit = ArrayBuffer | ArrayBufferView | string | NodeJS.ReadableStream; 117 | type RequestInfo = Request | string; 118 | 119 | export default function fetch(url: string | Request, init?: RequestInit): Promise; 120 | } 121 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # UAParser.js Code of Conduct 2 | 3 | ## Introduction 4 | 5 | Welcome to the Rockstat community! We're here to collaborate on developing an awesome project. Here are some general guidelines to make our community a great place: 6 | 7 | ### 1. Be Kind, Honest, and Respectful 8 | 9 | Always treat others with kindness and respect. We value different opinions and encourage positive communication. 10 | 11 | ### 2. Keep Conversations Civil and On-Topic 12 | 13 | Please keep discussions related to the project. If you want to talk about something else, find the right place for it. 14 | 15 | ### 3. Mutual Assistance, Appreciation, and Acknowledgement 16 | 17 | Feel free to ask for help, show gratitude for contributions, and make sure to give credit where it's due. 18 | 19 | ### 4. Resolving Disagreements 20 | 21 | In the event of a disagreement, we encourage open and respectful dialogue. It's important to remember that it's okay to have differing opinions, and if a common ground can't be reached, we suggest using the 'agree to disagree' approach. 22 | 23 | ## Reporting Issues 24 | 25 | If you see any behavior that goes against this code of conduct, report it to madiedinro@gmail.com. 26 | 27 | ## Conclusion 28 | 29 | Together, we can make this project awesome! 30 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # UAParser.js: How to Contribute 2 | 3 | * Fork and clone this repository 4 | * Make some changes as required 5 | * Write unit test to showcase its functionality under `/test` 6 | * Run the test suites to make sure it's not breaking anything `$ npm run test` 7 | * Submit a pull request under `*-dev` branch & check the CLA in the submission form 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG WEB_SDK_CONTAINER=rockstat/web-sdk:ng 2 | ARG BASE_CONTAINER=rockstat/band-base-ts:ng 3 | 4 | FROM $WEB_SDK_CONTAINER as web-sdk-build 5 | 6 | FROM $BASE_CONTAINER 7 | 8 | ENV PORT 8080 9 | ENV LOG_LEVEL debug 10 | 11 | WORKDIR /app/front 12 | 13 | ARG NPM_CONFIG_REGISTRY_ARG=https://registry.npmjs.org 14 | ENV NPM_CONFIG_REGISTRY=$NPM_CONFIG_REGISTRY_ARG 15 | 16 | COPY package.json . 17 | # COPY package-lock.json . 18 | 19 | RUN npm i --loglevel http && npm cache clean --force 20 | RUN cp -r /usr/src/rockme /app/rockmets 21 | COPY --from=web-sdk-build /usr/share/web-sdk /app/web-sdk 22 | 23 | COPY . . 24 | 25 | RUN npm run build 26 | 27 | EXPOSE 8080 28 | ENV NODE_ENV production 29 | ENV REDIS_DSN redis://redis:6379 30 | 31 | CMD ["npm", "run", "start"] 32 | # CMD ["node", "-r", "source-map-support/register", "./dist/start.js"] 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Dmitry Rodin 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | Front service of Rockstat 4 | 5 | ## About Rockstat 6 | 7 | Is an open source platform for a web and product analytics. 8 | It consists of a set of components: JavaScript tracking client for web applications; 9 | server-side data collector; services for geo-coding and detecting client device type; 10 | a new server deployment system. 11 | [Read more](https://rockstat.ru/about) 12 | 13 | ## About Rockstat Front 14 | 15 | Front service is a entrypoint for all external data. 16 | 17 | Look at the scheme 18 | 19 | ![Rockstat sheme](https://rock.st/static/images/docs/schemas/request-lifecycle.svg) 20 | 21 | ## Envs and defaults 22 | 23 | REDIS_URL=redis://127.0.0.1:6379 24 | STATSD_HOST=127.0.0.1 25 | 26 | HOST=0.0.0.0 27 | PORT=8080 28 | PORT_WS=8082 29 | PORT_WSS=8083 30 | 31 | LOG_LEVEL=info 32 | 33 | DATACENTER_ID=1 34 | WORKER_ID=1 35 | 36 | ## Building 37 | 38 | Source maps options 39 | 40 | "sourceMap": true, 41 | 42 | or 43 | 44 | "inlineSourceMap": true, 45 | 46 | ## License 47 | 48 | [LICENSE](LICENSE) 49 | -------------------------------------------------------------------------------- /config/client.yml: -------------------------------------------------------------------------------- 1 | # --- 2 | # client: 3 | # common: 4 | # trackClicks: true 5 | # trackForms: true 6 | # pixelSyncEnabled: true 7 | # trackActivity: true 8 | # allowHTTP: true 9 | # cookieDomain: 'auto' 10 | # # allowSendBeacon: false 11 | # # allowXHR: false 12 | # activateWs: true 13 | -------------------------------------------------------------------------------- /config/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Rockstat Front service 3 | selected_events_dir: /front_selected_events 4 | version: "<%= pkg.version %>" 5 | identify: 6 | param: uid # cookie param name 7 | cookieMaxAge: <%= 3 * 365 * 24 * 60 * 60 %> 8 | cookieDomain: auto 9 | cookiePath: / 10 | # have to prefixed with "." 11 | domain: <%= env.DOMAIN %> 12 | fallback_domain: <%= env.DOMAIN %> 13 | 14 | 15 | http: 16 | host: <%= env.HOST || '0.0.0.0' %> 17 | port: <%= env.PORT || '8080' %> 18 | # mark in url segment which tell return native response 19 | url_mark: band 20 | # services to channels map 21 | sevices_map: 22 | # service: channel 23 | # channels details look at src/constants/common.ts 24 | away: redir 25 | go: redir 26 | gif: pixel 27 | # gif: pixel 28 | # gifz: pixel 29 | # t4k: track 30 | # service_identify_map: 31 | # gifz: 'z' 32 | # gifn: 'n' 33 | services_params: 34 | gifq: 35 | alias_for: pixel 36 | uid_param: uidq 37 | collect_cookies: ['uid'] 38 | t4k: 39 | alias_for: track 40 | dig: yes 41 | url_check_websdk: yes 42 | action_params: 43 | ping38: 44 | collect_all_cookies: true 45 | remove_cookies: 46 | REMEMBERME: true 47 | PHPSESSID: true 48 | jwt_decode: 49 | jwt_bridge: true 50 | user_token: true 51 | 52 | 53 | 54 | 55 | websocket: 56 | http: 57 | host: <%= env.HOST || '0.0.0.0' %> 58 | port: <%= env.PORT_WS || env.PORT && Number(env.PORT) + 2 || '8082' %> 59 | path: /wss 60 | perMessageDeflate: # See zlib defaults. 61 | zlibDeflateOptions: 62 | chunkSize: 1024 63 | memLevel: 7 64 | level: 3 65 | zlibInflateOptions: 66 | chunkSize: <%= 10 * 1024 %> 67 | # Other options settable: 68 | clientNoContextTakeover: true # Defaults to negotiated value. 69 | serverNoContextTakeover: true # Defaults to negotiated value. 70 | clientMaxWindowBits: 10 # Defaults to negotiated value. 71 | serverMaxWindowBits: 10 # Defaults to negotiated value. 72 | # Below options specified as default values. 73 | concurrencyLimit: 10 # Limits zlib concurrency for perf. 74 | threshold: 1024 # Size (in bytes) below which messages 75 | # should not be compressed. 76 | 77 | rpc: 78 | name: front_<%= env.WORKER_ID || '1' %> 79 | channels: 80 | - front_<%= env.WORKER_ID || '1' %> 81 | - director 82 | listen_all: false 83 | listen_direct: true 84 | 85 | redis: 86 | dsn: <%= env.REDIS_DSN || 'redis://127.0.0.1:6379' %> 87 | log: 88 | use: pino 89 | pino: 90 | safe: true 91 | level: <%= env.LOG_LEVEL || 'info' %> 92 | prettyPrint: false 93 | 94 | client: {} # do not remove. root element should be present 95 | static: 96 | # lib.js: "node_modules/@rockstat/web_sdk/dist/lib.js" 97 | lib.js: "node_modules/@rockstat/web_sdk/dist/lib.js" 98 | lib-dev.js: "node_modules/@rockstat/web_sdk/dist/lib-dev.js" 99 | # env depended variables 100 | production: {} 101 | dev: {} 102 | bus: 103 | enrichers: 104 | # FingerPrintEnricher: [<%= IN_GENERIC %>.<%= SERVICE_TRACK %>] 105 | UserDataEnricher: [<%= IN_GENERIC %>.<%= SERVICE_TRACK %>, <%= IN_GENERIC %>.userdata] 106 | handlers: 107 | PixelHandler: <%= IN_GENERIC %>.<%= SERVICE_PIXEL %> 108 | RedirectHandler: <%= IN_GENERIC %>.<%= SERVICE_REDIR %> 109 | TrackHandler: <%= IN_GENERIC %>.<%= SERVICE_TRACK %> 110 | HelloHandler: <%= IN_GENERIC %>.hello 111 | StaticHandler: <%= IN_GENERIC %>.static 112 | transformers: 113 | MPAndroidTransformer: 114 | - <%= IN_GENERIC %>.app-track.events 115 | - <%= IN_GENERIC %>.mp_handler.events_betand 116 | - <%= IN_GENERIC %>.mp_handler.events_mbc 117 | MPIOSTransformer: 118 | - <%= IN_GENERIC %>.app-track-ios.track-ios 119 | - <%= IN_GENERIC %>.app-track-ios.events 120 | - <%= IN_GENERIC %>.mp_handler.track-ios 121 | MPIOSNativeTransformer: 122 | - <%= IN_GENERIC %>.ios.track 123 | PHNativeTransformer: 124 | - <%= IN_GENERIC %>.posthog.batch 125 | 126 | #!!js/undefined 127 | 128 | metrics: 129 | statsd: 130 | host: <%= env.STATSD_HOST || '127.0.0.1' %> 131 | port: 8125 132 | debug: false 133 | prefix: krn 134 | -------------------------------------------------------------------------------- /docs/ipc.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Configuration at dispatcher 4 | 5 | import * as ipc from 'node-ipc'; 6 | 7 | sock: string = 'var/alco.sock'; 8 | 9 | 10 | Object.assign(ipc.config, confugurer.ipcConfig); 11 | ipc.config.logger = this.log.debug.bind(this.log); 12 | 13 | 14 | startIPC() { 15 | 16 | this.log.info('Starting IPC'); 17 | ipc.serve(() => { 18 | ipc.server.on('message', (data, socket) => { 19 | ipc.log(data, 'got a message'); 20 | }); 21 | }); 22 | 23 | ipc.server.start(); 24 | } 25 | 26 | 27 | 28 | this.startIPC(); 29 | -------------------------------------------------------------------------------- /docs/requests.http: -------------------------------------------------------------------------------- 1 | 2 | https://tracker.rockstat.test/custom/hello 3 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const ts = require('gulp-typescript'); 3 | const nodemon = require('gulp-nodemon'); 4 | const del = require('del'); 5 | const pinop = require('pino').pretty; 6 | const pretty = require('pino-pretty'); 7 | 8 | 9 | const prettyConfig = { 10 | colorize: false, // --colorize 11 | // crlf: false, // --crlf 12 | // dateFormat: 'yyyy-mm-dd HH:MM:ss.l o', // --dateFormat 13 | // errorLikeObjectKeys: ['err', 'error'], // --errorLikeObjectKeys 14 | // errorProps: '', // --errorProps 15 | levelFirst: false, // --levelFirst 16 | localTime: false, // --localTime 17 | // messageKey: 'msg', // --messageKey 18 | translateTime: true, // --translateTime 19 | outputStream: process.stdout 20 | } 21 | 22 | const tsProject = ts.createProject('tsconfig.json', { typescript: require('typescript')}); 23 | 24 | gulp.task("nodemon", function(done) { 25 | const prettyfier = pretty(prettyConfig); 26 | const options = { 27 | watch: [ 28 | "dist/", 29 | "clientlib/", 30 | "config/" 31 | ], 32 | ext: 'js ts yml', 33 | script: "dist/start.js", 34 | ignore: ["node_modules/*"], 35 | stdout: false, 36 | readable: false, 37 | delay: 1 38 | } 39 | nodemon(options) 40 | .on("start", done) 41 | .on('readable', function() { 42 | this.stdout.pipe(pinop()).pipe(process.stdout) 43 | this.stderr.pipe(pinop()).pipe(process.stdout) 44 | }) 45 | // .on('crash',['clean']); 46 | // .on('exit',['clean']) 47 | }); 48 | 49 | gulp.task('clean', function() { 50 | return del([ 51 | 'dist/**/*', 52 | ]); 53 | }); 54 | 55 | gulp.task('scripts', function() { 56 | const tsResult = tsProject.src() 57 | .pipe(tsProject()) 58 | .pipe(gulp.dest('dist')); 59 | 60 | return tsResult; 61 | }); 62 | 63 | gulp.task('build', gulp.series('clean', 'scripts')); 64 | 65 | 66 | // https://github.com/gulpjs/gulp/blob/4.0/docs/API.md 67 | gulp.task('watch', gulp.series('clean', 'scripts', 'nodemon', function() { 68 | gulp.watch(['src/**/*.ts'], gulp.series('clean', 'scripts')); 69 | })); 70 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | include .env 2 | 3 | $(eval export $(shell sed -ne 's/ *#.*$$//; /./ s/=.*$$// p' .env)) 4 | 5 | 6 | bump-patch: 7 | bumpversion patch 8 | 9 | bump-minor: 10 | bumpversion minor 11 | 12 | build: 13 | docker build --progress=plain -t front . 14 | 15 | # --build-arg NPM_CONFIG_REGISTRY_ARG=http://host.docker.internal:4873/ 16 | build_macos_arm64: 17 | docker build --platform linux/amd64 --progress=plain --build-arg NPM_TOKEN=$$NPM_TOKEN -t front . 18 | 19 | tag-ng: 20 | docker tag front rockstat/front:ng 21 | 22 | tag-latest: 23 | docker tag front rockstat/front:latest 24 | 25 | push-latest: 26 | docker push rockstat/front:latest 27 | 28 | push-ng: 29 | docker push rockstat/front:ng 30 | 31 | all-ng: build_macos_arm64 tag-ng push-ng 32 | 33 | push-dev: 34 | docker tag front rockstat/front:dev 35 | docker push rockstat/front:dev 36 | 37 | to_master: 38 | @echo $(BR) 39 | git checkout master && git merge $(BR) && git checkout $(BR) 40 | 41 | travis-trigger: 42 | curl -vv -s -X POST \ 43 | -H "Content-Type: application/json" \ 44 | -H "Accept: application/json" \ 45 | -H "Travis-API-Version: 3" \ 46 | -H "Authorization: token $$TRAVIS_TOKEN" \ 47 | -d '{ "request": { "branch":"$(br)" }}' \ 48 | https://api.travis-ci.com/repo/$(subst $(DEL),$(PERCENT)2F,$(repo))/requests 49 | -------------------------------------------------------------------------------- /media/request_lifecycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rockstat/front/d3c456850c5f529e80b8b42a1029b78781eb8833/media/request_lifecycle.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rockstat/front", 3 | "version": "4.4.0", 4 | "description": "Rockstat Front service", 5 | "scripts": { 6 | "start": "ts-node ./src/start.ts", 7 | "start:prod": "node -r 'source-map-support/register' dist/start", 8 | "start:dev": "LOG_LEVEL=debug TS_NODE_BASEURL=./src ts-node-dev --watch ../web-sdk ./src/start.ts | pino-pretty -L debug", 9 | "build": "rimraf dist && tsc -p tsconfig.json", 10 | "clean": "rimraf dist", 11 | "lint": "tslint -c tslint.json -t stylish -p ./tsconfig.json", 12 | "security-check": "nsp check", 13 | "watch": "tsc -w", 14 | "tun": "lt --port 10001 -s rstdev", 15 | "build:push": "git add dist/* && git add package.json && git commit -m build && git push origin HEAD", 16 | "build:patch": "yarn run patch && git add package.json", 17 | "patch": "yarn version --loose-semver --new-version patch" 18 | }, 19 | "license": "Apache-2.0", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/rockstat/front" 23 | }, 24 | "_moduleAliases": { 25 | "@app": "./src" 26 | }, 27 | "author": "Dmitry Rodin ", 28 | "homepage": "https://rock.st", 29 | "devDependencies": { 30 | "@types/content-type": "^1.1.8", 31 | "@types/cookie": "^0.5.1", 32 | "@types/dotenv": "^8.2.0", 33 | "@types/get-value": "^3.0.3", 34 | "@types/jsonwebtoken": "^9.0.7", 35 | "@types/micro": "^7.3.7", 36 | "@types/mime-types": "^2.1.4", 37 | "@types/module-alias": "^2.0.4", 38 | "@types/node": "^18.13.0", 39 | "@types/pino": "^7.0.5", 40 | "@types/qs": "^6.9.7", 41 | "@types/statsd-client": "^0.4.3", 42 | "@types/ws": "^8.5.4", 43 | "module-alias": "^2.2.3", 44 | "ts-node": "^10.9.1", 45 | "ts-node-dev": "^2.0.0", 46 | "tsconfig-paths": "^4.1.2", 47 | "typescript": "^4.9.5" 48 | }, 49 | "dependencies": { 50 | "@rockstat/rock-me-ts": "file:../rockmets", 51 | "@rockstat/web_sdk": "file:../web-sdk", 52 | "content-type": "^1.0.5", 53 | "cookie": "^0.5.0", 54 | "eventemitter3": "^5.0.0", 55 | "fflate": "^0.8.2", 56 | "find-my-way": "^7.4.0", 57 | "get-value": "^3.0.1", 58 | "jsonwebtoken": "^9.0.2", 59 | "micro": "^10.0.1", 60 | "mime-types": "^3.0.1", 61 | "npm": "^10.7.0", 62 | "qs": "^6.11.0", 63 | "reflect-metadata": "^0.1.13", 64 | "rimraf": "^4.1.2", 65 | "source-map-support": "^0.5.21", 66 | "tsc-watch": "^6.0.0", 67 | "typedi": "^0.10.0", 68 | "ws": "^8.12.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/AppServer.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | // import { Container, Service } from 'typedi'; 3 | import { Dispatcher } from '@app/Dispatcher'; 4 | import { Logger, TheIds, Meter, RedisFactory, AppConfig, version as rockmeVersion, ENV_PROD } from '@rockstat/rock-me-ts'; 5 | import { 6 | // WebSocketServer, 7 | HttpServer 8 | } from '@app/http'; 9 | import { FrontierConfig } from '@app/types'; 10 | import * as constants from '@app/constants'; 11 | import { getAppDeps } from '@rockstat/rock-me-ts'; 12 | 13 | // @Service() 14 | export class AppServer { 15 | 16 | appConfig: AppConfig; 17 | httpServer: HttpServer; 18 | // wsServer: WebSocketServer; 19 | dispatcher: Dispatcher; 20 | log: Logger; 21 | meter: Meter; 22 | 23 | setup() { 24 | this.appConfig = new AppConfig({ vars: constants }) 25 | 26 | getAppDeps().setDep('config', this.appConfig); 27 | // Container.set(AppConfig, this.appConfig); 28 | 29 | const mainLog = new Logger(this.appConfig.log) 30 | getAppDeps().setDep('log', mainLog); 31 | // Container.set(Logger, mainLog); 32 | 33 | this.log = mainLog.for(this); 34 | this.log.info({}, `ENV ${AppConfig.env} ${ENV_PROD} ${String(AppConfig.env) === ENV_PROD}`); 35 | // this.log.info(this.appConfig.config, 'Config'); 36 | 37 | this.log.info({ 38 | version: this.appConfig.config.version, 39 | rockmeVersion 40 | }, 'Starting service'); 41 | 42 | 43 | this.meter = new Meter(this.appConfig.meter); 44 | getAppDeps().setDep('meter', this.meter); 45 | // Container.set(Meter, this.meter); 46 | 47 | const ids = new TheIds(); 48 | getAppDeps().setDep('ids', ids); 49 | // Container.set(TheIds, getAppDeps().getDep('ids')); 50 | 51 | 52 | const redisFactory = new RedisFactory({ log: this.log, meter: this.meter, ...this.appConfig.redis }); 53 | getAppDeps().setDep('redis', redisFactory); 54 | // Container.set(RedisFactory, redisFactory); 55 | 56 | this.dispatcher = new Dispatcher(); 57 | this.httpServer = new HttpServer(this.dispatcher); 58 | // this.wsServer = new WebSocketServer(); 59 | 60 | 61 | // Container.set(Dispatcher, this.dispatcher); 62 | // Container.set(HttpServer, this.httpServer); 63 | // Container.set(WebSocketServer, this.wsServer); 64 | 65 | // const dispatcher = this.dispatcher = Container.get(Dispatcher); 66 | this.dispatcher.setup(); 67 | } 68 | 69 | start() { 70 | this.attachSignals(); 71 | this.dispatcher.start(); 72 | this.log.info('Starting transports'); 73 | this.httpServer.start(); 74 | // this.wsServer.start(); 75 | } 76 | 77 | private onStop() { 78 | this.log.info('Stopping...'); 79 | process.exit(0); 80 | } 81 | 82 | private attachSignals() { 83 | // Handles normal process termination. 84 | process.on('exit', () => this.onStop()); 85 | // Handles `Ctrl+C`. 86 | process.on('SIGINT', () => this.onStop()); 87 | // Handles `kill pid`. 88 | process.on('SIGTERM', () => this.onStop()); 89 | } 90 | 91 | } 92 | 93 | export const appServer = new AppServer(); 94 | appServer.setup() 95 | -------------------------------------------------------------------------------- /src/Dispatcher.ts: -------------------------------------------------------------------------------- 1 | // import { Service, Inject, Container } from 'typedi'; 2 | import { 3 | BusMsgHdr, 4 | FrontierConfig, 5 | IncomingMessage, 6 | BaseIncomingMessage, 7 | MsgBusConfig, 8 | Dictionary, 9 | ServiceStatusStructRegisterItem, 10 | BaseIncomingMessageWithBatch, 11 | ServiceStatusStructData, 12 | } from '@app/types'; 13 | import { 14 | TreeBus, 15 | TreeNameBus 16 | } from '@app/bus'; 17 | import { 18 | IN_GENERIC, 19 | BROADCAST, 20 | ENRICH, 21 | } from '@app/constants'; 22 | import { 23 | AppConfig, 24 | TheIds, 25 | Logger, 26 | // AppStatus, 27 | RedisFactory, 28 | Meter, 29 | RPCAdapterRedis, 30 | RPCAgnostic, 31 | AgnosticRPCOptions, 32 | MethodRegRequest, 33 | EnrichersRequirements, 34 | METHOD_STATUS, 35 | STATUS_INT_ERROR, 36 | STATUS_OK, 37 | BandResponse, 38 | UnknownResponse, 39 | response, 40 | MethodRegistrationOptions, 41 | METHOD_IAMALIVE 42 | } from '@rockstat/rock-me-ts'; 43 | import * as EnrichersRepo from '@app/enrichers'; 44 | import * as HandlersRepo from '@app/handlers'; 45 | import * as TransformersRepo from '@app/transformers'; 46 | import { 47 | dotPropGetter, 48 | getvals 49 | } from '@app/helpers/getprop'; 50 | 51 | import { getAppDeps } from '@rockstat/rock-me-ts'; 52 | 53 | type TransformerRepo = typeof TransformersRepo; 54 | type TransformersNames = keyof TransformerRepo; 55 | type EnricherRepo = typeof EnrichersRepo 56 | type EnrichersNames = keyof EnricherRepo; 57 | type HandlerRepo = typeof HandlersRepo; 58 | type HandlersNames = keyof HandlerRepo; 59 | 60 | const HANDLER = 'handler'; 61 | const ENRICHER = 'enricher'; 62 | 63 | 64 | // @Service() 65 | export class Dispatcher { 66 | 67 | log: Logger; 68 | transformBus: TreeBus = new TreeBus('transformers'); 69 | enrichBus: TreeBus = new TreeBus('enrichers'); 70 | remoteEnrichers: TreeNameBus = new TreeNameBus() 71 | listenBus: TreeBus = new TreeBus('listeners'); 72 | handleBus: TreeBus = new TreeBus('handlers'); 73 | appConfig: AppConfig; 74 | idGen: TheIds; 75 | // status: AppStatus; 76 | registrationsHash: string = ''; 77 | rpc: RPCAgnostic; 78 | rpcHandlers: { [k: string]: [string, string, MethodRegistrationOptions] } = {}; 79 | rpcEnrichers: { [k: string]: Array } = {}; 80 | propGetters: { [k: string]: (obj: any) => { [k: string]: any } } = {}; 81 | enrichersRequirements: EnrichersRequirements = []; 82 | regs: Map>; 83 | regsTimers: Map; 84 | 85 | constructor() { 86 | 87 | // this.log = Container.get(Logger).for(this); 88 | this.log = getAppDeps().getDep('log').for(this); 89 | // this.status = new AppStatus(); 90 | this.log.info('Starting'); 91 | // this.appConfig = Container.get>(AppConfig); 92 | this.appConfig = getAppDeps().getDep('config'); 93 | // this.idGen = Container.get(TheIds); 94 | this.idGen = getAppDeps().getDep('ids'); 95 | this.regs = new Map(); 96 | this.regsTimers = new Map(); 97 | } 98 | 99 | /** 100 | * Initial asynchronous setup 101 | */ 102 | setup() { 103 | this.handleBus.subscribe('*', this.defaultHandler); 104 | this.transformBus.subscribe('*', this.defaultTransformer); 105 | 106 | // Core deps 107 | const redisFactory = getAppDeps().getDep('redis'); 108 | // Stat meter 109 | const meter = getAppDeps().getDep('meter'); 110 | 111 | // Setup RPC 112 | const channels = [this.appConfig.rpc.name]; 113 | const rpcOptions: AgnosticRPCOptions = { channels, redisFactory, log: this.log, meter, ...this.appConfig.rpc } 114 | const rpcAdaptor = new RPCAdapterRedis(rpcOptions); 115 | 116 | this.rpc = new RPCAgnostic(rpcOptions); 117 | this.rpc.setup(rpcAdaptor); 118 | 119 | // status notification for band director 120 | // setInterval(() => { 121 | // this.rpc.notify(SERVICE_DIRECTOR, RPC_IAMALIVE, { name: SERVICE_FRONTIER }) 122 | // }, 5 * 1000) 123 | // Registering status handler / payload receiver 124 | 125 | 126 | const regFunc = async () => { 127 | // if (data.register && data.state_hash) { 128 | // if (data.state_hash == this.registrationsHash) { 129 | // Skip handling. Nothing changed 130 | // return; 131 | // } 132 | const handlerRoutingKeys: string[] = []; 133 | const enrichersRequirements: EnrichersRequirements = []; 134 | for (let [service, reg] of this.regs) { 135 | if (reg && Array.isArray(reg) && reg.length) { 136 | for (let item of reg) { 137 | let { method, role, options } = item; 138 | const route = { service, method }; 139 | 140 | if (options && options.alias) { 141 | route.service = options.alias; 142 | } 143 | const routingPath = [IN_GENERIC, route.service].concat([route.method].filter(e => e !== '*')) 144 | const routingKey = routingPath.join('.'); 145 | if (role === HANDLER) { 146 | this.rpcHandlers[routingKey] = [service, method, options]; 147 | handlerRoutingKeys.push(routingKey); 148 | } 149 | if (role === ENRICHER && options && Array.isArray(options.keys)) { 150 | this.propGetters[route.service] = dotPropGetter(options.props || {}); 151 | // Handling enrichments data selection 152 | let opts = options.props; 153 | if (opts && typeof opts === "object" && !Array.isArray(opts)) { 154 | for (const [k, v] of Object.entries(opts)) { 155 | enrichersRequirements.push([k, v]); 156 | } 157 | } 158 | this.remoteEnrichers.subscribe(options.keys, route.service) 159 | } 160 | 161 | } 162 | } 163 | } 164 | 165 | // for (const row of data.register) { 166 | // const { service, method, options } = row; 167 | // const route = { service, method }; 168 | // if (options && options.alias) { 169 | // route.service = options.alias; 170 | // } 171 | // const routingPath = [IN_GENERIC, route.service].concat([route.method].filter(e => e !== '*')) 172 | // const routingKey = routingPath.join('.'); 173 | // if (row.role === HANDLER) { 174 | // this.rpcHandlers[routingKey] = [service, method, options]; 175 | // handlerRoutingKeys.push(routingKey); 176 | // } 177 | // if (row.role === ENRICHER && options && Array.isArray(options.keys)) { 178 | // this.propGetters[route.service] = dotPropGetter(options.props || {}); 179 | // // Handling enrichments data selection 180 | // let opts = options.props; 181 | // if (opts && typeof opts === "object" && Array.isArray(opts)) { 182 | // for (const [k, v] of Object.entries(opts)) { 183 | // enrichersRequirements.push([k, v]); 184 | // } 185 | // } 186 | // this.remoteEnrichers.subscribe(options.keys, route.service) 187 | // } 188 | // } 189 | // this.registrationsHash = data.state_hash; 190 | // TODO: split by services (when enrichers will be splitted) 191 | this.enrichersRequirements = enrichersRequirements; 192 | // Registering/unregistering remote handlers 193 | this.handleBus.replace(handlerRoutingKeys, this.handlersGateway) 194 | // } 195 | // return this.status.get({}); 196 | } 197 | 198 | // this.rpc.register(METHOD_STATUS, regFunc); 199 | 200 | this.rpc.register<{}>(METHOD_IAMALIVE, async (data: Dictionary) => { 201 | // console.log('data', data) 202 | // return this.rpcHandlers; 203 | if (data.name) { 204 | let service_status: ServiceStatusStructData = await this.rpc.request(data.name, METHOD_STATUS, {}); 205 | // console.log('service_status', service_status) 206 | if (service_status && typeof service_status === "object" && !Array.isArray(service_status)) { 207 | // if ('type__' in service_status && 'data' in service_status) { 208 | // let service_data = service_status['data'] 209 | // console.log('service_data', service_data) 210 | // todo: needed type with optional headers and statusCode 211 | // data.headers = data.headers || []; 212 | // data.statusCode = data.statusCode || STATUS_OK; 213 | // return data; 214 | 215 | this.regs.set(service_status.name, service_status.register) 216 | 217 | let int = this.regsTimers.get(service_status.name) 218 | if (int) { 219 | clearTimeout(int); 220 | } 221 | this.regsTimers.set(service_status.name, setTimeout(() => { 222 | this.regs.delete(service_status.name); 223 | this.regsTimers.delete(service_status.name); 224 | // console.log('removing service', service_status.name) 225 | regFunc().then(() => { }); 226 | }, 15000)); 227 | // } 228 | } 229 | } 230 | }); 231 | 232 | setInterval(() => { 233 | regFunc().then(() => { }) 234 | }, 5000) 235 | 236 | 237 | // if (service_data && typeof service_data === "object" && !Array.isArray(service_data)) { 238 | // let register = service_data['register'] 239 | // if (register && typeof register === "object" && Array.isArray(register)) { 240 | // for (let enr of register){ 241 | // console.log('enr', enr) 242 | // } 243 | // } 244 | // } 245 | 246 | this.rpc.register<{}>('enrichers', async () => { 247 | return this.rpcEnrichers; 248 | }) 249 | 250 | 251 | // Attaching transformers 252 | // const transformersConfig: { 253 | // [k in TransformersNames]?: MsgBusConfig['transformers'][k] 254 | // } = this.appConfig.get('bus').transformers; 255 | 256 | 257 | // Object.entries(transformersConfig) 258 | // .filter(([name, chan]) => chan && (name in TransformersRepo)) 259 | // .forEach(([name, chan]: [TransformersNames, string]) => { 260 | // const transformer = new TransformersRepo[name](); 261 | // this.transformBus.subscribe(chan, transformer.handle); 262 | // // chans.forEach(chan => this.transformBus.subscribe(chan, transformer.handle)); 263 | // }) 264 | 265 | // Attaching internal transformers 266 | const transformersConfig: { 267 | [k in TransformersNames]?: MsgBusConfig['transformers'][k] 268 | } = this.appConfig.get('bus').transformers; 269 | 270 | Object.entries(transformersConfig) 271 | .filter(([name, chans]) => chans && (name in TransformersRepo)) 272 | .forEach(([name, chans]: [TransformersNames, Array]) => { 273 | const transformer = new TransformersRepo[name](); 274 | chans.forEach(chan => this.transformBus.subscribe(chan, transformer.handle)); 275 | }) 276 | 277 | // Attaching internal enrichers 278 | const enrichersConfig: { 279 | [k in EnrichersNames]?: MsgBusConfig['enrichers'][k] 280 | } = this.appConfig.get('bus').enrichers; 281 | 282 | Object.entries(enrichersConfig) 283 | .filter(([name, chans]) => chans && (name in EnrichersRepo)) 284 | .forEach(([name, chans]: [EnrichersNames, Array]) => { 285 | const enricher = new EnrichersRepo[name](); 286 | chans.forEach(chan => this.enrichBus.subscribe(chan, enricher.handle)); 287 | }) 288 | 289 | // Registering remote enrichers notification 290 | this.enrichBus.subscribe('*', this.enrichersGateway); 291 | 292 | 293 | // Attaching internal handlers 294 | const handlersConfig: { 295 | [k in HandlersNames]?: MsgBusConfig['handlers'][k] 296 | } = this.appConfig.get('bus').handlers; 297 | 298 | Object.entries(handlersConfig) 299 | .filter(([name, chan]) => chan && (name in HandlersRepo)) 300 | .forEach(([name, chan]: [HandlersNames, string]) => { 301 | this.handleBus.subscribe(chan, HandlersRepo[name]()); 302 | }) 303 | 304 | // Remote listeners gateway 305 | this.listenBus.subscribe('*', this.listenersGateway); 306 | 307 | } 308 | 309 | start() { 310 | this.log.info('Started'); 311 | } 312 | 313 | 314 | listenersGateway = async (key: string, msg: IncomingMessage) => { 315 | try { 316 | return await this.rpc.notify(BROADCAST, BROADCAST, msg); 317 | } catch (error) { 318 | this.log.error(`catch! ${error.message}`); 319 | } 320 | } 321 | 322 | enrichersGateway = async (key: string, msg: IncomingMessage) => { 323 | try { 324 | const smallMsg = getvals(msg, this.enrichersRequirements); 325 | return await this.rpc.request(ENRICH, ENRICH, smallMsg, { services: this.remoteEnrichers.simulate(key) }); 326 | } catch (error) { 327 | this.log.error(`catch! ${error.message}`); 328 | } 329 | } 330 | 331 | 332 | /** 333 | * Using to handle event remotely 334 | */ 335 | handlersGateway = async (key: string, msg: BaseIncomingMessage): Promise => { 336 | if (msg.service && msg.name && this.rpcHandlers[key]) { 337 | // Real destination 338 | const [service, method, options] = this.rpcHandlers[key]; 339 | try { 340 | const data: UnknownResponse = await this.rpc.request(service, method, msg, { timeout: options.timeout }); 341 | // todo: check via isBandResponse 342 | if (data && typeof data === "object" && !Array.isArray(data)) { 343 | if ('type__' in data) { 344 | // todo: needed type with optional headers and statusCode 345 | data.headers = data.headers || []; 346 | data.statusCode = data.statusCode || STATUS_OK; 347 | return data; 348 | } 349 | } 350 | return response.data({ data }); 351 | } catch (error) { 352 | this.log.warn(error, { error, key, msg }); 353 | return response.error({ statusCode: STATUS_INT_ERROR, errorMessage: error.message }) 354 | } 355 | } 356 | return this.defaultHandler(key, msg); 357 | } 358 | 359 | defaultHandler: BusMsgHdr = async (key, msg): Promise => { 360 | // return Promise.resolve().then(() => 361 | // ) 362 | return response.data({ 363 | data: { 364 | key: key, 365 | id: msg.id, 366 | uid: msg.uid 367 | } 368 | }) 369 | } 370 | defaultTransformer: BusMsgHdr = async (key, msg): Promise => { 371 | // return Promise.resolve().then(() => msg) 372 | return [msg, []]; 373 | } 374 | registerListener(key: string, func: BusMsgHdr): void { 375 | this.log.info(`Registering subscriber for ${key}`); 376 | this.listenBus.subscribe(key, func); 377 | } 378 | 379 | async dispatch(key: string, orig_msg: BaseIncomingMessage): Promise { 380 | 381 | // if (msg.key === 'in.gen.track.registration_success') { 382 | // this.log.info({msg_data: msg}, '__registration_success_v2__'); 383 | // } 384 | 385 | // ### Phase 1: enriching 386 | // const transformer = this.transformBus.publish(key, msg); 387 | orig_msg.id = this.idGen.flake(); 388 | orig_msg.time = Number(new Date()); 389 | 390 | const transformer = this.transformBus.handler(key, orig_msg); 391 | const [msg, msgs]: BaseIncomingMessageWithBatch = await transformer; 392 | 393 | // console.log('msg', msg); 394 | 395 | for (let m of msgs) { 396 | m.id = this.idGen.flake(); 397 | // m.time = msg.time; 398 | // console.log(m) 399 | } 400 | 401 | 402 | 403 | this.log.debug(` ---> ${key} [${msg.id}]`); 404 | 405 | // console.log('transformed', msg); 406 | 407 | const enrichers = this.enrichBus.publish(key, msg); 408 | const enrichments = await Promise.all(enrichers); 409 | 410 | 411 | // console.log('enrichments', enrichments); 412 | 413 | if (enrichments.length && msg.data) { 414 | Object.assign(msg.data, ...enrichments); 415 | for (let submsg of msgs) { 416 | Object.assign(submsg, ...enrichments) 417 | } 418 | } 419 | 420 | // ### Phase 2: handling if configuring 421 | const handler = this.handleBus.handler(key, msg); 422 | this.log.debug(` <--- ${key}`, msg); 423 | const handled = await handler; 424 | 425 | // ### Phase 3: send to listeners 426 | // Scheduling using Promise to avoid waiting 427 | Promise.all(this.listenBus.publish(key, msg)) 428 | .then(() => this.log.debug('Listeners handled')) 429 | .catch(error => this.log.error(error)); 430 | 431 | Promise.all(msgs.map((submsg) => this.listenBus.publish(submsg.key, submsg))) 432 | 433 | return handled; 434 | } 435 | } 436 | -------------------------------------------------------------------------------- /src/StaticData.ts: -------------------------------------------------------------------------------- 1 | // import { Service, Container } from "typedi"; 2 | import { readFileSync } from 'fs'; 3 | import { StaticConfig, Envs, FrontierConfig } from "@app/types"; 4 | import { ENV_DEV } from "@app/constants"; 5 | import { LoggerType, Logger, AppConfig, getAppDeps } from '@rockstat/rock-me-ts'; 6 | import { readSync } from '@app/helpers' 7 | 8 | type LibParams = { [key: string]: any }; 9 | 10 | const index = new Uint8Array(Buffer.from([60, 33, 68, 79, 67, 84, 89, 80, 69, 32, 104, 116, 109, 108, 62, 10, 60, 104, 116, 109, 108, 32, 108, 97, 110, 103, 61, 34, 101, 110, 34, 62, 10, 60, 104, 101, 97, 100, 62, 10, 60, 109, 101, 116, 97, 32, 99, 104, 97, 114, 115, 101, 116, 61, 34, 85, 84, 70, 45, 56, 34, 62, 10, 60, 116, 105, 116, 108, 101, 62, 82, 111, 99, 107, 115, 116, 97, 116, 32, 112, 108, 97, 116, 102, 111, 114, 109, 60, 47, 116, 105, 116, 108, 101, 62, 10, 60, 115, 116, 121, 108, 101, 32, 116, 121, 112, 101, 61, 34, 116, 101, 120, 116, 47, 99, 115, 115, 34, 62, 10, 104, 116, 109, 108, 44, 10, 98, 111, 100, 121, 32, 123, 10, 119, 105, 100, 116, 104, 58, 32, 49, 48, 48, 37, 59, 10, 104, 101, 105, 103, 104, 116, 58, 32, 49, 48, 48, 37, 59, 10, 109, 97, 114, 103, 105, 110, 58, 32, 48, 59, 10, 112, 97, 100, 100, 105, 110, 103, 58, 32, 48, 59, 10, 98, 97, 99, 107, 103, 114, 111, 117, 110, 100, 45, 99, 111, 108, 111, 114, 58, 32, 35, 48, 48, 48, 59, 10, 125, 10, 35, 99, 111, 110, 116, 97, 105, 110, 101, 114, 32, 123, 10, 119, 105, 100, 116, 104, 58, 32, 49, 48, 48, 37, 59, 10, 104, 101, 105, 103, 104, 116, 58, 32, 49, 48, 48, 37, 59, 10, 109, 97, 114, 103, 105, 110, 58, 32, 48, 59, 10, 112, 97, 100, 100, 105, 110, 103, 58, 32, 48, 59, 10, 97, 108, 105, 103, 110, 45, 105, 116, 101, 109, 115, 58, 32, 99, 101, 110, 116, 101, 114, 59, 10, 106, 117, 115, 116, 105, 102, 121, 45, 99, 111, 110, 116, 101, 110, 116, 58, 32, 99, 101, 110, 116, 101, 114, 59, 10, 100, 105, 115, 112, 108, 97, 121, 58, 32, 102, 108, 101, 120, 59, 10, 125, 10, 60, 47, 115, 116, 121, 108, 101, 62, 10, 60, 47, 104, 101, 97, 100, 62, 10, 60, 98, 111, 100, 121, 62, 10, 60, 100, 105, 118, 32, 105, 100, 61, 39, 99, 111, 110, 116, 97, 105, 110, 101, 114, 39, 62, 10, 60, 100, 105, 118, 32, 105, 100, 61, 39, 105, 116, 101, 109, 39, 62, 10, 60, 115, 118, 103, 32, 119, 105, 100, 116, 104, 61, 34, 52, 56, 57, 34, 32, 104, 101, 105, 103, 104, 116, 61, 34, 49, 52, 51, 34, 32, 102, 105, 108, 108, 61, 34, 110, 111, 110, 101, 34, 32, 120, 109, 108, 110, 115, 61, 34, 104, 116, 116, 112, 58, 47, 47, 119, 119, 119, 46, 119, 51, 46, 111, 114, 103, 47, 50, 48, 48, 48, 47, 115, 118, 103, 34, 62, 60, 112, 97, 116, 104, 32, 102, 105, 108, 108, 45, 114, 117, 108, 101, 61, 34, 101, 118, 101, 110, 111, 100, 100, 34, 32, 99, 108, 105, 112, 45, 114, 117, 108, 101, 61, 34, 101, 118, 101, 110, 111, 100, 100, 34, 32, 100, 61, 34, 77, 49, 50, 50, 46, 55, 53, 32, 49, 48, 57, 46, 54, 51, 99, 45, 49, 46, 57, 32, 48, 45, 51, 46, 55, 52, 46, 51, 45, 53, 46, 52, 56, 46, 57, 45, 49, 46, 55, 53, 46, 54, 49, 45, 51, 46, 51, 32, 49, 46, 52, 54, 45, 52, 46, 54, 51, 32, 50, 46, 53, 52, 97, 49, 50, 46, 49, 32, 49, 50, 46, 49, 32, 48, 32, 48, 32, 48, 45, 51, 46, 50, 32, 51, 46, 57, 54, 99, 45, 46, 56, 32, 49, 46, 53, 53, 45, 49, 46, 50, 32, 51, 46, 50, 57, 45, 49, 46, 50, 32, 53, 46, 50, 118, 49, 57, 46, 56, 51, 72, 49, 48, 51, 118, 45, 51, 56, 46, 49, 53, 104, 52, 46, 52, 56, 108, 46, 52, 56, 32, 57, 46, 56, 50, 97, 49, 54, 46, 56, 56, 32, 49, 54, 46, 56, 56, 32, 48, 32, 48, 32, 49, 32, 50, 46, 57, 54, 45, 52, 46, 52, 51, 99, 49, 46, 50, 45, 49, 46, 51, 32, 50, 46, 53, 51, 45, 50, 46, 52, 50, 32, 51, 46, 57, 54, 45, 51, 46, 51, 52, 97, 49, 57, 46, 51, 50, 32, 49, 57, 46, 51, 50, 32, 48, 32, 48, 32, 49, 32, 52, 46, 52, 56, 45, 50, 46, 49, 52, 32, 49, 52, 46, 51, 32, 49, 52, 46, 51, 32, 48, 32, 48, 32, 49, 32, 52, 46, 52, 52, 45, 46, 55, 55, 108, 45, 49, 46, 48, 53, 32, 54, 46, 53, 56, 90, 109, 52, 46, 49, 32, 49, 51, 46, 51, 54, 97, 50, 48, 46, 49, 54, 32, 50, 48, 46, 49, 54, 32, 48, 32, 48, 32, 49, 32, 53, 46, 56, 55, 45, 49, 52, 46, 49, 54, 32, 50, 48, 46, 49, 57, 32, 50, 48, 46, 49, 57, 32, 48, 32, 48, 32, 49, 32, 50, 56, 46, 52, 56, 32, 48, 32, 49, 57, 46, 57, 51, 32, 49, 57, 46, 57, 51, 32, 48, 32, 48, 32, 49, 32, 52, 46, 50, 32, 54, 46, 51, 56, 32, 50, 48, 46, 48, 52, 32, 50, 48, 46, 48, 52, 32, 48, 32, 48, 32, 49, 32, 49, 46, 53, 51, 32, 55, 46, 55, 56, 32, 49, 57, 46, 55, 32, 49, 57, 46, 55, 32, 48, 32, 48, 32, 49, 45, 53, 46, 55, 55, 32, 49, 52, 46, 48, 50, 32, 50, 48, 46, 55, 54, 32, 50, 48, 46, 55, 54, 32, 48, 32, 48, 32, 49, 45, 54, 46, 51, 53, 32, 52, 46, 50, 52, 32, 49, 57, 46, 54, 56, 32, 49, 57, 46, 54, 56, 32, 48, 32, 48, 32, 49, 45, 55, 46, 57, 50, 32, 49, 46, 53, 56, 99, 45, 50, 46, 56, 32, 48, 45, 53, 46, 52, 45, 46, 53, 45, 55, 46, 56, 50, 45, 49, 46, 52, 56, 97, 49, 57, 46, 52, 50, 32, 49, 57, 46, 52, 50, 32, 48, 32, 48, 32, 49, 45, 49, 48, 46, 54, 52, 45, 49, 48, 46, 52, 53, 32, 49, 57, 46, 54, 53, 32, 49, 57, 46, 54, 53, 32, 48, 32, 48, 32, 49, 45, 49, 46, 53, 56, 45, 55, 46, 57, 49, 90, 109, 53, 46, 50, 53, 32, 48, 97, 49, 52, 46, 55, 53, 32, 49, 52, 46, 55, 53, 32, 48, 32, 48, 32, 48, 32, 56, 46, 57, 55, 32, 49, 51, 46, 53, 53, 99, 49, 46, 55, 56, 46, 55, 53, 32, 51, 46, 55, 32, 49, 46, 49, 51, 32, 53, 46, 55, 51, 32, 49, 46, 49, 51, 97, 49, 52, 46, 55, 51, 32, 49, 52, 46, 55, 51, 32, 48, 32, 48, 32, 48, 32, 49, 51, 46, 54, 52, 45, 50, 48, 46, 52, 53, 32, 49, 52, 46, 54, 52, 32, 49, 52, 46, 54, 52, 32, 48, 32, 48, 32, 48, 45, 51, 46, 49, 53, 45, 52, 46, 55, 51, 32, 49, 52, 46, 55, 51, 32, 49, 52, 46, 55, 51, 32, 48, 32, 48, 32, 48, 45, 49, 48, 46, 53, 45, 52, 46, 51, 32, 49, 52, 46, 55, 56, 32, 49, 52, 46, 55, 56, 32, 48, 32, 48, 32, 48, 45, 49, 48, 46, 52, 32, 52, 46, 51, 32, 49, 52, 46, 55, 50, 32, 49, 52, 46, 55, 50, 32, 48, 32, 48, 32, 48, 45, 52, 46, 51, 32, 49, 48, 46, 53, 90, 109, 55, 49, 46, 55, 53, 32, 49, 49, 46, 53, 52, 32, 51, 46, 50, 53, 32, 51, 46, 57, 97, 49, 57, 46, 52, 52, 32, 49, 57, 46, 52, 52, 32, 48, 32, 48, 32, 49, 45, 53, 46, 55, 55, 32, 51, 46, 50, 53, 99, 45, 50, 46, 49, 52, 46, 55, 54, 45, 52, 46, 52, 32, 49, 46, 49, 53, 45, 54, 46, 56, 51, 32, 49, 46, 49, 53, 45, 50, 46, 56, 32, 48, 45, 53, 46, 52, 50, 45, 46, 53, 51, 45, 55, 46, 56, 55, 45, 49, 46, 53, 56, 97, 50, 48, 46, 54, 32, 50, 48, 46, 54, 32, 48, 32, 48, 32, 49, 45, 54, 46, 52, 45, 52, 46, 50, 52, 32, 49, 57, 46, 55, 53, 32, 49, 57, 46, 55, 53, 32, 48, 32, 48, 32, 49, 32, 48, 45, 50, 56, 46, 49, 52, 32, 50, 48, 46, 54, 32, 50, 48, 46, 54, 32, 48, 32, 48, 32, 49, 32, 54, 46, 52, 45, 52, 46, 50, 53, 32, 49, 57, 46, 55, 56, 32, 49, 57, 46, 55, 56, 32, 48, 32, 48, 32, 49, 32, 55, 46, 56, 55, 45, 49, 46, 53, 55, 99, 50, 46, 52, 50, 32, 48, 32, 52, 46, 55, 49, 46, 51, 56, 32, 54, 46, 56, 55, 32, 49, 46, 49, 53, 97, 49, 57, 46, 51, 32, 49, 57, 46, 51, 32, 48, 32, 48, 32, 49, 32, 53, 46, 56, 50, 32, 51, 46, 50, 52, 99, 45, 46, 53, 46, 55, 45, 49, 46, 48, 52, 32, 49, 46, 51, 53, 45, 49, 46, 54, 50, 32, 49, 46, 57, 53, 45, 46, 53, 55, 46, 54, 45, 49, 46, 49, 52, 32, 49, 46, 50, 54, 45, 49, 46, 55, 50, 32, 49, 46, 57, 54, 97, 49, 52, 46, 52, 53, 32, 49, 52, 46, 52, 53, 32, 48, 32, 48, 32, 48, 45, 57, 46, 51, 53, 45, 51, 46, 49, 53, 32, 49, 52, 46, 57, 55, 32, 49, 52, 46, 57, 55, 32, 48, 32, 48, 32, 48, 45, 49, 48, 46, 53, 57, 32, 52, 46, 50, 53, 32, 49, 52, 46, 48, 57, 32, 49, 52, 46, 48, 57, 32, 48, 32, 48, 32, 48, 45, 51, 46, 49, 53, 32, 52, 46, 54, 55, 32, 49, 52, 46, 57, 55, 32, 49, 52, 46, 57, 55, 32, 48, 32, 48, 32, 48, 45, 49, 46, 49, 52, 32, 53, 46, 56, 55, 99, 48, 32, 50, 46, 48, 51, 46, 51, 56, 32, 51, 46, 57, 52, 32, 49, 46, 49, 52, 32, 53, 46, 55, 50, 97, 49, 52, 46, 54, 52, 32, 49, 52, 46, 54, 52, 32, 48, 32, 48, 32, 48, 32, 55, 46, 56, 55, 32, 55, 46, 56, 50, 99, 49, 46, 56, 50, 46, 55, 54, 32, 51, 46, 55, 56, 32, 49, 46, 49, 53, 32, 53, 46, 56, 55, 32, 49, 46, 49, 53, 32, 51, 46, 54, 51, 32, 48, 32, 54, 46, 55, 53, 45, 49, 46, 48, 53, 32, 57, 46, 51, 53, 45, 51, 46, 49, 53, 90, 109, 51, 56, 46, 54, 53, 45, 51, 49, 46, 53, 55, 32, 51, 46, 49, 53, 32, 51, 46, 50, 52, 99, 45, 50, 46, 52, 50, 32, 50, 46, 50, 50, 45, 52, 46, 55, 51, 32, 52, 46, 52, 45, 54, 46, 57, 50, 32, 54, 46, 53, 51, 45, 50, 46, 50, 32, 50, 46, 49, 51, 45, 52, 46, 53, 32, 52, 46, 51, 49, 45, 54, 46, 57, 50, 32, 54, 46, 53, 52, 108, 49, 54, 46, 54, 32, 50, 50, 46, 56, 104, 45, 54, 46, 49, 108, 45, 49, 52, 46, 50, 50, 45, 49, 57, 46, 50, 55, 45, 54, 46, 50, 32, 53, 46, 55, 50, 118, 49, 51, 46, 53, 52, 104, 45, 53, 46, 50, 53, 86, 54, 57, 104, 53, 46, 50, 53, 118, 53, 51, 46, 50, 50, 108, 49, 48, 46, 51, 45, 57, 46, 53, 56, 99, 51, 46, 51, 49, 45, 51, 46, 48, 57, 32, 54, 46, 55, 53, 45, 54, 46, 51, 49, 32, 49, 48, 46, 51, 45, 57, 46, 54, 56, 90, 109, 51, 51, 46, 54, 56, 32, 52, 46, 53, 55, 45, 50, 46, 56, 54, 32, 51, 46, 55, 50, 99, 45, 46, 53, 49, 45, 46, 55, 45, 49, 46, 53, 49, 45, 49, 46, 51, 53, 45, 51, 45, 49, 46, 57, 53, 97, 49, 50, 32, 49, 50, 32, 48, 32, 48, 32, 48, 45, 52, 46, 53, 52, 45, 46, 57, 99, 45, 46, 54, 52, 32, 48, 45, 49, 46, 51, 50, 46, 48, 57, 45, 50, 46, 48, 53, 46, 50, 56, 45, 46, 55, 51, 46, 49, 57, 45, 49, 46, 52, 46, 53, 45, 50, 32, 46, 57, 45, 46, 54, 49, 46, 52, 50, 45, 49, 46, 49, 50, 46, 57, 54, 45, 49, 46, 53, 51, 32, 49, 46, 54, 51, 97, 52, 46, 53, 51, 32, 52, 46, 53, 51, 32, 48, 32, 48, 32, 48, 45, 46, 54, 50, 32, 50, 46, 52, 51, 32, 52, 46, 54, 32, 52, 46, 54, 32, 48, 32, 48, 32, 48, 32, 49, 46, 51, 51, 32, 51, 46, 52, 56, 99, 46, 57, 46, 56, 54, 32, 50, 32, 49, 46, 54, 32, 51, 46, 51, 52, 32, 50, 46, 50, 52, 97, 53, 48, 46, 57, 32, 53, 48, 46, 57, 32, 48, 32, 48, 32, 48, 32, 52, 46, 51, 53, 32, 49, 46, 56, 49, 99, 49, 46, 53, 53, 46, 53, 56, 32, 51, 32, 49, 46, 50, 56, 32, 52, 46, 51, 52, 32, 50, 46, 49, 32, 49, 46, 51, 51, 46, 56, 51, 32, 50, 46, 52, 52, 32, 49, 46, 57, 32, 51, 46, 51, 52, 32, 51, 46, 50, 97, 56, 46, 55, 32, 56, 46, 55, 32, 48, 32, 48, 32, 49, 32, 49, 46, 51, 51, 32, 53, 99, 48, 32, 51, 46, 49, 57, 45, 49, 46, 48, 56, 32, 53, 46, 56, 55, 45, 51, 46, 50, 52, 32, 56, 46, 48, 54, 45, 50, 46, 49, 55, 32, 50, 46, 50, 45, 53, 46, 50, 53, 32, 51, 46, 51, 45, 57, 46, 50, 54, 32, 51, 46, 51, 45, 50, 46, 49, 54, 32, 48, 45, 52, 46, 51, 55, 45, 46, 51, 57, 45, 54, 46, 54, 51, 45, 49, 46, 49, 53, 97, 49, 50, 46, 48, 55, 32, 49, 50, 46, 48, 55, 32, 48, 32, 48, 32, 49, 45, 53, 46, 52, 57, 45, 51, 46, 54, 50, 99, 46, 50, 54, 45, 46, 50, 54, 46, 53, 56, 45, 46, 54, 46, 57, 54, 45, 49, 46, 48, 53, 97, 49, 50, 56, 46, 52, 56, 32, 49, 50, 56, 46, 52, 56, 32, 48, 32, 48, 32, 48, 32, 49, 46, 56, 54, 45, 50, 46, 50, 52, 99, 46, 50, 50, 45, 46, 50, 57, 46, 51, 55, 45, 46, 52, 51, 46, 52, 51, 45, 46, 52, 51, 46, 49, 57, 46, 51, 49, 46, 54, 46, 54, 54, 32, 49, 46, 50, 52, 32, 49, 46, 48, 53, 97, 50, 48, 46, 49, 54, 32, 50, 48, 46, 49, 54, 32, 48, 32, 48, 32, 48, 32, 52, 46, 56, 55, 32, 50, 32, 57, 46, 55, 52, 32, 57, 46, 55, 52, 32, 48, 32, 48, 32, 48, 32, 53, 46, 50, 46, 48, 53, 32, 55, 46, 52, 32, 55, 46, 52, 32, 48, 32, 48, 32, 48, 32, 50, 46, 51, 56, 45, 49, 46, 49, 32, 53, 46, 52, 32, 53, 46, 52, 32, 48, 32, 48, 32, 48, 32, 50, 46, 52, 51, 45, 52, 46, 54, 55, 99, 48, 45, 49, 46, 52, 55, 45, 46, 52, 52, 45, 50, 46, 54, 51, 45, 49, 46, 51, 51, 45, 51, 46, 52, 56, 45, 46, 57, 45, 46, 56, 54, 45, 50, 45, 49, 46, 54, 49, 45, 51, 46, 51, 52, 45, 50, 46, 50, 53, 97, 52, 48, 46, 53, 55, 32, 52, 48, 46, 53, 55, 32, 48, 32, 48, 32, 48, 45, 52, 46, 52, 45, 49, 46, 55, 54, 32, 50, 49, 46, 52, 32, 50, 49, 46, 52, 32, 48, 32, 48, 32, 49, 45, 52, 46, 51, 56, 45, 50, 46, 48, 53, 32, 49, 48, 46, 56, 52, 32, 49, 48, 46, 56, 52, 32, 48, 32, 48, 32, 49, 45, 51, 46, 51, 52, 45, 51, 46, 50, 52, 32, 57, 46, 49, 49, 32, 57, 46, 49, 49, 32, 48, 32, 48, 32, 49, 45, 49, 46, 51, 51, 45, 53, 46, 49, 54, 99, 48, 45, 49, 46, 57, 46, 51, 52, 45, 51, 46, 53, 32, 49, 46, 48, 52, 45, 52, 46, 56, 49, 97, 49, 48, 46, 50, 56, 32, 49, 48, 46, 50, 56, 32, 48, 32, 48, 32, 49, 32, 54, 46, 51, 53, 45, 53, 46, 49, 32, 49, 52, 46, 55, 32, 49, 52, 46, 55, 32, 48, 32, 48, 32, 49, 32, 51, 46, 57, 54, 45, 46, 53, 56, 99, 49, 46, 54, 53, 32, 48, 32, 51, 46, 52, 55, 46, 51, 53, 32, 53, 46, 52, 52, 32, 49, 46, 48, 53, 32, 49, 46, 57, 55, 46, 55, 32, 51, 46, 54, 57, 32, 49, 46, 55, 56, 32, 53, 46, 49, 53, 32, 51, 46, 50, 52, 90, 109, 49, 52, 46, 53, 45, 50, 48, 46, 51, 49, 104, 53, 46, 50, 53, 118, 49, 55, 46, 48, 55, 104, 49, 49, 46, 51, 54, 118, 52, 46, 55, 55, 104, 45, 49, 49, 46, 51, 54, 118, 51, 51, 104, 45, 53, 46, 50, 53, 118, 45, 51, 51, 104, 45, 55, 46, 54, 51, 118, 45, 52, 46, 55, 55, 104, 55, 46, 54, 51, 86, 56, 55, 46, 50, 50, 90, 109, 50, 48, 46, 52, 50, 32, 51, 53, 46, 57, 54, 99, 48, 45, 50, 46, 55, 52, 46, 53, 51, 45, 53, 46, 51, 51, 32, 49, 46, 53, 56, 45, 55, 46, 55, 56, 97, 50, 48, 46, 55, 52, 32, 50, 48, 46, 55, 52, 32, 48, 32, 48, 32, 49, 32, 52, 46, 50, 57, 45, 54, 46, 52, 52, 32, 49, 57, 46, 57, 51, 32, 49, 57, 46, 57, 51, 32, 48, 32, 48, 32, 49, 32, 54, 46, 51, 53, 45, 52, 46, 51, 51, 32, 49, 57, 46, 50, 32, 49, 57, 46, 50, 32, 48, 32, 48, 32, 49, 32, 55, 46, 55, 50, 45, 49, 46, 53, 56, 99, 49, 46, 57, 49, 32, 48, 32, 51, 46, 55, 46, 50, 54, 32, 53, 46, 51, 53, 46, 55, 55, 32, 49, 46, 54, 53, 46, 53, 32, 51, 46, 49, 51, 32, 49, 46, 49, 57, 32, 52, 46, 52, 51, 32, 50, 46, 48, 53, 32, 49, 46, 51, 46, 56, 53, 32, 50, 46, 52, 32, 49, 46, 56, 32, 51, 46, 51, 32, 50, 46, 56, 54, 46, 56, 57, 32, 49, 46, 48, 53, 32, 49, 46, 53, 54, 32, 50, 46, 48, 56, 32, 50, 32, 51, 46, 49, 108, 46, 51, 56, 45, 55, 46, 57, 50, 104, 52, 46, 56, 55, 118, 51, 56, 46, 49, 53, 104, 45, 53, 46, 50, 53, 118, 45, 51, 46, 57, 99, 48, 45, 46, 55, 46, 48, 50, 45, 49, 46, 51, 56, 46, 48, 53, 45, 50, 46, 48, 49, 46, 48, 51, 45, 46, 54, 52, 46, 48, 52, 45, 49, 46, 50, 52, 46, 48, 52, 45, 49, 46, 56, 49, 97, 49, 48, 46, 55, 52, 32, 49, 48, 46, 55, 52, 32, 48, 32, 48, 32, 49, 45, 50, 32, 51, 46, 49, 99, 45, 46, 56, 57, 46, 57, 56, 45, 49, 46, 57, 57, 32, 49, 46, 56, 57, 45, 51, 46, 50, 57, 32, 50, 46, 55, 50, 45, 49, 46, 51, 46, 56, 50, 45, 50, 46, 55, 56, 32, 49, 46, 52, 55, 45, 52, 46, 52, 52, 32, 49, 46, 57, 53, 45, 49, 46, 54, 53, 46, 52, 56, 45, 51, 46, 52, 54, 46, 55, 50, 45, 53, 46, 52, 52, 46, 55, 50, 97, 50, 48, 46, 48, 50, 32, 50, 48, 46, 48, 50, 32, 48, 32, 48, 32, 49, 45, 49, 52, 46, 50, 54, 45, 53, 46, 56, 50, 32, 50, 48, 46, 48, 53, 32, 50, 48, 46, 48, 53, 32, 48, 32, 48, 32, 49, 45, 52, 46, 49, 53, 45, 54, 46, 50, 53, 32, 49, 57, 46, 49, 49, 32, 49, 57, 46, 49, 49, 32, 48, 32, 48, 32, 49, 45, 49, 46, 53, 51, 45, 55, 46, 53, 56, 90, 109, 53, 46, 50, 53, 45, 46, 50, 57, 97, 49, 52, 46, 54, 49, 32, 49, 52, 46, 54, 49, 32, 48, 32, 48, 32, 48, 32, 57, 46, 48, 50, 32, 49, 51, 46, 54, 52, 99, 49, 46, 56, 49, 46, 55, 54, 32, 51, 46, 55, 55, 32, 49, 46, 49, 53, 32, 53, 46, 56, 55, 32, 49, 46, 49, 53, 97, 49, 52, 46, 57, 55, 32, 49, 52, 46, 57, 55, 32, 48, 32, 48, 32, 48, 32, 49, 48, 46, 53, 57, 45, 52, 46, 51, 32, 49, 52, 46, 55, 53, 32, 49, 52, 46, 55, 53, 32, 48, 32, 48, 32, 48, 45, 49, 48, 46, 54, 45, 50, 53, 46, 49, 56, 32, 49, 52, 46, 57, 55, 32, 49, 52, 46, 57, 55, 32, 48, 32, 48, 32, 48, 45, 49, 48, 46, 53, 57, 32, 52, 46, 50, 53, 32, 49, 52, 46, 50, 32, 49, 52, 46, 50, 32, 48, 32, 48, 32, 48, 45, 51, 46, 49, 52, 32, 52, 46, 54, 50, 32, 49, 52, 46, 54, 49, 32, 49, 52, 46, 54, 49, 32, 48, 32, 48, 32, 48, 45, 49, 46, 49, 53, 32, 53, 46, 56, 50, 90, 109, 53, 49, 46, 48, 53, 45, 51, 53, 46, 54, 55, 104, 53, 46, 50, 53, 118, 49, 55, 46, 48, 55, 72, 51, 56, 52, 118, 52, 46, 55, 55, 104, 45, 49, 49, 46, 51, 53, 118, 51, 51, 104, 45, 53, 46, 50, 53, 118, 45, 51, 51, 104, 45, 55, 46, 54, 52, 118, 45, 52, 46, 55, 55, 104, 55, 46, 54, 52, 86, 56, 55, 46, 50, 50, 90, 34, 32, 102, 105, 108, 108, 61, 34, 35, 49, 49, 49, 34, 47, 62, 60, 112, 97, 116, 104, 32, 100, 61, 34, 77, 46, 51, 52, 32, 50, 46, 48, 55, 86, 49, 76, 50, 46, 50, 49, 46, 50, 72, 54, 46, 55, 108, 45, 46, 56, 50, 32, 49, 46, 56, 56, 72, 50, 46, 50, 49, 118, 50, 50, 46, 49, 54, 72, 54, 46, 55, 108, 45, 46, 56, 50, 32, 49, 46, 56, 55, 72, 46, 51, 52, 86, 50, 46, 48, 55, 90, 109, 49, 52, 46, 50, 32, 53, 46, 54, 45, 50, 46, 55, 54, 32, 50, 46, 54, 51, 45, 49, 46, 49, 45, 49, 46, 52, 56, 32, 51, 46, 56, 54, 45, 51, 46, 55, 32, 49, 46, 56, 56, 45, 46, 56, 51, 118, 49, 54, 46, 56, 57, 108, 45, 49, 46, 56, 56, 46, 56, 50, 86, 55, 46, 54, 55, 90, 109, 49, 51, 46, 52, 56, 32, 48, 45, 50, 46, 55, 55, 32, 50, 46, 54, 51, 45, 49, 46, 49, 45, 49, 46, 52, 56, 32, 51, 46, 56, 55, 45, 51, 46, 55, 32, 49, 46, 56, 55, 45, 46, 56, 51, 118, 49, 54, 46, 56, 57, 108, 45, 49, 46, 56, 55, 46, 56, 50, 86, 55, 46, 54, 55, 90, 109, 49, 54, 46, 52, 56, 32, 49, 48, 104, 45, 55, 46, 57, 49, 108, 46, 56, 50, 45, 49, 46, 56, 55, 76, 52, 52, 46, 53, 32, 53, 46, 49, 49, 108, 49, 46, 56, 55, 45, 46, 56, 50, 86, 49, 53, 46, 56, 104, 49, 46, 55, 53, 108, 45, 46, 56, 50, 32, 49, 46, 56, 56, 104, 45, 46, 57, 51, 118, 51, 46, 53, 76, 52, 52, 46, 53, 32, 50, 50, 118, 45, 52, 46, 51, 50, 90, 109, 48, 45, 49, 46, 56, 55, 86, 56, 46, 50, 56, 108, 45, 53, 32, 55, 46, 53, 50, 104, 53, 90, 109, 49, 48, 46, 55, 50, 32, 54, 46, 49, 53, 99, 45, 46, 50, 54, 45, 46, 48, 54, 45, 46, 52, 57, 45, 46, 50, 45, 46, 55, 45, 46, 52, 97, 49, 46, 53, 32, 49, 46, 53, 32, 48, 32, 48, 32, 49, 45, 46, 52, 52, 45, 49, 46, 49, 99, 48, 45, 46, 52, 51, 46, 49, 53, 45, 46, 55, 57, 46, 52, 53, 45, 49, 46, 48, 57, 46, 51, 45, 46, 51, 46, 54, 53, 45, 46, 52, 53, 32, 49, 46, 48, 56, 45, 46, 52, 53, 97, 49, 46, 55, 55, 32, 49, 46, 55, 55, 32, 48, 32, 48, 32, 49, 32, 49, 46, 54, 57, 32, 49, 46, 54, 50, 32, 52, 46, 48, 56, 32, 52, 46, 48, 56, 32, 48, 32, 48, 32, 49, 45, 46, 52, 51, 32, 50, 46, 51, 54, 99, 45, 46, 53, 55, 32, 49, 46, 49, 50, 45, 49, 46, 50, 56, 32, 49, 46, 55, 55, 45, 50, 46, 49, 51, 32, 49, 46, 57, 53, 97, 51, 46, 50, 57, 32, 51, 46, 50, 57, 32, 48, 32, 48, 32, 49, 45, 49, 46, 48, 55, 46, 48, 52, 108, 46, 52, 54, 45, 49, 46, 50, 55, 99, 46, 51, 51, 45, 46, 48, 52, 46, 54, 45, 46, 50, 52, 46, 56, 51, 45, 46, 53, 57, 46, 49, 55, 45, 46, 50, 55, 46, 50, 54, 45, 46, 53, 52, 46, 50, 54, 45, 46, 56, 50, 118, 45, 46, 50, 53, 90, 77, 54, 56, 46, 52, 53, 32, 55, 46, 54, 55, 108, 45, 50, 46, 55, 55, 32, 50, 46, 54, 51, 45, 49, 46, 49, 45, 49, 46, 52, 56, 32, 51, 46, 56, 55, 45, 51, 46, 55, 32, 49, 46, 56, 55, 45, 46, 56, 51, 118, 49, 54, 46, 56, 57, 108, 45, 49, 46, 56, 55, 46, 56, 50, 86, 55, 46, 54, 55, 90, 109, 49, 51, 46, 52, 56, 32, 48, 45, 50, 46, 55, 55, 32, 50, 46, 54, 51, 45, 49, 46, 49, 45, 49, 46, 52, 56, 32, 51, 46, 56, 55, 45, 51, 46, 55, 32, 49, 46, 56, 55, 45, 46, 56, 51, 118, 49, 54, 46, 56, 57, 108, 45, 49, 46, 56, 55, 46, 56, 50, 86, 55, 46, 54, 55, 90, 109, 49, 51, 46, 52, 55, 32, 48, 45, 50, 46, 55, 54, 32, 50, 46, 54, 51, 45, 49, 46, 49, 45, 49, 46, 52, 56, 32, 51, 46, 56, 54, 45, 51, 46, 55, 32, 49, 46, 56, 56, 45, 46, 56, 51, 118, 49, 54, 46, 56, 57, 76, 57, 53, 46, 52, 32, 50, 50, 86, 55, 46, 54, 55, 90, 109, 49, 51, 46, 55, 50, 32, 49, 52, 46, 50, 56, 99, 45, 46, 50, 53, 45, 46, 48, 54, 45, 46, 52, 56, 45, 46, 50, 45, 46, 54, 57, 45, 46, 52, 97, 49, 46, 53, 32, 49, 46, 53, 32, 48, 32, 48, 32, 49, 45, 46, 52, 52, 45, 49, 46, 49, 99, 48, 45, 46, 52, 51, 46, 49, 53, 45, 46, 55, 57, 46, 52, 52, 45, 49, 46, 48, 57, 46, 51, 45, 46, 51, 46, 54, 54, 45, 46, 52, 53, 32, 49, 46, 48, 56, 45, 46, 52, 53, 97, 49, 46, 55, 55, 32, 49, 46, 55, 55, 32, 48, 32, 48, 32, 49, 32, 49, 46, 54, 57, 32, 49, 46, 54, 50, 32, 51, 46, 57, 56, 32, 51, 46, 57, 56, 32, 48, 32, 48, 32, 49, 45, 46, 52, 50, 32, 50, 46, 51, 54, 99, 45, 46, 53, 55, 32, 49, 46, 49, 50, 45, 49, 46, 50, 56, 32, 49, 46, 55, 55, 45, 50, 46, 49, 52, 32, 49, 46, 57, 53, 97, 51, 46, 51, 32, 51, 46, 51, 32, 48, 32, 48, 32, 49, 45, 49, 46, 48, 54, 46, 48, 52, 108, 46, 52, 53, 45, 49, 46, 50, 55, 99, 46, 51, 51, 45, 46, 48, 52, 46, 54, 49, 45, 46, 50, 52, 46, 56, 52, 45, 46, 53, 57, 46, 49, 55, 45, 46, 50, 55, 46, 50, 53, 45, 46, 53, 52, 46, 50, 53, 45, 46, 56, 50, 118, 45, 46, 50, 53, 90, 109, 49, 52, 46, 49, 45, 49, 46, 55, 52, 97, 51, 46, 51, 32, 51, 46, 51, 32, 48, 32, 48, 32, 48, 32, 50, 46, 52, 45, 46, 57, 57, 99, 46, 54, 55, 45, 46, 54, 54, 32, 49, 45, 49, 46, 52, 54, 32, 49, 45, 50, 46, 52, 118, 45, 51, 46, 51, 54, 97, 53, 46, 49, 55, 32, 53, 46, 49, 55, 32, 48, 32, 48, 32, 49, 45, 51, 46, 52, 32, 49, 46, 50, 52, 32, 53, 46, 48, 54, 32, 53, 46, 48, 54, 32, 48, 32, 48, 32, 49, 45, 51, 46, 55, 53, 45, 49, 46, 53, 52, 32, 53, 46, 49, 53, 32, 53, 46, 49, 53, 32, 48, 32, 48, 32, 49, 32, 48, 45, 55, 46, 51, 53, 32, 52, 46, 57, 54, 32, 52, 46, 57, 54, 32, 48, 32, 48, 32, 49, 32, 51, 46, 55, 52, 45, 49, 46, 53, 50, 99, 49, 46, 52, 55, 32, 48, 32, 50, 46, 55, 50, 46, 53, 49, 32, 51, 46, 55, 52, 32, 49, 46, 53, 51, 97, 52, 46, 57, 57, 32, 52, 46, 57, 57, 32, 48, 32, 48, 32, 49, 32, 49, 46, 53, 53, 32, 51, 46, 54, 57, 118, 55, 46, 50, 56, 99, 48, 32, 49, 46, 52, 50, 45, 46, 53, 51, 32, 50, 46, 54, 53, 45, 49, 46, 54, 32, 51, 46, 54, 55, 97, 53, 46, 49, 54, 32, 53, 46, 49, 54, 32, 48, 32, 48, 32, 49, 45, 51, 46, 54, 57, 32, 49, 46, 53, 52, 99, 45, 49, 46, 52, 56, 32, 48, 45, 50, 46, 55, 51, 45, 46, 53, 45, 51, 46, 55, 52, 45, 49, 46, 53, 50, 97, 53, 46, 48, 57, 32, 53, 46, 48, 57, 32, 48, 32, 48, 32, 49, 45, 49, 46, 53, 52, 45, 51, 46, 55, 118, 45, 46, 49, 53, 108, 49, 46, 56, 55, 45, 46, 56, 50, 118, 49, 46, 48, 49, 99, 48, 32, 46, 57, 52, 46, 51, 52, 32, 49, 46, 55, 51, 32, 49, 32, 50, 46, 52, 46, 54, 56, 46, 54, 54, 32, 49, 46, 52, 56, 46, 57, 57, 32, 50, 46, 52, 49, 46, 57, 57, 90, 77, 49, 49, 57, 46, 56, 32, 57, 46, 53, 118, 46, 48, 50, 99, 48, 32, 46, 57, 51, 46, 51, 52, 32, 49, 46, 55, 51, 32, 49, 32, 50, 46, 51, 57, 46, 54, 56, 46, 54, 54, 32, 49, 46, 52, 56, 32, 49, 32, 50, 46, 52, 49, 32, 49, 32, 46, 57, 32, 48, 32, 49, 46, 55, 45, 46, 51, 52, 32, 50, 46, 52, 50, 45, 49, 32, 46, 54, 54, 45, 46, 54, 32, 49, 45, 49, 46, 52, 32, 49, 45, 50, 46, 51, 57, 118, 45, 46, 48, 53, 99, 48, 45, 46, 57, 51, 45, 46, 51, 52, 45, 49, 46, 55, 50, 45, 49, 45, 50, 46, 51, 57, 45, 46, 54, 55, 45, 46, 54, 54, 45, 49, 46, 52, 55, 45, 49, 45, 50, 46, 52, 50, 45, 49, 45, 46, 57, 49, 32, 48, 45, 49, 46, 55, 49, 46, 51, 52, 45, 50, 46, 52, 32, 49, 45, 46, 54, 55, 46, 54, 54, 45, 49, 32, 49, 46, 52, 54, 45, 49, 32, 50, 46, 52, 50, 90, 109, 49, 54, 46, 56, 57, 32, 49, 48, 46, 55, 97, 51, 46, 51, 32, 51, 46, 51, 32, 48, 32, 48, 32, 48, 32, 50, 46, 52, 49, 45, 46, 57, 56, 99, 46, 54, 55, 45, 46, 54, 54, 32, 49, 45, 49, 46, 52, 54, 32, 49, 45, 50, 46, 52, 118, 45, 51, 46, 51, 54, 97, 53, 46, 49, 55, 32, 53, 46, 49, 55, 32, 48, 32, 48, 32, 49, 45, 51, 46, 52, 49, 32, 49, 46, 50, 52, 32, 53, 46, 48, 54, 32, 53, 46, 48, 54, 32, 48, 32, 48, 32, 49, 45, 51, 46, 55, 52, 45, 49, 46, 53, 52, 32, 53, 46, 49, 54, 32, 53, 46, 49, 54, 32, 48, 32, 48, 32, 49, 32, 48, 45, 55, 46, 51, 53, 32, 52, 46, 57, 54, 32, 52, 46, 57, 54, 32, 48, 32, 48, 32, 49, 32, 51, 46, 55, 52, 45, 49, 46, 53, 50, 99, 49, 46, 52, 55, 32, 48, 32, 50, 46, 55, 49, 46, 53, 49, 32, 51, 46, 55, 52, 32, 49, 46, 53, 51, 97, 52, 46, 57, 57, 32, 52, 46, 57, 57, 32, 48, 32, 48, 32, 49, 32, 49, 46, 53, 52, 32, 51, 46, 54, 57, 118, 55, 46, 50, 56, 99, 48, 32, 49, 46, 52, 50, 45, 46, 53, 51, 32, 50, 46, 54, 53, 45, 49, 46, 54, 32, 51, 46, 54, 55, 65, 53, 46, 49, 54, 32, 53, 46, 49, 54, 32, 48, 32, 48, 32, 49, 32, 49, 51, 54, 46, 55, 32, 50, 50, 99, 45, 49, 46, 52, 57, 32, 48, 45, 50, 46, 55, 51, 45, 46, 53, 45, 51, 46, 55, 52, 45, 49, 46, 53, 50, 97, 53, 46, 49, 32, 53, 46, 49, 32, 48, 32, 48, 32, 49, 45, 49, 46, 53, 53, 45, 51, 46, 55, 118, 45, 46, 49, 53, 108, 49, 46, 56, 56, 45, 46, 56, 50, 118, 49, 46, 48, 49, 99, 48, 32, 46, 57, 52, 46, 51, 51, 32, 49, 46, 55, 51, 32, 49, 32, 50, 46, 52, 46, 54, 56, 46, 54, 54, 32, 49, 46, 52, 56, 46, 57, 57, 32, 50, 46, 52, 46, 57, 57, 90, 109, 45, 51, 46, 52, 49, 45, 49, 48, 46, 55, 118, 46, 48, 50, 99, 48, 32, 46, 57, 51, 46, 51, 51, 32, 49, 46, 55, 51, 32, 49, 32, 50, 46, 51, 57, 46, 54, 56, 46, 54, 54, 32, 49, 46, 52, 56, 32, 49, 32, 50, 46, 52, 32, 49, 32, 46, 57, 32, 48, 32, 49, 46, 55, 49, 45, 46, 51, 52, 32, 50, 46, 52, 50, 45, 49, 32, 46, 54, 55, 45, 46, 54, 32, 49, 45, 49, 46, 52, 32, 49, 45, 50, 46, 51, 57, 118, 45, 46, 48, 53, 99, 48, 45, 46, 57, 51, 45, 46, 51, 51, 45, 49, 46, 55, 50, 45, 49, 45, 50, 46, 51, 57, 45, 46, 54, 54, 45, 46, 54, 54, 45, 49, 46, 52, 55, 45, 49, 45, 50, 46, 52, 49, 45, 49, 45, 46, 57, 50, 32, 48, 45, 49, 46, 55, 50, 46, 51, 52, 45, 50, 46, 52, 32, 49, 45, 46, 54, 56, 46, 54, 54, 45, 49, 46, 48, 49, 32, 49, 46, 52, 54, 45, 49, 46, 48, 49, 32, 50, 46, 52, 50, 90, 109, 49, 54, 46, 50, 56, 32, 49, 50, 46, 52, 53, 99, 45, 46, 50, 54, 45, 46, 48, 54, 45, 46, 53, 45, 46, 50, 45, 46, 55, 45, 46, 52, 97, 49, 46, 53, 32, 49, 46, 53, 32, 48, 32, 48, 32, 49, 45, 46, 52, 52, 45, 49, 46, 49, 99, 48, 45, 46, 52, 51, 46, 49, 53, 45, 46, 55, 57, 46, 52, 52, 45, 49, 46, 48, 57, 46, 51, 45, 46, 51, 46, 54, 54, 45, 46, 52, 53, 32, 49, 46, 48, 56, 45, 46, 52, 53, 46, 51, 56, 32, 48, 32, 46, 55, 52, 46, 49, 52, 32, 49, 46, 48, 56, 46, 52, 50, 46, 51, 51, 46, 50, 55, 46, 53, 52, 46, 54, 55, 46, 54, 32, 49, 46, 50, 97, 52, 46, 48, 51, 32, 52, 46, 48, 51, 32, 48, 32, 48, 32, 49, 45, 46, 52, 50, 32, 50, 46, 51, 55, 99, 45, 46, 53, 54, 32, 49, 46, 49, 45, 49, 46, 50, 55, 32, 49, 46, 55, 54, 45, 50, 46, 49, 51, 32, 49, 46, 57, 52, 97, 51, 46, 50, 56, 32, 51, 46, 50, 56, 32, 48, 32, 48, 32, 49, 45, 49, 46, 48, 54, 46, 48, 52, 108, 46, 52, 54, 45, 49, 46, 50, 55, 99, 46, 51, 50, 45, 46, 48, 52, 46, 54, 45, 46, 50, 52, 46, 56, 51, 45, 46, 53, 57, 46, 49, 55, 45, 46, 50, 55, 46, 50, 53, 45, 46, 53, 52, 46, 50, 53, 45, 46, 56, 50, 118, 45, 46, 50, 53, 90, 109, 49, 51, 46, 50, 50, 45, 49, 52, 46, 50, 56, 45, 50, 46, 55, 54, 32, 50, 46, 54, 51, 45, 49, 46, 49, 45, 49, 46, 52, 56, 32, 51, 46, 56, 55, 45, 51, 46, 55, 32, 49, 46, 56, 55, 45, 46, 56, 51, 118, 49, 54, 46, 56, 57, 108, 45, 49, 46, 56, 56, 46, 56, 50, 86, 55, 46, 54, 55, 90, 109, 49, 48, 46, 57, 51, 32, 49, 46, 56, 118, 55, 46, 51, 53, 99, 48, 32, 46, 50, 53, 46, 48, 50, 46, 52, 57, 46, 48, 55, 46, 55, 50, 108, 53, 46, 51, 52, 45, 49, 48, 46, 56, 97, 51, 46, 50, 54, 32, 51, 46, 50, 54, 32, 48, 32, 48, 32, 48, 45, 50, 45, 46, 54, 53, 99, 45, 46, 57, 53, 32, 48, 45, 49, 46, 55, 53, 46, 51, 51, 45, 50, 46, 52, 50, 32, 49, 45, 46, 54, 54, 46, 54, 53, 45, 49, 32, 49, 46, 52, 53, 45, 49, 32, 50, 46, 51, 56, 90, 109, 56, 46, 55, 46, 48, 52, 118, 55, 46, 50, 56, 99, 48, 32, 49, 46, 52, 51, 45, 46, 53, 50, 32, 50, 46, 54, 54, 45, 49, 46, 53, 54, 32, 51, 46, 54, 57, 97, 53, 46, 49, 51, 32, 53, 46, 49, 51, 32, 48, 32, 48, 32, 49, 45, 51, 46, 55, 51, 32, 49, 46, 53, 50, 32, 53, 46, 49, 32, 53, 46, 49, 32, 48, 32, 48, 32, 49, 45, 51, 46, 55, 52, 45, 49, 46, 53, 50, 32, 53, 46, 48, 50, 32, 53, 46, 48, 50, 32, 48, 32, 48, 32, 49, 45, 49, 46, 53, 53, 45, 51, 46, 55, 86, 57, 46, 53, 50, 99, 48, 45, 49, 46, 52, 52, 46, 53, 50, 45, 50, 46, 54, 55, 32, 49, 46, 53, 53, 45, 51, 46, 54, 56, 97, 53, 46, 49, 49, 32, 53, 46, 49, 49, 32, 48, 32, 48, 32, 49, 32, 51, 46, 55, 52, 45, 49, 46, 53, 52, 99, 49, 46, 52, 54, 32, 48, 32, 50, 46, 55, 46, 53, 49, 32, 51, 46, 55, 51, 32, 49, 46, 53, 52, 97, 52, 46, 57, 55, 32, 52, 46, 57, 55, 32, 48, 32, 48, 32, 49, 32, 49, 46, 53, 53, 32, 51, 46, 54, 56, 90, 109, 45, 50, 46, 48, 51, 45, 49, 46, 48, 55, 45, 53, 46, 52, 52, 32, 49, 48, 46, 57, 56, 99, 46, 54, 50, 46, 53, 51, 32, 49, 46, 51, 52, 46, 55, 57, 32, 50, 46, 49, 56, 46, 55, 57, 97, 51, 46, 51, 32, 51, 46, 51, 32, 48, 32, 48, 32, 48, 32, 50, 46, 52, 45, 49, 99, 46, 54, 54, 45, 46, 54, 54, 32, 49, 45, 49, 46, 52, 53, 32, 49, 45, 50, 46, 51, 57, 86, 57, 46, 52, 55, 99, 48, 45, 46, 51, 54, 45, 46, 48, 52, 45, 46, 55, 45, 46, 49, 53, 45, 49, 46, 48, 51, 90, 109, 55, 46, 52, 52, 32, 49, 50, 46, 55, 53, 45, 50, 46, 53, 49, 46, 56, 49, 32, 55, 46, 56, 45, 49, 53, 46, 54, 104, 45, 56, 46, 48, 50, 108, 46, 56, 50, 45, 49, 46, 56, 55, 104, 49, 48, 46, 50, 108, 45, 56, 46, 51, 32, 49, 54, 46, 54, 54, 90, 109, 49, 53, 46, 54, 52, 46, 55, 54, 99, 45, 46, 50, 54, 45, 46, 48, 54, 45, 46, 52, 57, 45, 46, 50, 45, 46, 54, 57, 45, 46, 52, 97, 49, 46, 53, 32, 49, 46, 53, 32, 48, 32, 48, 32, 49, 45, 46, 52, 53, 45, 49, 46, 49, 99, 48, 45, 46, 52, 51, 46, 49, 53, 45, 46, 55, 57, 46, 52, 53, 45, 49, 46, 48, 57, 46, 51, 45, 46, 51, 46, 54, 54, 45, 46, 52, 53, 32, 49, 46, 48, 56, 45, 46, 52, 53, 97, 49, 46, 55, 55, 32, 49, 46, 55, 55, 32, 48, 32, 48, 32, 49, 32, 49, 46, 54, 57, 32, 49, 46, 54, 50, 32, 51, 46, 57, 56, 32, 51, 46, 57, 56, 32, 48, 32, 48, 32, 49, 45, 46, 52, 51, 32, 50, 46, 51, 54, 99, 45, 46, 53, 55, 32, 49, 46, 49, 50, 45, 49, 46, 50, 56, 32, 49, 46, 55, 55, 45, 50, 46, 49, 51, 32, 49, 46, 57, 53, 97, 51, 46, 50, 56, 32, 51, 46, 50, 56, 32, 48, 32, 48, 32, 49, 45, 49, 46, 48, 55, 46, 48, 52, 108, 46, 52, 54, 45, 49, 46, 50, 55, 99, 46, 51, 51, 45, 46, 48, 52, 46, 54, 45, 46, 50, 52, 46, 56, 51, 45, 46, 53, 57, 46, 49, 56, 45, 46, 50, 55, 46, 50, 54, 45, 46, 53, 52, 46, 50, 54, 45, 46, 56, 50, 118, 45, 46, 50, 53, 90, 77, 50, 49, 54, 46, 55, 32, 55, 46, 54, 55, 108, 45, 50, 46, 55, 54, 32, 50, 46, 54, 51, 45, 49, 46, 49, 45, 49, 46, 52, 56, 32, 51, 46, 56, 54, 45, 51, 46, 55, 32, 49, 46, 56, 56, 45, 46, 56, 51, 118, 49, 54, 46, 56, 57, 108, 45, 49, 46, 56, 56, 46, 56, 50, 86, 55, 46, 54, 55, 90, 109, 49, 51, 46, 52, 56, 32, 48, 45, 50, 46, 55, 55, 32, 50, 46, 54, 51, 45, 49, 46, 49, 45, 49, 46, 52, 56, 32, 51, 46, 56, 55, 45, 51, 46, 55, 32, 49, 46, 56, 55, 45, 46, 56, 51, 118, 49, 54, 46, 56, 57, 108, 45, 49, 46, 56, 55, 46, 56, 50, 86, 55, 46, 54, 55, 90, 109, 49, 51, 46, 57, 50, 32, 53, 46, 54, 49, 99, 45, 46, 53, 32, 48, 45, 49, 46, 48, 51, 46, 49, 45, 49, 46, 54, 46, 51, 50, 108, 45, 46, 57, 52, 46, 52, 53, 45, 49, 46, 52, 45, 49, 46, 50, 51, 32, 49, 46, 53, 53, 45, 56, 46, 51, 104, 55, 46, 53, 51, 108, 45, 46, 57, 56, 32, 49, 46, 56, 56, 104, 45, 53, 108, 45, 49, 46, 48, 49, 32, 53, 46, 52, 97, 54, 46, 54, 56, 32, 54, 46, 54, 56, 32, 48, 32, 48, 32, 49, 32, 50, 46, 52, 55, 45, 46, 50, 56, 99, 49, 46, 54, 46, 49, 53, 32, 50, 46, 56, 51, 46, 54, 55, 32, 51, 46, 55, 32, 49, 46, 53, 54, 46, 57, 49, 46, 57, 50, 32, 49, 46, 51, 54, 32, 50, 46, 49, 51, 32, 49, 46, 51, 54, 32, 51, 46, 54, 50, 118, 46, 48, 52, 99, 48, 32, 49, 46, 52, 56, 45, 46, 53, 32, 50, 46, 55, 51, 45, 49, 46, 53, 50, 32, 51, 46, 55, 52, 97, 53, 46, 49, 32, 53, 46, 49, 32, 48, 32, 48, 32, 49, 45, 51, 46, 55, 52, 32, 49, 46, 53, 50, 99, 45, 49, 46, 52, 56, 32, 48, 45, 50, 46, 55, 51, 45, 46, 53, 45, 51, 46, 55, 52, 45, 49, 46, 53, 50, 97, 53, 46, 48, 57, 32, 53, 46, 48, 57, 32, 48, 32, 48, 32, 49, 45, 49, 46, 53, 52, 45, 51, 46, 55, 118, 45, 46, 49, 53, 108, 49, 46, 56, 55, 45, 46, 56, 50, 118, 49, 46, 48, 49, 99, 48, 32, 46, 57, 52, 46, 51, 52, 32, 49, 46, 55, 51, 32, 49, 32, 50, 46, 52, 46, 54, 56, 46, 54, 54, 32, 49, 46, 52, 56, 46, 57, 57, 32, 50, 46, 52, 49, 46, 57, 57, 46, 57, 53, 32, 48, 32, 49, 46, 55, 53, 45, 46, 51, 52, 32, 50, 46, 52, 50, 45, 49, 97, 51, 46, 51, 32, 51, 46, 51, 32, 48, 32, 48, 32, 48, 32, 49, 45, 50, 46, 52, 99, 48, 45, 49, 46, 49, 50, 45, 46, 51, 52, 45, 49, 46, 57, 54, 45, 49, 45, 50, 46, 53, 50, 97, 52, 46, 48, 55, 32, 52, 46, 48, 55, 32, 48, 32, 48, 32, 48, 45, 50, 46, 56, 52, 45, 49, 90, 109, 49, 51, 46, 50, 56, 32, 56, 46, 54, 55, 99, 45, 46, 50, 54, 45, 46, 48, 54, 45, 46, 53, 45, 46, 50, 45, 46, 55, 45, 46, 52, 97, 49, 46, 53, 32, 49, 46, 53, 32, 48, 32, 48, 32, 49, 45, 46, 52, 52, 45, 49, 46, 49, 99, 48, 45, 46, 52, 51, 46, 49, 53, 45, 46, 55, 57, 46, 52, 53, 45, 49, 46, 48, 57, 46, 51, 45, 46, 51, 46, 54, 53, 45, 46, 52, 53, 32, 49, 46, 48, 55, 45, 46, 52, 53, 97, 49, 46, 55, 55, 32, 49, 46, 55, 55, 32, 48, 32, 48, 32, 49, 32, 49, 46, 54, 57, 32, 49, 46, 54, 50, 32, 51, 46, 57, 56, 32, 51, 46, 57, 56, 32, 48, 32, 48, 32, 49, 45, 46, 52, 50, 32, 50, 46, 51, 54, 99, 45, 46, 53, 55, 32, 49, 46, 49, 50, 45, 49, 46, 50, 56, 32, 49, 46, 55, 55, 45, 50, 46, 49, 51, 32, 49, 46, 57, 53, 97, 51, 46, 51, 32, 51, 46, 51, 32, 48, 32, 48, 32, 49, 45, 49, 46, 48, 55, 46, 48, 52, 108, 46, 52, 54, 45, 49, 46, 50, 55, 99, 46, 51, 51, 45, 46, 48, 52, 46, 54, 45, 46, 50, 52, 46, 56, 51, 45, 46, 53, 57, 46, 49, 55, 45, 46, 50, 55, 46, 50, 54, 45, 46, 53, 52, 46, 50, 54, 45, 46, 56, 50, 118, 45, 46, 50, 53, 90, 77, 50, 55, 48, 46, 54, 32, 55, 46, 54, 55, 108, 45, 50, 46, 55, 55, 32, 50, 46, 54, 51, 45, 49, 46, 49, 45, 49, 46, 52, 56, 32, 51, 46, 56, 55, 45, 51, 46, 55, 32, 49, 46, 56, 55, 45, 46, 56, 51, 118, 49, 54, 46, 56, 57, 108, 45, 49, 46, 56, 55, 46, 56, 50, 86, 55, 46, 54, 55, 90, 109, 49, 51, 46, 52, 55, 32, 48, 45, 50, 46, 55, 54, 32, 50, 46, 54, 51, 45, 49, 46, 49, 45, 49, 46, 52, 56, 32, 51, 46, 56, 54, 45, 51, 46, 55, 32, 49, 46, 56, 56, 45, 46, 56, 51, 118, 49, 54, 46, 56, 57, 108, 45, 49, 46, 56, 56, 46, 56, 50, 86, 55, 46, 54, 55, 90, 109, 49, 52, 46, 51, 52, 45, 49, 46, 53, 56, 97, 51, 46, 51, 32, 51, 46, 51, 32, 48, 32, 48, 32, 48, 45, 50, 46, 52, 50, 46, 57, 56, 99, 45, 46, 54, 54, 46, 54, 54, 45, 49, 32, 49, 46, 52, 55, 45, 49, 32, 50, 46, 52, 118, 51, 46, 51, 55, 99, 49, 45, 46, 56, 51, 32, 50, 46, 49, 51, 45, 49, 46, 50, 53, 32, 51, 46, 52, 50, 45, 49, 46, 50, 53, 32, 49, 46, 52, 55, 32, 48, 32, 50, 46, 55, 50, 46, 53, 50, 32, 51, 46, 55, 51, 32, 49, 46, 53, 52, 97, 53, 46, 49, 53, 32, 53, 46, 49, 53, 32, 48, 32, 48, 32, 49, 32, 48, 32, 55, 46, 51, 53, 32, 52, 46, 57, 54, 32, 52, 46, 57, 54, 32, 48, 32, 48, 32, 49, 45, 51, 46, 55, 51, 32, 49, 46, 53, 50, 32, 53, 46, 49, 32, 53, 46, 49, 32, 48, 32, 48, 32, 49, 45, 51, 46, 55, 52, 45, 49, 46, 53, 50, 32, 52, 46, 57, 57, 32, 52, 46, 57, 57, 32, 48, 32, 48, 32, 49, 45, 49, 46, 53, 53, 45, 51, 46, 55, 86, 57, 46, 53, 50, 99, 48, 45, 49, 46, 52, 51, 46, 53, 51, 45, 50, 46, 54, 54, 32, 49, 46, 54, 45, 51, 46, 54, 56, 97, 53, 46, 49, 54, 32, 53, 46, 49, 54, 32, 48, 32, 48, 32, 49, 32, 51, 46, 54, 57, 45, 49, 46, 53, 52, 99, 49, 46, 52, 56, 32, 48, 32, 50, 46, 55, 51, 46, 53, 49, 32, 51, 46, 55, 51, 32, 49, 46, 53, 51, 97, 53, 46, 48, 57, 32, 53, 46, 48, 57, 32, 48, 32, 48, 32, 49, 32, 49, 46, 53, 53, 32, 51, 46, 54, 57, 118, 46, 49, 53, 108, 45, 49, 46, 56, 55, 46, 56, 50, 118, 45, 49, 99, 48, 45, 46, 57, 52, 45, 46, 51, 52, 45, 49, 46, 55, 52, 45, 49, 46, 48, 49, 45, 50, 46, 52, 97, 51, 46, 51, 32, 51, 46, 51, 32, 48, 32, 48, 32, 48, 45, 50, 46, 52, 45, 49, 90, 109, 51, 46, 52, 32, 49, 48, 46, 55, 118, 45, 46, 48, 50, 99, 48, 45, 46, 57, 51, 45, 46, 51, 51, 45, 49, 46, 55, 50, 45, 49, 45, 50, 46, 51, 57, 97, 51, 46, 51, 32, 51, 46, 51, 32, 48, 32, 48, 32, 48, 45, 50, 46, 52, 45, 49, 99, 45, 46, 57, 32, 48, 45, 49, 46, 55, 46, 51, 52, 45, 50, 46, 52, 50, 32, 49, 45, 46, 54, 54, 46, 54, 49, 45, 49, 32, 49, 46, 52, 45, 49, 32, 50, 46, 52, 118, 46, 48, 52, 99, 48, 32, 46, 57, 51, 46, 51, 52, 32, 49, 46, 55, 51, 32, 49, 32, 50, 46, 52, 46, 54, 55, 46, 54, 54, 32, 49, 46, 52, 55, 46, 57, 57, 32, 50, 46, 52, 50, 46, 57, 57, 46, 57, 49, 32, 48, 32, 49, 46, 55, 49, 45, 46, 51, 52, 32, 50, 46, 52, 45, 49, 32, 46, 54, 55, 45, 46, 54, 54, 32, 49, 45, 49, 46, 52, 54, 32, 49, 45, 50, 46, 52, 49, 90, 109, 57, 46, 52, 54, 32, 53, 46, 49, 54, 99, 45, 46, 50, 53, 45, 46, 48, 54, 45, 46, 52, 56, 45, 46, 50, 45, 46, 54, 57, 45, 46, 52, 97, 49, 46, 53, 32, 49, 46, 53, 32, 48, 32, 48, 32, 49, 45, 46, 52, 52, 45, 49, 46, 49, 99, 48, 45, 46, 52, 51, 46, 49, 53, 45, 46, 55, 57, 46, 52, 52, 45, 49, 46, 48, 57, 46, 51, 45, 46, 51, 46, 54, 54, 45, 46, 52, 53, 32, 49, 46, 48, 56, 45, 46, 52, 53, 46, 51, 56, 32, 48, 32, 46, 55, 51, 46, 49, 52, 32, 49, 46, 48, 56, 46, 52, 50, 46, 51, 51, 46, 50, 55, 46, 53, 52, 46, 54, 55, 46, 54, 32, 49, 46, 50, 97, 52, 46, 48, 52, 32, 52, 46, 48, 52, 32, 48, 32, 48, 32, 49, 45, 46, 52, 50, 32, 50, 46, 51, 55, 99, 45, 46, 53, 54, 32, 49, 46, 49, 45, 49, 46, 50, 56, 32, 49, 46, 55, 54, 45, 50, 46, 49, 51, 32, 49, 46, 57, 52, 97, 51, 46, 51, 32, 51, 46, 51, 32, 48, 32, 48, 32, 49, 45, 49, 46, 48, 54, 46, 48, 52, 108, 46, 52, 53, 45, 49, 46, 50, 55, 99, 46, 51, 51, 45, 46, 48, 52, 46, 54, 45, 46, 50, 52, 46, 56, 52, 45, 46, 53, 57, 46, 49, 55, 45, 46, 50, 55, 46, 50, 53, 45, 46, 53, 52, 46, 50, 53, 45, 46, 56, 50, 118, 45, 46, 50, 53, 90, 109, 49, 52, 46, 48, 57, 45, 49, 46, 55, 52, 97, 51, 46, 51, 32, 51, 46, 51, 32, 48, 32, 48, 32, 48, 32, 50, 46, 52, 49, 45, 46, 57, 57, 99, 46, 54, 55, 45, 46, 54, 54, 32, 49, 45, 49, 46, 52, 54, 32, 49, 45, 50, 46, 52, 118, 45, 51, 46, 51, 54, 97, 53, 46, 49, 55, 32, 53, 46, 49, 55, 32, 48, 32, 48, 32, 49, 45, 51, 46, 52, 49, 32, 49, 46, 50, 52, 32, 53, 46, 48, 54, 32, 53, 46, 48, 54, 32, 48, 32, 48, 32, 49, 45, 51, 46, 55, 52, 45, 49, 46, 53, 52, 32, 53, 46, 49, 54, 32, 53, 46, 49, 54, 32, 48, 32, 48, 32, 49, 32, 48, 45, 55, 46, 51, 53, 32, 52, 46, 57, 54, 32, 52, 46, 57, 54, 32, 48, 32, 48, 32, 49, 32, 51, 46, 55, 52, 45, 49, 46, 53, 50, 99, 49, 46, 52, 55, 32, 48, 32, 50, 46, 55, 49, 46, 53, 49, 32, 51, 46, 55, 52, 32, 49, 46, 53, 51, 97, 52, 46, 57, 57, 32, 52, 46, 57, 57, 32, 48, 32, 48, 32, 49, 32, 49, 46, 53, 52, 32, 51, 46, 54, 57, 118, 55, 46, 50, 56, 99, 48, 32, 49, 46, 52, 50, 45, 46, 53, 51, 32, 50, 46, 54, 53, 45, 49, 46, 53, 57, 32, 51, 46, 54, 55, 97, 53, 46, 49, 54, 32, 53, 46, 49, 54, 32, 48, 32, 48, 32, 49, 45, 51, 46, 55, 32, 49, 46, 53, 52, 99, 45, 49, 46, 52, 56, 32, 48, 45, 50, 46, 55, 50, 45, 46, 53, 45, 51, 46, 55, 51, 45, 49, 46, 53, 50, 97, 53, 46, 49, 32, 53, 46, 49, 32, 48, 32, 48, 32, 49, 45, 49, 46, 53, 53, 45, 51, 46, 55, 118, 45, 46, 49, 53, 108, 49, 46, 56, 56, 45, 46, 56, 50, 118, 49, 46, 48, 49, 99, 48, 32, 46, 57, 52, 46, 51, 52, 32, 49, 46, 55, 51, 32, 49, 32, 50, 46, 52, 46, 54, 56, 46, 54, 54, 32, 49, 46, 52, 56, 46, 57, 57, 32, 50, 46, 52, 46, 57, 57, 90, 77, 51, 50, 49, 46, 57, 53, 32, 57, 46, 53, 118, 46, 48, 50, 99, 48, 32, 46, 57, 51, 46, 51, 52, 32, 49, 46, 55, 51, 32, 49, 32, 50, 46, 51, 57, 46, 54, 56, 46, 54, 54, 32, 49, 46, 52, 56, 32, 49, 32, 50, 46, 52, 32, 49, 32, 46, 57, 32, 48, 32, 49, 46, 55, 49, 45, 46, 51, 52, 32, 50, 46, 52, 50, 45, 49, 32, 46, 54, 55, 45, 46, 54, 32, 49, 45, 49, 46, 52, 32, 49, 45, 50, 46, 51, 57, 118, 45, 46, 48, 53, 99, 48, 45, 46, 57, 51, 45, 46, 51, 51, 45, 49, 46, 55, 50, 45, 49, 45, 50, 46, 51, 57, 45, 46, 54, 54, 45, 46, 54, 54, 45, 49, 46, 52, 54, 45, 49, 45, 50, 46, 52, 49, 45, 49, 45, 46, 57, 50, 32, 48, 45, 49, 46, 55, 50, 46, 51, 52, 45, 50, 46, 52, 32, 49, 45, 46, 54, 55, 46, 54, 54, 45, 49, 46, 48, 49, 32, 49, 46, 52, 54, 45, 49, 46, 48, 49, 32, 50, 46, 52, 50, 90, 109, 49, 52, 46, 49, 32, 49, 49, 46, 55, 45, 50, 46, 53, 46, 56, 32, 55, 46, 56, 45, 49, 53, 46, 54, 104, 45, 56, 46, 48, 50, 108, 46, 56, 50, 45, 49, 46, 56, 55, 104, 49, 48, 46, 50, 108, 45, 56, 46, 51, 32, 49, 54, 46, 54, 54, 90, 109, 49, 53, 46, 54, 53, 46, 55, 53, 99, 45, 46, 50, 53, 45, 46, 48, 54, 45, 46, 52, 57, 45, 46, 50, 45, 46, 54, 57, 45, 46, 52, 97, 49, 46, 53, 32, 49, 46, 53, 32, 48, 32, 48, 32, 49, 45, 46, 52, 52, 45, 49, 46, 49, 99, 48, 45, 46, 52, 51, 46, 49, 52, 45, 46, 55, 57, 46, 52, 52, 45, 49, 46, 48, 57, 46, 51, 45, 46, 51, 46, 54, 54, 45, 46, 52, 53, 32, 49, 46, 48, 56, 45, 46, 52, 53, 97, 49, 46, 55, 55, 32, 49, 46, 55, 55, 32, 48, 32, 48, 32, 49, 32, 49, 46, 54, 57, 32, 49, 46, 54, 50, 32, 51, 46, 57, 56, 32, 51, 46, 57, 56, 32, 48, 32, 48, 32, 49, 45, 46, 52, 51, 32, 50, 46, 51, 54, 99, 45, 46, 53, 54, 32, 49, 46, 49, 50, 45, 49, 46, 50, 56, 32, 49, 46, 55, 55, 45, 50, 46, 49, 51, 32, 49, 46, 57, 53, 97, 51, 46, 51, 32, 51, 46, 51, 32, 48, 32, 48, 32, 49, 45, 49, 46, 48, 54, 46, 48, 52, 108, 46, 52, 53, 45, 49, 46, 50, 55, 99, 46, 51, 51, 45, 46, 48, 52, 46, 54, 45, 46, 50, 52, 46, 56, 51, 45, 46, 53, 57, 46, 49, 56, 45, 46, 50, 55, 46, 50, 54, 45, 46, 53, 52, 46, 50, 54, 45, 46, 56, 50, 118, 45, 46, 50, 53, 90, 109, 49, 51, 46, 50, 51, 45, 49, 52, 46, 50, 56, 45, 50, 46, 55, 54, 32, 50, 46, 54, 51, 45, 49, 46, 49, 45, 49, 46, 52, 56, 32, 51, 46, 56, 54, 45, 51, 46, 55, 32, 49, 46, 56, 56, 45, 46, 56, 51, 118, 49, 54, 46, 56, 57, 108, 45, 49, 46, 56, 56, 46, 56, 50, 86, 55, 46, 54, 55, 90, 109, 49, 51, 46, 52, 56, 32, 48, 45, 50, 46, 55, 55, 32, 50, 46, 54, 51, 45, 49, 46, 49, 45, 49, 46, 52, 56, 32, 51, 46, 56, 55, 45, 51, 46, 55, 32, 49, 46, 56, 56, 45, 46, 56, 51, 118, 49, 54, 46, 56, 57, 108, 45, 49, 46, 56, 56, 46, 56, 50, 86, 55, 46, 54, 55, 90, 109, 49, 52, 46, 51, 51, 45, 49, 46, 53, 56, 97, 51, 46, 51, 32, 51, 46, 51, 32, 48, 32, 48, 32, 48, 45, 50, 46, 52, 49, 46, 57, 56, 99, 45, 46, 54, 55, 46, 54, 54, 45, 49, 32, 49, 46, 52, 55, 45, 49, 32, 50, 46, 52, 118, 51, 46, 51, 55, 99, 49, 45, 46, 56, 51, 32, 50, 46, 49, 51, 45, 49, 46, 50, 53, 32, 51, 46, 52, 49, 45, 49, 46, 50, 53, 32, 49, 46, 52, 56, 32, 48, 32, 50, 46, 55, 50, 46, 53, 50, 32, 51, 46, 55, 52, 32, 49, 46, 53, 52, 97, 53, 46, 49, 53, 32, 53, 46, 49, 53, 32, 48, 32, 48, 32, 49, 32, 48, 32, 55, 46, 51, 53, 32, 52, 46, 57, 54, 32, 52, 46, 57, 54, 32, 48, 32, 48, 32, 49, 45, 51, 46, 55, 52, 32, 49, 46, 53, 50, 32, 53, 46, 49, 32, 53, 46, 49, 32, 48, 32, 48, 32, 49, 45, 51, 46, 55, 52, 45, 49, 46, 53, 50, 32, 52, 46, 57, 57, 32, 52, 46, 57, 57, 32, 48, 32, 48, 32, 49, 45, 49, 46, 53, 52, 45, 51, 46, 55, 86, 57, 46, 53, 50, 99, 48, 45, 49, 46, 52, 51, 46, 53, 51, 45, 50, 46, 54, 54, 32, 49, 46, 54, 45, 51, 46, 54, 56, 97, 53, 46, 49, 54, 32, 53, 46, 49, 54, 32, 48, 32, 48, 32, 49, 32, 51, 46, 54, 56, 45, 49, 46, 53, 52, 99, 49, 46, 52, 57, 32, 48, 32, 50, 46, 55, 51, 46, 53, 49, 32, 51, 46, 55, 52, 32, 49, 46, 53, 51, 97, 53, 46, 48, 57, 32, 53, 46, 48, 57, 32, 48, 32, 48, 32, 49, 32, 49, 46, 53, 53, 32, 51, 46, 54, 57, 118, 46, 49, 53, 108, 45, 49, 46, 56, 56, 46, 56, 50, 118, 45, 49, 99, 48, 45, 46, 57, 52, 45, 46, 51, 51, 45, 49, 46, 55, 52, 45, 49, 45, 50, 46, 52, 97, 51, 46, 51, 32, 51, 46, 51, 32, 48, 32, 48, 32, 48, 45, 50, 46, 52, 45, 49, 90, 109, 51, 46, 52, 49, 32, 49, 48, 46, 55, 118, 45, 46, 48, 50, 99, 48, 45, 46, 57, 51, 45, 46, 51, 51, 45, 49, 46, 55, 50, 45, 49, 45, 50, 46, 51, 57, 97, 51, 46, 51, 32, 51, 46, 51, 32, 48, 32, 48, 32, 48, 45, 50, 46, 52, 45, 49, 99, 45, 46, 57, 32, 48, 45, 49, 46, 55, 49, 46, 51, 52, 45, 50, 46, 52, 50, 32, 49, 45, 46, 54, 55, 46, 54, 49, 45, 49, 32, 49, 46, 52, 45, 49, 32, 50, 46, 52, 118, 46, 48, 52, 99, 48, 32, 46, 57, 51, 46, 51, 51, 32, 49, 46, 55, 51, 32, 49, 32, 50, 46, 52, 46, 54, 54, 46, 54, 54, 32, 49, 46, 52, 55, 46, 57, 57, 32, 50, 46, 52, 49, 46, 57, 57, 46, 57, 50, 32, 48, 32, 49, 46, 55, 50, 45, 46, 51, 52, 32, 50, 46, 52, 45, 49, 32, 46, 54, 56, 45, 46, 54, 54, 32, 49, 46, 48, 49, 45, 49, 46, 52, 54, 32, 49, 46, 48, 49, 45, 50, 46, 52, 49, 90, 109, 57, 46, 52, 54, 32, 53, 46, 49, 54, 99, 45, 46, 50, 54, 45, 46, 48, 54, 45, 46, 52, 57, 45, 46, 50, 45, 46, 55, 45, 46, 52, 97, 49, 46, 53, 32, 49, 46, 53, 32, 48, 32, 48, 32, 49, 45, 46, 52, 52, 45, 49, 46, 49, 99, 48, 45, 46, 52, 51, 46, 49, 53, 45, 46, 55, 57, 46, 52, 53, 45, 49, 46, 48, 57, 46, 51, 45, 46, 51, 46, 54, 53, 45, 46, 52, 53, 32, 49, 46, 48, 56, 45, 46, 52, 53, 46, 51, 55, 32, 48, 32, 46, 55, 51, 46, 49, 52, 32, 49, 46, 48, 55, 46, 52, 50, 46, 51, 52, 46, 50, 55, 46, 53, 52, 46, 54, 55, 46, 54, 49, 32, 49, 46, 50, 97, 52, 46, 48, 52, 32, 52, 46, 48, 52, 32, 48, 32, 48, 32, 49, 45, 46, 52, 50, 32, 50, 46, 51, 55, 99, 45, 46, 53, 55, 32, 49, 46, 49, 45, 49, 46, 50, 56, 32, 49, 46, 55, 54, 45, 50, 46, 49, 51, 32, 49, 46, 57, 52, 97, 51, 46, 51, 32, 51, 46, 51, 32, 48, 32, 48, 32, 49, 45, 49, 46, 48, 55, 46, 48, 52, 108, 46, 52, 54, 45, 49, 46, 50, 55, 99, 46, 51, 51, 45, 46, 48, 52, 46, 54, 45, 46, 50, 52, 46, 56, 51, 45, 46, 53, 57, 46, 49, 55, 45, 46, 50, 55, 46, 50, 54, 45, 46, 53, 52, 46, 50, 54, 45, 46, 56, 50, 118, 45, 46, 50, 53, 90, 109, 49, 51, 46, 55, 51, 45, 56, 46, 54, 56, 99, 45, 46, 50, 50, 32, 48, 45, 46, 53, 54, 46, 48, 56, 45, 49, 32, 46, 50, 53, 108, 45, 46, 57, 57, 45, 46, 56, 55, 32, 51, 46, 56, 45, 54, 46, 50, 53, 104, 45, 54, 46, 51, 108, 46, 56, 49, 45, 49, 46, 56, 55, 104, 56, 46, 55, 55, 108, 45, 52, 46, 48, 51, 32, 54, 46, 57, 54, 99, 49, 46, 49, 52, 46, 49, 55, 32, 50, 46, 49, 53, 46, 54, 56, 32, 51, 46, 48, 50, 32, 49, 46, 53, 51, 97, 52, 46, 57, 51, 32, 52, 46, 57, 51, 32, 48, 32, 48, 32, 49, 32, 49, 46, 53, 53, 32, 51, 46, 54, 56, 118, 46, 48, 52, 99, 48, 32, 49, 46, 52, 56, 45, 46, 53, 32, 50, 46, 55, 51, 45, 49, 46, 53, 51, 32, 51, 46, 55, 52, 97, 53, 46, 49, 32, 53, 46, 49, 32, 48, 32, 48, 32, 49, 45, 51, 46, 55, 51, 32, 49, 46, 53, 50, 99, 45, 49, 46, 52, 57, 32, 48, 45, 50, 46, 55, 51, 45, 46, 53, 45, 51, 46, 55, 52, 45, 49, 46, 53, 50, 97, 53, 46, 49, 32, 53, 46, 49, 32, 48, 32, 48, 32, 49, 45, 49, 46, 53, 53, 45, 51, 46, 55, 118, 45, 46, 49, 53, 108, 49, 46, 56, 56, 45, 46, 56, 50, 118, 49, 46, 48, 49, 99, 48, 32, 46, 57, 52, 46, 51, 51, 32, 49, 46, 55, 51, 32, 49, 32, 50, 46, 52, 46, 54, 56, 46, 54, 54, 32, 49, 46, 52, 56, 46, 57, 57, 32, 50, 46, 52, 46, 57, 57, 46, 57, 53, 32, 48, 32, 49, 46, 55, 54, 45, 46, 51, 52, 32, 50, 46, 52, 50, 45, 49, 97, 51, 46, 51, 32, 51, 46, 51, 32, 48, 32, 48, 32, 48, 32, 49, 45, 50, 46, 52, 99, 48, 45, 46, 57, 56, 45, 46, 51, 50, 45, 49, 46, 55, 55, 45, 46, 57, 54, 45, 50, 46, 51, 56, 97, 52, 46, 49, 32, 52, 46, 49, 32, 48, 32, 48, 32, 48, 45, 50, 46, 56, 50, 45, 49, 46, 49, 54, 90, 109, 49, 57, 46, 48, 53, 45, 51, 46, 53, 97, 52, 46, 50, 32, 52, 46, 50, 32, 48, 32, 48, 32, 49, 45, 49, 46, 55, 52, 32, 51, 46, 52, 53, 32, 57, 54, 46, 52, 32, 57, 54, 46, 52, 32, 48, 32, 48, 32, 48, 45, 51, 46, 50, 53, 32, 50, 46, 54, 32, 49, 50, 46, 51, 53, 32, 49, 50, 46, 51, 53, 32, 48, 32, 48, 32, 48, 45, 50, 46, 55, 32, 51, 46, 56, 54, 108, 45, 46, 49, 46, 50, 49, 104, 56, 108, 45, 46, 56, 49, 32, 49, 46, 56, 56, 104, 45, 49, 48, 46, 48, 54, 108, 46, 57, 45, 50, 46, 51, 97, 49, 53, 46, 48, 51, 32, 49, 53, 46, 48, 51, 32, 48, 32, 48, 32, 49, 32, 52, 46, 56, 55, 45, 54, 46, 49, 53, 99, 49, 46, 48, 50, 45, 46, 55, 54, 32, 49, 46, 55, 45, 49, 46, 50, 54, 32, 50, 46, 48, 53, 45, 49, 46, 53, 46, 54, 53, 45, 46, 52, 54, 46, 57, 55, 45, 49, 46, 49, 56, 46, 57, 55, 45, 50, 46, 49, 54, 118, 45, 46, 49, 56, 97, 51, 46, 52, 32, 51, 46, 52, 32, 48, 32, 48, 32, 48, 45, 53, 46, 56, 45, 50, 46, 52, 99, 45, 46, 54, 56, 46, 54, 55, 45, 49, 46, 48, 50, 32, 49, 46, 52, 52, 45, 49, 46, 48, 50, 32, 50, 46, 51, 51, 118, 46, 48, 54, 108, 46, 48, 49, 46, 49, 50, 45, 49, 46, 56, 55, 46, 56, 50, 45, 46, 48, 49, 45, 46, 57, 99, 48, 45, 49, 46, 52, 46, 53, 49, 45, 50, 46, 54, 51, 32, 49, 46, 53, 53, 45, 51, 46, 54, 56, 97, 53, 46, 48, 54, 32, 53, 46, 48, 54, 32, 48, 32, 48, 32, 49, 32, 51, 46, 55, 51, 45, 49, 46, 53, 52, 99, 49, 46, 52, 56, 32, 48, 32, 50, 46, 55, 51, 46, 53, 49, 32, 51, 46, 55, 52, 32, 49, 46, 53, 51, 97, 53, 46, 51, 49, 32, 53, 46, 51, 49, 32, 48, 32, 48, 32, 49, 32, 49, 46, 53, 52, 32, 51, 46, 57, 54, 90, 109, 55, 46, 54, 53, 32, 49, 50, 46, 49, 56, 99, 45, 46, 50, 54, 45, 46, 48, 54, 45, 46, 52, 57, 45, 46, 50, 45, 46, 55, 45, 46, 52, 97, 49, 46, 53, 32, 49, 46, 53, 32, 48, 32, 48, 32, 49, 45, 46, 52, 52, 45, 49, 46, 49, 99, 48, 45, 46, 52, 51, 46, 49, 53, 45, 46, 55, 57, 46, 52, 53, 45, 49, 46, 48, 57, 46, 51, 45, 46, 51, 46, 54, 53, 45, 46, 52, 53, 32, 49, 46, 48, 56, 45, 46, 52, 53, 97, 49, 46, 55, 55, 32, 49, 46, 55, 55, 32, 48, 32, 48, 32, 49, 32, 49, 46, 54, 57, 32, 49, 46, 54, 50, 32, 51, 46, 57, 56, 32, 51, 46, 57, 56, 32, 48, 32, 48, 32, 49, 45, 46, 52, 51, 32, 50, 46, 51, 54, 99, 45, 46, 53, 55, 32, 49, 46, 49, 50, 45, 49, 46, 50, 56, 32, 49, 46, 55, 55, 45, 50, 46, 49, 51, 32, 49, 46, 57, 53, 97, 51, 46, 51, 32, 51, 46, 51, 32, 48, 32, 48, 32, 49, 45, 49, 46, 48, 55, 46, 48, 52, 108, 46, 52, 54, 45, 49, 46, 50, 55, 99, 46, 51, 51, 45, 46, 48, 52, 46, 54, 45, 46, 50, 52, 46, 56, 51, 45, 46, 53, 57, 46, 49, 55, 45, 46, 50, 55, 46, 50, 54, 45, 46, 53, 52, 46, 50, 54, 45, 46, 56, 50, 118, 45, 46, 50, 53, 90, 109, 49, 51, 46, 54, 55, 45, 56, 46, 54, 55, 99, 45, 46, 53, 32, 48, 45, 49, 46, 48, 51, 46, 49, 45, 49, 46, 53, 57, 46, 51, 50, 108, 45, 46, 57, 53, 46, 52, 53, 45, 49, 46, 52, 45, 49, 46, 50, 51, 32, 49, 46, 53, 53, 45, 56, 46, 51, 104, 55, 46, 53, 52, 108, 45, 46, 57, 57, 32, 49, 46, 56, 56, 104, 45, 52, 46, 57, 57, 108, 45, 49, 46, 48, 50, 32, 53, 46, 52, 97, 54, 46, 54, 56, 32, 54, 46, 54, 56, 32, 48, 32, 48, 32, 49, 32, 50, 46, 52, 56, 45, 46, 50, 56, 99, 49, 46, 53, 57, 46, 49, 53, 32, 50, 46, 56, 50, 46, 54, 55, 32, 51, 46, 55, 32, 49, 46, 53, 54, 46, 57, 46, 57, 50, 32, 49, 46, 51, 54, 32, 50, 46, 49, 51, 32, 49, 46, 51, 54, 32, 51, 46, 54, 50, 118, 46, 48, 52, 99, 48, 32, 49, 46, 52, 56, 45, 46, 53, 32, 50, 46, 55, 51, 45, 49, 46, 53, 50, 32, 51, 46, 55, 52, 97, 53, 46, 49, 32, 53, 46, 49, 32, 48, 32, 48, 32, 49, 45, 51, 46, 55, 52, 32, 49, 46, 53, 50, 99, 45, 49, 46, 52, 57, 32, 48, 45, 50, 46, 55, 51, 45, 46, 53, 45, 51, 46, 55, 52, 45, 49, 46, 53, 50, 97, 53, 46, 48, 57, 32, 53, 46, 48, 57, 32, 48, 32, 48, 32, 49, 45, 49, 46, 53, 53, 45, 51, 46, 55, 118, 45, 46, 49, 53, 108, 49, 46, 56, 56, 45, 46, 56, 50, 118, 49, 46, 48, 49, 99, 48, 32, 46, 57, 52, 46, 51, 51, 32, 49, 46, 55, 51, 32, 49, 32, 50, 46, 52, 46, 54, 56, 46, 54, 54, 32, 49, 46, 52, 56, 46, 57, 57, 32, 50, 46, 52, 46, 57, 57, 46, 57, 53, 32, 48, 32, 49, 46, 55, 54, 45, 46, 51, 52, 32, 50, 46, 52, 50, 45, 49, 97, 51, 46, 51, 32, 51, 46, 51, 32, 48, 32, 48, 32, 48, 32, 49, 45, 50, 46, 52, 99, 48, 45, 49, 46, 49, 50, 45, 46, 51, 51, 45, 49, 46, 57, 54, 45, 46, 57, 57, 45, 50, 46, 53, 50, 97, 52, 46, 48, 54, 32, 52, 46, 48, 54, 32, 48, 32, 48, 32, 48, 45, 50, 46, 56, 53, 45, 49, 90, 109, 49, 57, 46, 49, 45, 51, 46, 53, 97, 52, 46, 50, 32, 52, 46, 50, 32, 48, 32, 48, 32, 49, 45, 49, 46, 55, 51, 32, 51, 46, 52, 52, 32, 57, 55, 46, 50, 52, 32, 57, 55, 46, 50, 52, 32, 48, 32, 48, 32, 48, 45, 51, 46, 50, 54, 32, 50, 46, 54, 32, 49, 50, 46, 51, 54, 32, 49, 50, 46, 51, 54, 32, 48, 32, 48, 32, 48, 45, 50, 46, 55, 32, 51, 46, 56, 54, 108, 45, 46, 49, 46, 50, 49, 104, 56, 46, 48, 50, 108, 45, 46, 56, 50, 32, 49, 46, 56, 56, 104, 45, 49, 48, 46, 48, 54, 108, 46, 57, 45, 50, 46, 51, 97, 49, 53, 46, 48, 50, 32, 49, 53, 46, 48, 50, 32, 48, 32, 48, 32, 49, 32, 52, 46, 56, 55, 45, 54, 46, 49, 53, 99, 49, 46, 48, 50, 45, 46, 55, 54, 32, 49, 46, 55, 45, 49, 46, 50, 54, 32, 50, 46, 48, 53, 45, 49, 46, 53, 46, 54, 53, 45, 46, 52, 54, 46, 57, 55, 45, 49, 46, 49, 56, 46, 57, 55, 45, 50, 46, 49, 54, 118, 45, 46, 49, 56, 97, 51, 46, 52, 32, 51, 46, 52, 32, 48, 32, 48, 32, 48, 45, 53, 46, 56, 45, 50, 46, 52, 99, 45, 46, 54, 56, 46, 54, 55, 45, 49, 46, 48, 50, 32, 49, 46, 52, 52, 45, 49, 46, 48, 50, 32, 50, 46, 51, 51, 118, 46, 48, 54, 108, 46, 48, 49, 46, 49, 50, 45, 49, 46, 56, 55, 46, 56, 50, 45, 46, 48, 49, 45, 46, 57, 99, 48, 45, 49, 46, 52, 46, 53, 49, 45, 50, 46, 54, 51, 32, 49, 46, 53, 53, 45, 51, 46, 54, 56, 97, 53, 46, 48, 54, 32, 53, 46, 48, 54, 32, 48, 32, 48, 32, 49, 32, 51, 46, 55, 51, 45, 49, 46, 53, 52, 99, 49, 46, 52, 56, 32, 48, 32, 50, 46, 55, 51, 46, 53, 49, 32, 51, 46, 55, 52, 32, 49, 46, 53, 51, 97, 53, 46, 51, 32, 53, 46, 51, 32, 48, 32, 48, 32, 49, 32, 49, 46, 53, 52, 32, 51, 46, 57, 54, 90, 109, 57, 46, 56, 53, 32, 49, 52, 46, 52, 53, 118, 49, 46, 48, 53, 108, 45, 49, 46, 56, 56, 46, 56, 50, 104, 45, 52, 46, 52, 55, 108, 46, 56, 50, 45, 49, 46, 56, 55, 104, 51, 46, 54, 54, 86, 50, 46, 48, 55, 104, 45, 52, 46, 52, 56, 108, 46, 56, 50, 45, 49, 46, 56, 56, 104, 53, 46, 53, 51, 118, 50, 52, 46, 48, 52, 90, 34, 32, 102, 105, 108, 108, 61, 34, 35, 48, 70, 70, 34, 47, 62, 60, 47, 115, 118, 103, 62, 10, 60, 47, 100, 105, 118, 62, 10, 60, 47, 100, 105, 118, 62, 10, 60, 47, 98, 111, 100, 121, 62, 10, 60, 47, 104, 116, 109, 108, 62])); 11 | 12 | const favicon = new Uint8Array(Buffer.from([0, 0, 1, 0, 1, 0, 16, 16, 0, 0, 1, 0, 24, 0, 104, 3, 0, 0, 22, 0, 0, 0, 40, 0, 0, 0, 16, 0, 0, 0, 32, 0, 0, 0, 1, 0, 24, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 255, 0, 0, 146, 73, 0, 0, 146, 73, 0, 0, 146, 73, 0, 0, 242, 73, 0, 0, 242, 73, 0, 0, 242, 73, 0, 0, 243, 249, 0, 0, 243, 249, 0, 0, 243, 249, 0, 0, 243, 249, 0, 0, 243, 249, 0, 0, 243, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 60, 33, 45, 45, 10, 36, 40, 39, 98, 111, 100, 121, 39, 41, 46, 101, 113, 40, 48, 41, 46, 99, 115, 115, 40, 39, 119, 105, 100, 116, 104, 39, 41, 10, 45, 45, 62, 10])) 13 | 14 | export class StaticData { 15 | 16 | private options: StaticConfig; 17 | private content: { [k: string]: Uint8Array } = { favicon, index }; 18 | private log: LoggerType; 19 | private dev: boolean; 20 | private _paths: Array = [] 21 | 22 | constructor() { 23 | // this.log = Container.get(Logger).for(this); 24 | // const appConfig = Container.get>(AppConfig); 25 | 26 | this.log = getAppDeps().getDep('log'); 27 | const appConfig: AppConfig = getAppDeps().getDep('config'); 28 | 29 | this.dev = appConfig.env === ENV_DEV; 30 | this.options = appConfig.static; 31 | // warmup lib 32 | for (const [key, fn] of Object.entries(this.options)) { 33 | this._paths.push(key); 34 | const raw = this.content[key] = new Uint8Array(readSync(fn)); 35 | const size = Math.round(raw.length / 1024) 36 | this.log.info(`Loaded static file: ${key}/${fn} ${size}kb`) 37 | } 38 | } 39 | 40 | get paths(): string[] { 41 | return this._paths; 42 | } 43 | 44 | getItem(key: string, libParams?: LibParams) { 45 | return this.content[key]; 46 | } 47 | 48 | rtConfig(params: LibParams): string { 49 | const now = Number(new Date()); 50 | let res = '; if(window["rstat4"]){'; 51 | res += `window["rstat4"]('configure',${JSON.stringify(params)});`; 52 | res += `window["rstat4"]('setTimeDelta', (new Date()) - (new Date(${now})));`; 53 | res += '} '; 54 | return res; 55 | } 56 | 57 | prepareLib(params: LibParams) { 58 | const cmd = Buffer.from(this.rtConfig(params)); 59 | return Buffer.concat([new Uint8Array(cmd), this.content['lib.js']]); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/bus/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tree'; 2 | export * from './tree_name'; 3 | -------------------------------------------------------------------------------- /src/bus/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { BusMsgHdr, BusMsgHdrResult } from '@app/types'; 2 | export { BusMsgHdr, BusMsgHdrResult } from '@app/types'; 3 | 4 | export interface LevelChildrenStr { 5 | handlers: string[]; 6 | children: { [key: string]: LevelChildrenStr }; 7 | } 8 | 9 | export interface LevelChildrenAsync { 10 | handlers: BusMsgHdr[]; 11 | children: { [key: string]: LevelChildrenAsync }; 12 | } 13 | 14 | export type LevelChildren = LevelChildrenAsync | LevelChildrenStr; 15 | -------------------------------------------------------------------------------- /src/bus/tree.ts: -------------------------------------------------------------------------------- 1 | 2 | // import Container from 'typedi'; 3 | import { Logger, getAppDeps } from '@rockstat/rock-me-ts'; 4 | import { LevelChildrenAsync, BusMsgHdr, BusMsgHdrResult } from './interfaces' 5 | 6 | export class TreeBus { 7 | 8 | name: string; 9 | protected map: WeakMap> = new WeakMap(); 10 | protected log: Logger 11 | 12 | private tree: LevelChildrenAsync = { 13 | handlers: [], 14 | children: {} 15 | }; 16 | 17 | constructor(name = 'untitled') { 18 | // this.log = Container.get(Logger).for(this); 19 | this.log = getAppDeps().getDep('log').for(this); 20 | this.name = name; 21 | } 22 | 23 | handlerEvents(handler: BusMsgHdr): Array { 24 | let hel = this.map.get(handler); 25 | if (!hel) { 26 | hel = []; 27 | this.map.set(handler, hel); 28 | } 29 | return hel; 30 | } 31 | 32 | replace(keys: string[], handler: BusMsgHdr) { 33 | const hel = this.handlerEvents(handler); 34 | const newKeys = keys.filter(k => !hel.includes(k)); 35 | const rmKeys = hel.filter(k => !keys.includes(k)); 36 | for (const k of rmKeys) { 37 | this.unSubscribe(k, handler); 38 | } 39 | for (const k of newKeys) { 40 | this.subscribe(k, handler); 41 | } 42 | } 43 | 44 | subscribe(key: string | string[], handler: BusMsgHdr) { 45 | if (!handler) { 46 | throw new ReferenceError(`${this.name}: handler not present`); 47 | } 48 | if (Array.isArray(key)) { 49 | for (const k of key) { 50 | this.subscribe(k, handler); 51 | } 52 | return; 53 | } 54 | this.log.info(`${this.name}: Registering handler for ${key}}`) 55 | const parts = []; 56 | const path = key === '*' ? [] : key.split('.'); 57 | let node = this.tree; 58 | for (const name of path) { 59 | parts.push(name); 60 | if (!node.children[name]) { 61 | node.children[name] = { 62 | handlers: [], 63 | children: {} 64 | } 65 | } 66 | node = node.children[name]; 67 | } 68 | // Adding handler key 69 | node.handlers.push(handler); 70 | this.handlerEvents(handler).push(key); 71 | this.log.info(`> ${parts.join('.')}`) 72 | return this; 73 | } 74 | 75 | unSubscribe(key: string, handler: BusMsgHdr) { 76 | if (!handler) { 77 | throw new ReferenceError(`${this.name}: handler not present`); 78 | } 79 | const path = key === '*' ? [] : key.split('.').filter(e => e !== '*');; 80 | let node = this.tree; 81 | for (const name of path) { 82 | if (!node.children[name]) { 83 | return this; 84 | } 85 | node = node.children[name]; 86 | } 87 | // removing handler 88 | this.log.info(`${this.name}: Unregistering handler for ${key}}`) 89 | 90 | while (node.handlers.includes(handler)) { 91 | node.handlers.splice(node.handlers.indexOf(handler), 1); 92 | } 93 | // removing key form handler dictionary 94 | const hel = this.handlerEvents(handler); 95 | while (hel.includes(key)) { 96 | hel.splice(hel.indexOf(key), 1); 97 | } 98 | return this; 99 | } 100 | 101 | publish(key: string, msg: any): Array> { 102 | const path = key.split('.').concat(['']); 103 | let node = this.tree; 104 | const handlers: BusMsgHdrResult[] = []; 105 | for (const name of path) { 106 | for (let handler of node.handlers) { 107 | handlers.push(handler(key, msg)); 108 | } 109 | if (!node.children[name]) { 110 | break; 111 | } 112 | node = node.children[name]; 113 | } 114 | return handlers; 115 | } 116 | 117 | handler(key: string, msg: any): PromiseLike { 118 | const parts = key.split('.').concat(['']); 119 | const path: Array = []; 120 | let node = this.tree; 121 | const handlers: BusMsgHdr[] = []; 122 | for (const name of parts) { 123 | for (let handler of node.handlers) { 124 | handlers.push(handler); 125 | } 126 | if (!node.children[name]) { 127 | break; 128 | } 129 | path.push(name); 130 | node = node.children[name]; 131 | } 132 | return handlers[handlers.length - 1](path.join('.'), msg); 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/bus/tree_name.ts: -------------------------------------------------------------------------------- 1 | // import Container from 'typedi'; 2 | import { Logger, getAppDeps } from '@rockstat/rock-me-ts'; 3 | // import { printTree } from "./print"; 4 | import { LevelChildrenStr } from './interfaces' 5 | 6 | export class TreeNameBus { 7 | map: Map> = new Map(); 8 | log: Logger 9 | 10 | protected tree: LevelChildrenStr = { 11 | handlers: [], 12 | children: {} 13 | }; 14 | 15 | constructor() { 16 | this.log = getAppDeps().getDep('log').for(this); 17 | } 18 | 19 | handlerEvents(handler: string): Array { 20 | let hel = this.map.get(handler); 21 | if (!hel) { 22 | hel = []; 23 | this.map.set(handler, hel); 24 | } 25 | return hel; 26 | } 27 | 28 | 29 | replace(keys: string[], handler: string) { 30 | const hel = this.handlerEvents(handler); 31 | const newKeys = keys.filter(k => !hel.includes(k)); 32 | const rmKeys = hel.filter(k => !keys.includes(k)); 33 | for (const k of rmKeys) { 34 | this.unSubscribe(k, handler); 35 | } 36 | for (const k of newKeys) { 37 | this.subscribe(k, handler); 38 | } 39 | } 40 | 41 | subscribe(key: string | string[], handler: string) { 42 | if (!handler || !key) { 43 | throw new ReferenceError('handler or key not present'); 44 | } 45 | const hel = this.handlerEvents(handler); 46 | if (Array.isArray(key)) { 47 | for (const k of key) { 48 | this.subscribe(k, handler); 49 | } 50 | return; 51 | } 52 | if (hel.indexOf(key) >= 0) { 53 | return; 54 | } 55 | 56 | const path = key === '*' ? [] : key.split('.'); 57 | let node = this.tree; 58 | for (const name of path) { 59 | if (!node.children[name]) { 60 | node.children[name] = { 61 | handlers: [], 62 | children: {} 63 | } 64 | } 65 | node = node.children[name]; 66 | } 67 | // Adding handler key 68 | node.handlers.push(handler); 69 | this.handlerEvents(handler).push(key); 70 | this.log.info(`+ added handler ${handler} to ${key} | Curr: ${hel}`); 71 | // printTree(this.tree) 72 | return this; 73 | } 74 | 75 | unSubscribe(key: string, handler: string) { 76 | if (!handler || !key) { 77 | throw new ReferenceError('handler or key not present'); 78 | } 79 | this.log.info(`- removing handler to ${key}`); 80 | const path = key === '*' ? [] : key.split('.'); 81 | let node = this.tree; 82 | for (const name of path) { 83 | if (!node.children[name]) { 84 | return this; 85 | } 86 | node = node.children[name]; 87 | } 88 | // removing handler 89 | while (node.handlers.includes(handler)) { 90 | node.handlers.splice(node.handlers.indexOf(handler), 1); 91 | } 92 | // removing key form handler dictionary 93 | const hel = this.handlerEvents(handler); 94 | while (hel.includes(key)) { 95 | hel.splice(hel.indexOf(key), 1); 96 | } 97 | // printTree(this.tree) 98 | return this; 99 | } 100 | 101 | simulate(key: string): string[] { 102 | const path = key.split('.').concat(['']); 103 | let node = this.tree; 104 | let cpath:Array = []; 105 | const handlers: string[] = []; 106 | for (const name of path) { 107 | for (let handler of node.handlers) { 108 | handlers.push(handler); 109 | } 110 | if (!node.children[name]) { 111 | break; 112 | } 113 | node = node.children[name]; 114 | } 115 | return handlers 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/constants/common.ts: -------------------------------------------------------------------------------- 1 | import { epglue, epchild } from '@app/helpers'; 2 | 3 | // === ENVs 4 | export const ENV_DEV = 'dev'; 5 | export const ENV_PROD = 'prod'; 6 | export const ENV_STAGE = 'stage'; 7 | 8 | // === RPC services 9 | 10 | export const SERVICE_DIRECTOR = 'director'; 11 | export const SERVICE_FRONTIER = 'front'; 12 | export const SERVICE_TRACK = 'track'; 13 | export const SERVICE_APP_TRACK_AND = 'app-track'; 14 | export const SERVICE_APP_TRACK_IOS = 'app-track-ios'; 15 | export const SERVICE_PIXEL = 'pixel'; 16 | export const SERVICE_REDIR = 'redir'; 17 | export const SERVICE_NONE = 'stub_none'; 18 | 19 | 20 | export const BROADCAST = 'broadcast'; 21 | export const ENRICH = 'enrich'; 22 | export const OTHER = 'other'; 23 | export const EMPTY = ''; 24 | 25 | 26 | 27 | 28 | // === RPC methods 29 | 30 | export const RPC_IAMALIVE = '__iamalive'; 31 | export const METHOD_MP_SDK_EVENTS = 'events'; 32 | export const METHOD_PING38 = 'ping38'; 33 | 34 | // === TYPES 35 | 36 | export const STRING = 'string'; 37 | 38 | 39 | // === INPUT CHANNELS 40 | export const CHANNEL_WEBSOCK = 'ws'; 41 | export const CHANNEL_HTTP = 'http'; 42 | export const CHANNEL_STATIC = 'static-request'; 43 | export const CHANNEL_HTTP_WEBHOOK = 'wh'; 44 | export const CHANNEL_HTTP_PIXEL = 'pixel'; 45 | export const CHANNEL_HTTP_REDIR = 'redir'; 46 | // unknown 47 | export const CHANNEL_NONE = 'none'; 48 | // common 49 | export const CHANNEL_GENERIC = 'gen'; 50 | // channel for web-sdk 51 | export const CHANNEL_HTTP_TRACK = 'track'; 52 | 53 | 54 | // === CATEGORIES 55 | export const INCOMING = 'in'; 56 | export const OUTGOING = 'out'; 57 | export const COMMAND = 'cmd'; 58 | 59 | // === KEYS 60 | export const KEY_NONE = 'none'; 61 | export const KEY_BROADCAST = 'broadcast'; 62 | export const KEY_HELLO = 'hello'; 63 | export const KEY_ECHO = 'echo'; 64 | 65 | // === CHANNELS+DIRECTIONS 66 | // ws 67 | export const IN_WEBSOCK = epglue(INCOMING, CHANNEL_WEBSOCK); 68 | export const OUT_WEBSOCK = epglue(OUTGOING, CHANNEL_WEBSOCK); 69 | export const CMD_WEBSOCK = epglue(COMMAND, CHANNEL_WEBSOCK); 70 | 71 | export const IN_GENERIC = epglue(INCOMING, CHANNEL_GENERIC); 72 | 73 | export const IN_WEBHOOK = epglue(INCOMING, CHANNEL_HTTP_WEBHOOK); 74 | export const IN_PIXEL = epglue(INCOMING, CHANNEL_HTTP_PIXEL); 75 | export const IN_TRACK = epglue(INCOMING, CHANNEL_HTTP_TRACK); 76 | export const IN_REDIR = epglue(INCOMING, CHANNEL_HTTP_REDIR); 77 | 78 | // base http 79 | export const PATH_HTTP_TEAPOT = epglue(CHANNEL_HTTP, '418'); 80 | export const PATH_HTTP_404 = epglue(CHANNEL_HTTP, '404'); 81 | export const PATH_HTTP_OPTS = epglue(CHANNEL_HTTP, 'options'); 82 | 83 | // === COMMANDS 84 | export const IN_WEBSOCK_HELLO = epglue(IN_WEBSOCK, KEY_HELLO); 85 | export const OUT_WEBSOCK_BROADCAST = epglue(OUT_WEBSOCK, KEY_BROADCAST); 86 | export const CMD_WEBSOCK_ADD_GROUP = epglue(CMD_WEBSOCK, 'groupadd'); 87 | -------------------------------------------------------------------------------- /src/constants/errors.ts: -------------------------------------------------------------------------------- 1 | export const ERROR_NOT_OBJECT = 'Expected object got smth else'; 2 | export const ERROR_ABSENT_DATA = 'Empry request'; 3 | -------------------------------------------------------------------------------- /src/constants/http.ts: -------------------------------------------------------------------------------- 1 | 2 | export const CONTENT_TYPE_HTML = 'text/html'; 3 | export const CONTENT_TYPE_OCTET = 'application/octet-stream'; 4 | export const CONTENT_TYPE_PLAIN = 'text/plain'; 5 | export const CONTENT_TYPE_GIF = 'image/gif'; 6 | export const CONTENT_TYPE_ICON = 'image/x-icon'; 7 | export const CONTENT_TYPE_URLENCODED = 'application/x-www-form-urlencoded'; 8 | export const CONTENT_TYPE_JSON = 'application/json'; 9 | export const CONTENT_TYPE_JS = 'text/javascript'; 10 | 11 | export const HEADER_CONTENT_TYPE = 'Content-Type'; 12 | export const HEADER_CONTENT_LENGTH = 'Content-Length'; 13 | export const HEADER_LOCATION = 'Location'; 14 | export const HEADER_RESPONSE_TIME = 'X-Response-Time'; 15 | export const HEADER_REAL_IP = 'X-Real-IP'; 16 | export const HEADER_FORWARDED_HOST = 'X-Forwarded-Host'; 17 | 18 | export const HEADER_REFERER = 'Referer'; 19 | export const HEADER_MY_NAME = 'X-My-Name'; 20 | export const HEADER_USER_AGENT = 'User-Agent' 21 | 22 | export const RESPONSE_GIF = 'GIF'; 23 | export const RESPONSE_REDIR = 'REDIR'; 24 | export const RESPONSE_JSON = 'JSON'; 25 | export const RESPONSE_AUTO = 'AUTO'; 26 | 27 | 28 | export const METHOD_OPTIONS = 'OPTIONS'; 29 | export const METHOD_POST = 'POST'; 30 | export const METHOD_GET = 'GET'; 31 | 32 | export const CONTENT_BAD_REQUEST = '{"error":"Bad request"}'; 33 | 34 | // For testing purpose 35 | export const AbsentRedir = 'https://alcolytics.ru?utm_source=AbsenRedir'; 36 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common'; 2 | export * from './http'; 3 | export * from './errors'; 4 | -------------------------------------------------------------------------------- /src/enrichers/fingerprint.ts: -------------------------------------------------------------------------------- 1 | import { BaseIncomingMessage, BusBaseEnricher, Dictionary } from "@app/types"; 2 | import { xxhash } from "@rockstat/rock-me-ts"; 3 | 4 | export class FingerPrintEnricher implements BusBaseEnricher { 5 | 6 | 7 | handle = async (key: string, msg: BaseIncomingMessage): Promise> => { 8 | if (msg.td && msg.td.ip && msg.td.ua) { 9 | const fpid = xxhash(`${msg.td.ip}:${msg.td.ua}`); 10 | return { fpid }; 11 | } 12 | return {}; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/enrichers/index.ts: -------------------------------------------------------------------------------- 1 | // export * from './fingerprint' 2 | export * from './userdata' 3 | -------------------------------------------------------------------------------- /src/enrichers/userdata.ts: -------------------------------------------------------------------------------- 1 | import { BaseIncomingMessage, Dictionary, BusBaseEnricher } from "@app/types"; 2 | import { RedisClient, getAppDeps } from "@rockstat/rock-me-ts"; 3 | // import { IN_GENERIC, SERVICE_TRACK } from "@app/constants"; 4 | // import { epglue } from "@app/helpers"; 5 | // import { Container } from "typedi"; 6 | 7 | // TheIds.SInt64ToBase64() 8 | const build_key = (uid: string): string => { 9 | return 's:' + uid; 10 | } 11 | 12 | const default_ttl = 24 * 60 * 60 * 30; 13 | 14 | export class UserDataEnricher implements BusBaseEnricher { 15 | 16 | redis: RedisClient = getAppDeps().getDep('redis').create(); 17 | 18 | handle = async (key: string, msg: BaseIncomingMessage): Promise> => { 19 | 20 | if (msg.uid) { 21 | const skey = build_key(msg.uid); 22 | 23 | if (msg.service === 'userdata' && msg.name === 'update' && msg.data) { 24 | try { 25 | 26 | const data = []; 27 | 28 | for (let [k, v] of Object.entries(msg.data)) { 29 | if (k === 'uid' || k === 'ttl') { 30 | continue; 31 | } 32 | data.push(k, JSON.stringify(v)); 33 | } 34 | 35 | const ttl = msg.data.ttl || default_ttl; 36 | 37 | if (data.length) { 38 | await this.redis.hmset(skey, ...data); 39 | await this.redis.expire(skey, ttl); 40 | } 41 | 42 | return {} 43 | 44 | } catch (e) { 45 | console.error(e); 46 | } 47 | } 48 | 49 | try { 50 | 51 | const hdata = await this.redis.hgetall(skey); 52 | const stored: Dictionary = {}; 53 | if (Array.isArray(hdata) && hdata.length) { 54 | for (let i = 0; i < hdata.length; i += 2) { 55 | stored[hdata[i]] = JSON.parse(hdata[i + 1]); 56 | } 57 | return { stored }; 58 | } 59 | } catch (e) { 60 | console.error(e); 61 | } 62 | } 63 | return {}; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | 2 | class NotFoundError extends Error { 3 | constructor(message?:string){ 4 | super(message || 'NotFound'); 5 | this.name = 'NotFoundError'; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/handlers/hello.ts: -------------------------------------------------------------------------------- 1 | import { BaseIncomingMessage } from "@app/types"; 2 | import { BandResponse, response, STATUS_NOT_FOUND } from "@rockstat/rock-me-ts"; 3 | 4 | export const HelloHandler = () => { 5 | return async (key: string, msg: BaseIncomingMessage): Promise => { 6 | if (msg.name === 'ping') { 7 | return response.data({ 8 | data: { 9 | message: 'pong' 10 | } 11 | }) 12 | } 13 | if (msg.name === 'hello') { 14 | return response.data({ data: { message: 'hi' } }) 15 | } 16 | return response.error({ statusCode: STATUS_NOT_FOUND }) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/handlers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './redirect' 2 | export * from './pixel' 3 | export * from './track' 4 | export * from './hello' 5 | export * from './static' 6 | -------------------------------------------------------------------------------- /src/handlers/pixel.ts: -------------------------------------------------------------------------------- 1 | import { BaseIncomingMessage } from "@app/types"; 2 | import { BandResponse, response } from "@rockstat/rock-me-ts"; 3 | 4 | export const PixelHandler = () => { 5 | return async (key: string, msg: BaseIncomingMessage): Promise => { 6 | return response.pixel({}) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/handlers/redirect.ts: -------------------------------------------------------------------------------- 1 | import { BaseIncomingMessage } from "@app/types"; 2 | import { BandResponse, STATUS_BAD_REQUEST, response } from "@rockstat/rock-me-ts"; 3 | 4 | export const RedirectHandler = () => { 5 | return async (key: string, msg: BaseIncomingMessage): Promise => { 6 | if (msg.data.to) { 7 | return response.redirect({ location: msg.data.to }) 8 | } else { 9 | return response.error({ errorMessage: 'Parameter "to" is required', statusCode: STATUS_BAD_REQUEST }) 10 | } 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/handlers/static.ts: -------------------------------------------------------------------------------- 1 | import { BaseIncomingMessage } from "@app/types"; 2 | import { BandResponse, response, STATUS_BAD_REQUEST } from "@rockstat/rock-me-ts"; 3 | import { promises as fs } from 'fs'; 4 | import * as mime from 'mime-types'; 5 | 6 | const re_naming = new RegExp('^([a-zA-Z0-9_-]{1,50}\.[a-zA-Z0-9]{1,5})$'); 7 | 8 | 9 | export const StaticHandler = () => { 10 | return async (key: string, msg: BaseIncomingMessage): Promise => { 11 | 12 | if (!msg.ext) { 13 | return response.error({ errorMessage: 'The file extension is missing', statusCode: STATUS_BAD_REQUEST }) 14 | } 15 | 16 | const fn = msg.name + '.' + (msg.ext || '') 17 | 18 | if (re_naming.exec(fn) === null) { 19 | return response.error({ errorMessage: 'Invalid file name', statusCode: STATUS_BAD_REQUEST }) 20 | } 21 | 22 | const contentType = mime.lookup(msg.ext) || undefined; 23 | 24 | try { 25 | const data = await fs.readFile(`public/${fn}`); 26 | return response.data({ data, contentType }) 27 | 28 | } catch (error) { 29 | return response.error({ errorMessage: 'Can not find the requested file', statusCode: STATUS_BAD_REQUEST }) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/handlers/track.ts: -------------------------------------------------------------------------------- 1 | import { BaseIncomingMessage } from "@app/types"; 2 | import { BandResponse, response, STATUS_NOT_FOUND } from "@rockstat/rock-me-ts"; 3 | import { CHANNEL_HTTP_PIXEL, CHANNEL_HTTP, CHANNEL_WEBSOCK } from "@app/constants"; 4 | 5 | export const TrackHandler = () => { 6 | return async (key: string, msg: BaseIncomingMessage): Promise => { 7 | if (msg.channel === CHANNEL_HTTP_PIXEL){ 8 | return response.pixel({}); 9 | } 10 | if (msg.channel === CHANNEL_HTTP){ 11 | return response.data({data: {id: msg.id}}); 12 | } 13 | if (msg.channel === CHANNEL_WEBSOCK){ 14 | if (msg.name === 'ping') { 15 | return response.data({ 16 | data: { message: 'pong' } 17 | }) 18 | } 19 | else if (msg.name === 'hello') { 20 | return response.data({ data: { message: 'hi' } }) 21 | } 22 | 23 | return response.data({data: {id: msg.id}}); 24 | } 25 | return response.error({ statusCode: STATUS_NOT_FOUND }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/helpers/auto-domain.ts: -------------------------------------------------------------------------------- 1 | import { removeWWW } from '@app/helpers/remove-www'; 2 | import { Dictionary } from '@app/types'; 3 | 4 | export function autoDomain(domain: string) { 5 | domain = removeWWW(domain); 6 | if (!domain) return; 7 | const parts = domain.split('.'); 8 | return parts.slice(parts.length > 2 ? 1 : 0).join('.') 9 | } 10 | -------------------------------------------------------------------------------- /src/helpers/class.ts: -------------------------------------------------------------------------------- 1 | export function handleStart(cls: { started?: boolean }) { 2 | if (cls.started === true) { 3 | throw new Error('Already started'); 4 | } else { 5 | cls.started = true; 6 | } 7 | } 8 | 9 | 10 | export function handleSetup(cls: { started?: boolean }) { 11 | if (cls.started === true) { 12 | throw new Error('Already setup'); 13 | } else { 14 | cls.started = true; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/helpers/common.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Event path glue 3 | * @param args 4 | */ 5 | export function epglue(...args: Array): string { 6 | return args.join('.'); 7 | } 8 | 9 | /** 10 | * Check event path in child of parent 11 | * @param parent Parent path 12 | * @param child Child path 13 | */ 14 | export function epchild(parent: string, child: string): boolean | string { 15 | return child.substr(0, parent.length) === parent 16 | ? child.slice(parent.length, child.length) 17 | : false; 18 | 19 | } 20 | 21 | export function listVal(input?: string | string[]): string | undefined { 22 | return Array.isArray(input) ? input[0] : input; 23 | } 24 | 25 | export function pick(obj: T, paths: K[]): Pick { 26 | return { ...paths.reduce((mem, key) => ({ ...mem, [key]: obj[key] }), {}) } as Pick; 27 | } 28 | 29 | export function pick2(obj: T, paths: K[]): Pick { 30 | return Object.assign({}, ...paths.map(prop => ({ [prop]: obj[prop] }))) as Pick; 31 | } 32 | 33 | -------------------------------------------------------------------------------- /src/helpers/cybr53.ts: -------------------------------------------------------------------------------- 1 | /* 2 | cyrb53 (c) 2018 bryc (github.com/bryc) 3 | License: Public domain (or MIT if needed). Attribution appreciated. 4 | A fast and simple 53-bit string hash function with decent collision resistance. 5 | Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity. 6 | */ 7 | export const cyrb53 = (str:string, seed = 0) => { 8 | let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed; 9 | for(let i = 0, ch; i < str.length; i++) { 10 | ch = str.charCodeAt(i); 11 | h1 = Math.imul(h1 ^ ch, 2654435761); 12 | h2 = Math.imul(h2 ^ ch, 1597334677); 13 | } 14 | h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); 15 | h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); 16 | h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); 17 | h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); 18 | return 4294967296 * (2097151 & h2) + (h1 >>> 0); 19 | }; 20 | 21 | /* 22 | cyrb53a beta (c) 2023 bryc (github.com/bryc) 23 | License: Public domain (or MIT if needed). Attribution appreciated. 24 | This is a work-in-progress, and changes to the algorithm are expected. 25 | The original cyrb53 has a slight mixing bias in the low bits of h1. 26 | This doesn't affect collision rate, but I want to try to improve it. 27 | This new version has preliminary improvements in avalanche behavior. 28 | */ 29 | export const cyrb53a_beta = (str:string, seed = 0) => { 30 | let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed; 31 | for(let i = 0, ch; i < str.length; i++) { 32 | ch = str.charCodeAt(i); 33 | h1 = Math.imul(h1 ^ ch, 0x85ebca77); 34 | h2 = Math.imul(h2 ^ ch, 0xc2b2ae3d); 35 | } 36 | h1 ^= Math.imul(h1 ^ (h2 >>> 15), 0x735a2d97); 37 | h2 ^= Math.imul(h2 ^ (h1 >>> 15), 0xcaf649a9); 38 | h1 ^= h2 >>> 16; h2 ^= h1 >>> 16; 39 | return 2097152 * (h2 >>> 0) + (h1 >>> 11); 40 | }; 41 | -------------------------------------------------------------------------------- /src/helpers/file-loader.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { join } from 'path'; 3 | 4 | export const readSync = (...parts: string[]) => { 5 | return readFileSync(join(__dirname, '..', '..', ...parts)); 6 | } 7 | -------------------------------------------------------------------------------- /src/helpers/getprop.ts: -------------------------------------------------------------------------------- 1 | import * as getval from 'get-value'; 2 | import { EnrichersRequirements } from '@rockstat/rock-me-ts'; 3 | 4 | export interface DotPropGetterOptions { 5 | [k: string]: string; 6 | } 7 | 8 | export function getvals(obj: { [k: string]: any }, keys: EnrichersRequirements) { 9 | const result: { [k: string]: any } = {}; 10 | for (const [k, prop] of keys) { 11 | result[k] = getval(obj, prop); 12 | } 13 | return result; 14 | } 15 | 16 | export function dotPropGetter(options: DotPropGetterOptions) { 17 | const keys = Object.keys(options); 18 | const props = Object.values(options); 19 | 20 | return (obj: { [k: string]: any }) => { 21 | const res: { [k in keyof DotPropGetterOptions]: any } = {}; 22 | for (const [k, v] of Object.entries(options)) { 23 | res[k] = getval(obj, v); 24 | } 25 | return res; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/helpers/http.ts: -------------------------------------------------------------------------------- 1 | import { parse as parseQs } from 'qs'; 2 | import { parse as parseUrl } from 'url'; 3 | import { listVal } from './common'; 4 | import { 5 | CONTENT_TYPE_URLENCODED, 6 | CONTENT_TYPE_JSON, 7 | HEADER_REAL_IP, 8 | HEADER_USER_AGENT, 9 | HEADER_REFERER, 10 | HEADER_FORWARDED_HOST 11 | } from '../constants/http'; 12 | import { 13 | HTTPHeaders, 14 | HTTPTransportData 15 | } from '@app/types'; 16 | import { 17 | IncomingMessage 18 | } from 'http'; 19 | 20 | 21 | 22 | /** 23 | * Transparent 1x1 gif 24 | */ 25 | export const emptyGif = Buffer.from('R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', 'base64'); 26 | 27 | 28 | /** 29 | * Handles url path string 30 | * @param path 31 | */ 32 | export const pathParts = (pathString: string, mark: string): PathPartsResult => { 33 | const parts = pathString.split('/'); 34 | let ext, native = false; 35 | // remove firts slash 36 | parts.shift() 37 | let last = parts.pop() 38 | if (last) { 39 | const idx = last.lastIndexOf('.'); 40 | // if ext found, extract them 41 | if (idx >= 0) { 42 | ext = last.substr(idx + 1, last.length) 43 | last = last.substr(0, idx) 44 | } 45 | parts.push(last) 46 | } 47 | const idx = parts.indexOf(mark); 48 | if (idx >= 0) { 49 | parts.splice(idx, 1); 50 | native = true; 51 | } 52 | return { 53 | native, 54 | parts, 55 | ext 56 | } 57 | } 58 | 59 | interface PathPartsResult { 60 | parts: Array; 61 | native: boolean; 62 | ext?: string; 63 | } 64 | 65 | 66 | /** 67 | * 68 | * @param url 69 | */ 70 | function extrachHost(url?: string): string | undefined { 71 | if (!url) return; 72 | const parts = parseUrl(url); 73 | return `${parts.protocol}//${parts.host}`; 74 | } 75 | 76 | /** 77 | * Computes origin based on user agent or just take 78 | * @param origin 79 | * @param referer 80 | */ 81 | export function computeOrigin(origin?: string | string[], referer?: string | string[]): string { 82 | 83 | return listVal(origin) || extrachHost(listVal(referer)) || '*'; 84 | } 85 | 86 | /** 87 | * Main cors consts 88 | */ 89 | const CORS_MAX_AGE_SECONDS: string = `${60 * 60 * 24}`; // 24 hours 90 | const CORS_METHODS: string[] = ['POST', 'GET'] // , 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 91 | const CORS_HEADERS: string[] = ['X-Requested-With', 'Access-Control-Allow-Origin', 'Content-Type', 'Authorization', 'Accept'] 92 | const CORS_EXPOSE_HEADERS: string[] = ['Content-Length', 'Content-Type'] 93 | 94 | /** 95 | * Cookies 96 | * @param allowOrigin 97 | */ 98 | export function cookieHeaders(cookie: Array): HTTPHeaders { 99 | return [['Set-Cookie', cookie]] 100 | } 101 | 102 | /** 103 | * CORS headers 104 | * @param allowOrigin 105 | * docs: https://developer.mozilla.org/ru/docs/Web/HTTP/CORS 106 | */ 107 | const CorsHeaders = Object.entries({ 108 | 'Access-Control-Allow-Credentials': 'true', 109 | 'Access-Control-Expose-Headers': CORS_EXPOSE_HEADERS.join(','), 110 | }); 111 | export function corsHeaders(allowOrigin: string): HTTPHeaders { 112 | const ch: HTTPHeaders = [['Access-Control-Allow-Origin', allowOrigin]]; 113 | return ch.concat(CorsHeaders) 114 | } 115 | 116 | const CorsAnswerHeaders = Object.entries({ 117 | 'Access-Control-Allow-Methods': CORS_METHODS.join(','), 118 | 'Access-Control-Allow-Headers': CORS_HEADERS.join(','), 119 | 'Access-Control-Max-Age-Scope': 'domain', 120 | 'Access-Control-Max-Age': CORS_MAX_AGE_SECONDS, 121 | }); 122 | export function corsAnswerHeaders(): HTTPHeaders { 123 | return CorsAnswerHeaders; 124 | } 125 | 126 | const CorsAdditionalHeaders = Object.entries({ 127 | 'Content-Length': '0', 128 | 'Cache-Control': 'max-age=3600', 129 | 'Vary': 'Origin' 130 | }); 131 | export function corsAdditionalHeaders(): HTTPHeaders { 132 | return CorsAdditionalHeaders; 133 | } 134 | 135 | 136 | /** 137 | * Cache headers 138 | */ 139 | const NoCacheHeaders = Object.entries({ 140 | 'Pragma': 'no-cache', 141 | 'Cache-Control': 'no-cache, no-store, must-revalidate', 142 | 'Expires': 'Mon, 01 Jan 1990 21:00:12 GMT', 143 | 'Last-Modified': 'Sun, 17 May 1998 03:44:30 GMT' 144 | }); 145 | export function noCacheHeaders(): HTTPHeaders { 146 | return NoCacheHeaders; 147 | } 148 | 149 | 150 | const f = (i?: string | string[]) => Array.isArray(i) ? i[0] : i; 151 | 152 | /** 153 | * 154 | * @param req request 155 | */ 156 | export const extractTransportData: (req: IncomingMessage) => HTTPTransportData = (req) => { 157 | return { 158 | ip: f(req.headers[HEADER_REAL_IP.toLowerCase()]) || req.connection.remoteAddress || '', 159 | ua: f(req.headers[HEADER_USER_AGENT.toLowerCase()]), 160 | ref: f(req.headers[HEADER_REFERER.toLowerCase()]), 161 | host: f(req.headers[HEADER_FORWARDED_HOST.toLowerCase()]) 162 | }; 163 | } 164 | 165 | /** 166 | * Security headers 167 | */ 168 | const SecureHeaders = Object.entries({ 169 | // 'X-Content-Type-Options': 'nosniff', 170 | 'X-Frame-Options': 'SAMEORIGIN', 171 | 'X-XSS-Protection': '1' 172 | }); 173 | export function secureHeaders(): HTTPHeaders { 174 | return SecureHeaders; 175 | 176 | // Referrer-Policy 177 | // See https://www.w3.org/TR/referrer-policy/#referrer-policies 178 | } 179 | 180 | /** 181 | * Check content type is JSON 182 | * @param str 183 | */ 184 | export function isCTypeJson(str: string) { 185 | return str.toLocaleLowerCase().indexOf(CONTENT_TYPE_JSON) >= 0; 186 | } 187 | 188 | /** 189 | * Chech conten url encoded 190 | * @param str 191 | */ 192 | export function isCTypeUrlEnc(str: string) { 193 | return str.toLocaleLowerCase().indexOf(CONTENT_TYPE_URLENCODED) >= 0; 194 | } 195 | 196 | /** 197 | * Urldecode options. Size limited earlier at body parser 198 | */ 199 | const PARSE_QUERY_OPTS = { 200 | depth: 2, 201 | parseArrays: false, 202 | ignoreQueryPrefix: true 203 | }; 204 | 205 | /** 206 | * Used for parse query string and urlencoded body 207 | * @param query 208 | */ 209 | export function parseQuery(query?: string): { [key: string]: any } { 210 | return parseQs(query || '', PARSE_QUERY_OPTS); 211 | } 212 | -------------------------------------------------------------------------------- /src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common'; 2 | export * from './http'; 3 | export * from './stringTemplate'; 4 | export * from './validation'; 5 | export * from './auto-domain'; 6 | export * from './remove-www'; 7 | export * from './file-loader'; 8 | -------------------------------------------------------------------------------- /src/helpers/remove-www.ts: -------------------------------------------------------------------------------- 1 | export function removeWWW (domain: string) { 2 | if (!domain) return domain; 3 | if (domain.substr(0, 4) === 'www.') { 4 | domain = domain.substr(4, domain.length - 4); 5 | } 6 | return domain; 7 | }; 8 | -------------------------------------------------------------------------------- /src/helpers/simpleHash.ts: -------------------------------------------------------------------------------- 1 | export default function(s:string) { 2 | /* Simple hash function. */ 3 | let a = 1, c = 0, h, o; 4 | if (s) { 5 | a = 0; 6 | /*jshint plusplus:false bitwise:false*/ 7 | for (h = s.length - 1; h >= 0; h--) { 8 | o = s.charCodeAt(h); 9 | a = (a<<6&268435455) + o + (o<<14); 10 | c = a & 266338304; 11 | a = c!==0?a^c>>21:a; 12 | } 13 | } 14 | return String(a); 15 | }; 16 | -------------------------------------------------------------------------------- /src/helpers/stringTemplate.ts: -------------------------------------------------------------------------------- 1 | type TemplateParams = { 2 | [key: string]: string | number 3 | }; 4 | type TemplateCallable = (map: TemplateParams) => string 5 | 6 | export function generateTemplateString(template: string): TemplateCallable { 7 | 8 | var sanitized = template 9 | .replace(/\$\{([\s]*[^;\s\{]+[\s]*)\}/g, function (_, match) { 10 | return `\$\{map.${match.trim()}\}`; 11 | }) 12 | // Afterwards, replace anything that's not ${map.expressions}' (etc) with a blank string. 13 | .replace(/(\$\{(?!map\.)[^}]+\})/g, ''); 14 | 15 | return Function('map', `return \`${sanitized}\``); 16 | 17 | }; 18 | 19 | -------------------------------------------------------------------------------- /src/helpers/validation.ts: -------------------------------------------------------------------------------- 1 | import { ParsedQs } from "qs"; 2 | 3 | const objectToString = Object.prototype.toString; 4 | const objectAsString = '[object Object]'; 5 | 6 | const uidRE = new RegExp('^[0-9]{10,22}$'); 7 | 8 | export function isValidUid(uid?: string): boolean { 9 | if (!uid) return false; 10 | return uidRE.test(uid); 11 | }; 12 | 13 | /** 14 | * return valid uid or undefined 15 | */ 16 | export function cleanUid(uid: any): string | undefined { 17 | if (typeof uid === 'string' && isValidUid(uid)) { 18 | return uid; 19 | } 20 | } 21 | 22 | 23 | export function isObject(v: any): boolean { 24 | return !!v && typeof v === 'object' && !Array.isArray(v) && objectToString.call(v) === objectAsString; 25 | } 26 | 27 | /** 28 | * Utilized / non empty object 29 | * @param v 30 | */ 31 | export function isEmptyObject(v: any): boolean { 32 | return isObject(v) && Object.keys(v).length === 0; 33 | } 34 | 35 | /** 36 | * Check is primitive string 37 | * @param v 38 | */ 39 | export function isString(v: any): boolean { 40 | return typeof v === "string" 41 | } 42 | 43 | export function isEmptyString(v: any) { 44 | return v === ''; 45 | } 46 | 47 | /** 48 | * Check is primitive number 49 | * @param v 50 | */ 51 | export function isNumber(v: any): boolean { 52 | return typeof v === 'number'; 53 | } 54 | export const ENUM = 'enum'; 55 | 56 | export function stringToNumber(v: string): number | undefined { 57 | return isEmptyString(v) ? undefined : +v; 58 | } 59 | 60 | /** 61 | * Check is primitive boolean 62 | * @param v 63 | */ 64 | export function isBoolean(v: any): boolean { 65 | return typeof v === "boolean" 66 | } 67 | 68 | /** 69 | * Check is primitive exists 70 | * @param v 71 | */ 72 | export function isNil(v: any): boolean { 73 | return v === undefined || v === null; 74 | } 75 | -------------------------------------------------------------------------------- /src/http/http_server.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage, ServerResponse, createServer, Server, ServerOptions, OutgoingHttpHeaders } from 'http'; 2 | import { parse as urlParse } from 'url'; 3 | import * as Cookie from 'cookie'; 4 | import * as qs from 'qs'; 5 | import { parse as parseQs } from 'qs'; 6 | import * as jwt from 'jsonwebtoken'; 7 | 8 | import * as zlib from 'zlib'; 9 | import * as getRawBody from 'raw-body'; 10 | 11 | import { 12 | Meter, 13 | Logger, 14 | TheIds, 15 | AppConfig, 16 | RESP_REDIRECT, 17 | RESP_PIXEL, 18 | RESP_DATA, 19 | RESP_ERROR, 20 | response, 21 | STATUS_NOT_FOUND, 22 | STATUS_BAD_REQUEST, 23 | STATUS_INT_ERROR, 24 | STATUS_TEAPOT, 25 | BandResponse, 26 | } from '@rockstat/rock-me-ts'; 27 | import { StaticData } from '@app/StaticData'; 28 | import { Dispatcher } from '@app/Dispatcher'; 29 | import { 30 | IN_GENERIC, 31 | CHANNEL_HTTP, 32 | HEADER_RESPONSE_TIME, 33 | HEADER_CONTENT_TYPE, 34 | HEADER_CONTENT_LENGTH, 35 | HEADER_LOCATION, 36 | HEADER_MY_NAME, 37 | METHOD_GET, 38 | METHOD_POST, 39 | METHOD_OPTIONS, 40 | CONTENT_TYPE_GIF, 41 | CONTENT_TYPE_ICON, 42 | CONTENT_TYPE_JSON, 43 | CONTENT_TYPE_JS, 44 | CONTENT_TYPE_HTML, 45 | CONTENT_TYPE_OCTET, 46 | CHANNEL_HTTP_PIXEL, 47 | } from '@app/constants'; 48 | import { 49 | computeOrigin, 50 | corsHeaders, 51 | corsAnswerHeaders, 52 | secureHeaders, 53 | noCacheHeaders, 54 | emptyGif, 55 | cookieHeaders, 56 | corsAdditionalHeaders, 57 | autoDomain, 58 | epglue, 59 | cleanUid, 60 | pathParts, 61 | extractTransportData 62 | } from '@app/helpers'; 63 | import { 64 | HttpConfig, 65 | IdentifyConfig, 66 | ClientConfig, 67 | FrontierConfig, 68 | HTTPBodyParams, 69 | RouteOn, 70 | BaseIncomingMessage, 71 | Dictionary, 72 | HTTPServiceParams, 73 | } from '@app/types'; 74 | 75 | import { getAppDeps } from '@rockstat/rock-me-ts'; 76 | import { cyrb53 } from '@app/helpers/cybr53'; 77 | 78 | const extContentTypeMap: Dictionary = { 79 | 'json': CONTENT_TYPE_JSON, 80 | 'gif': CONTENT_TYPE_GIF, 81 | 'js': CONTENT_TYPE_JS, 82 | 'html': CONTENT_TYPE_HTML 83 | } 84 | 85 | const f = (i?: string | string[]) => Array.isArray(i) ? i[0] : i; 86 | 87 | const re_naming = new RegExp('^([a-zA-Z0-9\._-]{1,50})$'); 88 | 89 | type Query = qs.ParsedQs; 90 | type Cookie = Dictionary; 91 | 92 | export class HttpServer { 93 | 94 | httpServer: Server; 95 | options: HttpConfig; 96 | identopts: IdentifyConfig; 97 | clientopts: ClientConfig; 98 | dispatcher: Dispatcher; 99 | idGen: TheIds; 100 | static: StaticData; 101 | metrics: Meter; 102 | log: Logger; 103 | title: string; 104 | uidParam: string = 'uid'; 105 | uidCookie: string; 106 | urlMark: string; 107 | cookieExpires: Date; 108 | cookieDomain?: string; 109 | servicesParams: { [k: string]: HTTPServiceParams } 110 | 111 | constructor(dispatcher: Dispatcher) { 112 | const config: AppConfig = getAppDeps().getDep('config') 113 | this.metrics = getAppDeps().getDep('meter'); 114 | this.idGen = getAppDeps().getDep('ids'); 115 | this.dispatcher = dispatcher; 116 | this.static = new StaticData() 117 | this.options = config.http; 118 | this.title = config.get('name'); 119 | this.identopts = config.identify; 120 | this.uidCookie = this.identopts.param; 121 | this.clientopts = config.client.common; 122 | this.urlMark = config.http.url_mark; 123 | this.log = getAppDeps().getDep('log').for(this); 124 | 125 | // Preparing services params 126 | this.servicesParams = this.options.services_params || []; 127 | 128 | for (let [k, v] of Object.entries(this.options.sevices_map)) { 129 | this.servicesParams[k] = { 130 | alias_for: v 131 | } 132 | } 133 | 134 | this.cookieExpires = new Date(new Date().getTime() + this.identopts.cookieMaxAge * 1000); 135 | this.cookieDomain = this.identopts.cookieDomain === 'auto' 136 | ? (this.identopts.domain ? '.' + autoDomain(this.identopts.domain) : undefined) 137 | : this.identopts.cookieDomain 138 | 139 | } 140 | 141 | /** 142 | * Start listening 143 | */ 144 | start() { 145 | const { host, port } = this.options; 146 | this.log.info('Starting HTTP transport %s:%s', host, port); 147 | this.log.info({ finalCookieDomain: this.cookieDomain, ...this.identopts }, 'Indentify options'); 148 | const httpServerOptions: ServerOptions = { 149 | connectionsCheckingInterval: 15000, 150 | keepAlive: true, 151 | keepAliveTimeout: 5000, 152 | requestTimeout: 5000 153 | }; 154 | 155 | this.httpServer = createServer(httpServerOptions, (req, res) => { 156 | const requestTime = this.metrics.timenote('http.request'); 157 | this.metrics.tick('http.request'); 158 | this.handle(req) 159 | .then((result: BandResponse) => { 160 | const reqTime = requestTime(); 161 | this.send(res, result, reqTime); 162 | }) 163 | .catch(exc => { 164 | this.log.error(exc, 'exception caused | handle exec at start'); 165 | const reqTime = requestTime(); 166 | this.send(res, response.error({ statusCode: STATUS_INT_ERROR }), reqTime); 167 | }) 168 | }); 169 | this.httpServer.listen(this.options.port, this.options.host); 170 | } 171 | 172 | 173 | /** 174 | * Send a response to the client 175 | * @param res 176 | * @param resp 177 | * @param reqTime 178 | */ 179 | private send(res: ServerResponse, resp: BandResponse, reqTime: number) { 180 | 181 | resp.headers.push([HEADER_RESPONSE_TIME, reqTime]) 182 | let raw: Buffer | string = ''; 183 | let contentType: string = CONTENT_TYPE_JSON; 184 | const { headers, ...rest } = resp; 185 | 186 | 187 | if (resp.native__) { 188 | raw = JSON.stringify(rest); 189 | } 190 | else { 191 | 192 | if (rest.type__ === RESP_DATA) { 193 | // override null values with empty string 194 | if (rest.data === null) { 195 | rest.data = ''; 196 | } 197 | 198 | // Object or Buffer or Array... 199 | else if (typeof rest.data === 'object') { 200 | // buffer -> raw data 201 | if (rest.data instanceof Buffer) { 202 | contentType = rest.contentType || CONTENT_TYPE_OCTET; 203 | raw = rest.data; 204 | } 205 | // Object or Array -> need to serialize 206 | else { 207 | contentType = CONTENT_TYPE_JSON; 208 | raw = JSON.stringify(rest.data); 209 | } 210 | } 211 | // Raw string responses 212 | else { 213 | raw = String(rest.data); 214 | } 215 | } 216 | 217 | if (rest.type__ === RESP_REDIRECT) { 218 | contentType = CONTENT_TYPE_HTML; 219 | headers.push([HEADER_LOCATION, rest.location]); 220 | } 221 | 222 | if (rest.type__ === RESP_PIXEL) { 223 | raw = emptyGif; 224 | contentType = CONTENT_TYPE_GIF; 225 | } 226 | 227 | if (rest.type__ === RESP_ERROR) { 228 | raw = JSON.stringify({ message: rest.errorMessage }); 229 | contentType = CONTENT_TYPE_JSON; 230 | } 231 | } 232 | 233 | headers.push([HEADER_CONTENT_TYPE, contentType]) 234 | headers.push([HEADER_CONTENT_LENGTH, Buffer.isBuffer(raw) ? raw.byteLength : Buffer.byteLength(raw)]) 235 | 236 | for (const [h, v] of resp.headers) { 237 | res.setHeader(h, v); 238 | } 239 | res.statusCode = resp.statusCode; 240 | res.end(raw); 241 | } 242 | 243 | 244 | /** 245 | * Main request handler 246 | * @param req 247 | * @param res 248 | */ 249 | private async handle(req: IncomingMessage): Promise { 250 | 251 | if (!req.url || !req.method) { 252 | console.error(Error('Request url/method not present')) 253 | return response.error({ statusCode: STATUS_BAD_REQUEST }) 254 | } 255 | 256 | if (!req.connection.remoteAddress) { 257 | console.error(Error('Connection remote addr not present')) 258 | return response.error({ statusCode: STATUS_INT_ERROR }) 259 | } 260 | 261 | // parsing url 262 | const urlParts = urlParse(req.url); 263 | let query: Query = urlParts.query ? qs.parse(urlParts.query) : {}; 264 | const urlPath = urlParts.pathname || '' 265 | const { native, ...parsedPath } = pathParts(urlPath, this.urlMark); 266 | const [urlService, urlName, urlProjectId] = parsedPath.parts; 267 | 268 | 269 | if (re_naming.exec(urlService) === null || re_naming.exec(urlName) === null) { 270 | this.metrics.tick('http.handle_illegal_names'); 271 | return response.error({ statusCode: STATUS_BAD_REQUEST }); 272 | } 273 | 274 | // parse cookie 275 | const cookie: Cookie = Cookie.parse(f(req.headers.cookie) || ''); 276 | // pancake 277 | const pancake: { [k: string]: any } = {}; 278 | 279 | 280 | // Prerouting 281 | const urlServiceParams = this.servicesParams[urlService || 'no_fcuking_way']; 282 | 283 | // extracting useful headers 284 | const { 285 | 'content-type': ContentTypeHeader, 286 | 'content-encoding': ContentEncoding, 287 | 'origin': originHeader, 288 | 'referer': refererHeader 289 | } = req.headers; 290 | let dig = undefined; 291 | 292 | // custom url settings 293 | 294 | if (urlServiceParams) { 295 | if (urlServiceParams.dig) { 296 | 297 | if (!query.dig) { 298 | this.metrics.tick('http.request_no_dig_required') 299 | return response.error({ statusCode: STATUS_BAD_REQUEST }) 300 | } 301 | 302 | if (urlServiceParams.url_check_websdk) { 303 | let draft_query_dig = Math.floor(Number(query.dig)); 304 | if (!(draft_query_dig !== Infinity && String(draft_query_dig) === query.dig && draft_query_dig >= 0)) { 305 | this.metrics.tick('http.request_hueviy_dig') 306 | return response.error({ statusCode: STATUS_BAD_REQUEST }) 307 | } 308 | } 309 | 310 | dig = Number(query.dig) 311 | 312 | } 313 | } 314 | 315 | // Content-type based on file extension. Used for correct POST request parsing 316 | const contentType = parsedPath.ext && extContentTypeMap[parsedPath.ext] 317 | || ContentTypeHeader 318 | || ''; 319 | 320 | // Preparing post data 321 | let body: HTTPBodyParams | undefined = {}; 322 | if (req.method === METHOD_POST) { 323 | body = await this.parseBody(req, contentType, ContentEncoding, dig); 324 | if (!body) { 325 | this.metrics.tick('http.request_no_body') 326 | return response.error({ statusCode: STATUS_BAD_REQUEST }) 327 | } 328 | } 329 | 330 | // Prerouting 331 | const service = query.service || body.service || (urlServiceParams && urlServiceParams.alias_for) || urlService; 332 | const name = urlName || query.name || body.name; 333 | const uidParam = urlServiceParams && urlServiceParams.uid_param || this.uidParam; 334 | const projectId = Number(urlProjectId || query.projectId || body.projectId || 0); 335 | 336 | // pancakes 337 | if (urlServiceParams) { 338 | if (urlServiceParams.collect_cookies) { 339 | for (const k of urlServiceParams.collect_cookies) { 340 | if (cookie[k]) { 341 | pancake[k] = cookie[k]; 342 | } 343 | } 344 | } 345 | 346 | if (urlServiceParams.action_params && urlServiceParams.action_params[name]) { 347 | const nameParams = urlServiceParams.action_params[name]; 348 | // console.log(nameParams); 349 | if (nameParams.collect_all_cookies) { 350 | for (const [k, v] of Object.entries(cookie)) { 351 | if (nameParams.remove_cookies[k]) { 352 | continue; 353 | } 354 | if (nameParams.jwt_decode && nameParams.jwt_decode[k]) { 355 | try { 356 | const token = jwt.decode(String(v)); 357 | pancake[k] = token; 358 | 359 | } catch (e) { 360 | this.log.error(e, 'jwt decode error'); 361 | } 362 | } else { 363 | pancake[k] = v; 364 | } 365 | } 366 | } 367 | } 368 | } 369 | 370 | 371 | // uid 372 | const uid = ( 373 | cleanUid(query[uidParam]) || 374 | cleanUid(body && body[uidParam]) || 375 | cleanUid(cookie[uidParam]) || 376 | this.idGen.flake() 377 | ) 378 | 379 | const transportData = extractTransportData(req); 380 | 381 | // Data for routing request 382 | const routeOn: RouteOn = { 383 | method: req.method, 384 | contentType, 385 | query, 386 | cookie, 387 | pancake, 388 | body, 389 | uid, 390 | uidParam, 391 | path: urlPath, 392 | ext: parsedPath.ext, 393 | service, 394 | name, 395 | projectId, 396 | origin: computeOrigin(originHeader, refererHeader), 397 | td: transportData 398 | }; 399 | 400 | // Routing request (choose handler and handle) 401 | 402 | const routed = await this.route(routeOn) 403 | routed.native__ = native; 404 | 405 | routed.headers.push( 406 | ...secureHeaders(), 407 | ...corsHeaders(routeOn.origin), 408 | ...noCacheHeaders(), 409 | ...cookieHeaders([this.prepareUidCookie(routeOn)]) 410 | ) 411 | return routed; 412 | 413 | } 414 | 415 | private async route(routeOn: RouteOn): Promise { 416 | 417 | // ### CORS preflight // Early Response 418 | if (routeOn.method === METHOD_OPTIONS) { 419 | return response.data({ 420 | headers: [ 421 | ...corsHeaders(routeOn.origin), 422 | ...corsAnswerHeaders(), 423 | ...corsAdditionalHeaders(), 424 | ], 425 | data: null 426 | }) 427 | } 428 | 429 | // ### Allow only GET and POST 430 | if (routeOn.method !== METHOD_GET && routeOn.method !== METHOD_POST) { 431 | return response.error({ statusCode: STATUS_BAD_REQUEST }); 432 | } 433 | 434 | // ### Coffe test 435 | if (routeOn.path === '/coffee') { 436 | return response.error({ 437 | statusCode: STATUS_TEAPOT, 438 | headers: [ 439 | [HEADER_MY_NAME, this.title], 440 | ] 441 | }); 442 | } 443 | 444 | // ### Allow only GET and POST 445 | if (routeOn.path === '/lib.js') { 446 | return response.data({ 447 | data: this.static.prepareLib({ initialUid: routeOn.uid, urlMark: this.urlMark, ...this.clientopts }), 448 | contentType: CONTENT_TYPE_JS 449 | }); 450 | } 451 | 452 | // ### Index 453 | if (routeOn.path === '/') { 454 | return response.data({ 455 | data: this.static.getItem('index'), 456 | contentType: CONTENT_TYPE_HTML 457 | }); 458 | } 459 | 460 | if (routeOn.path === '/favicon.ico') { 461 | return response.data({ 462 | data: this.static.getItem('favicon'), 463 | contentType: CONTENT_TYPE_ICON 464 | }); 465 | } 466 | 467 | // ### Send request to BUS 468 | if (routeOn.service && routeOn.name) { 469 | const key = epglue(IN_GENERIC, routeOn.service, routeOn.name); 470 | let additional_data = {}; 471 | 472 | const msg: BaseIncomingMessage = { 473 | key, 474 | channel: routeOn.contentType.includes('image') ? CHANNEL_HTTP_PIXEL : CHANNEL_HTTP, 475 | service: routeOn.service, 476 | name: routeOn.name, 477 | ext: routeOn.ext, 478 | projectId: routeOn.projectId, 479 | uid_param: routeOn.uidParam, 480 | uid: routeOn.uid, 481 | td: routeOn.td, 482 | data: { ...routeOn.body, ...routeOn.query, ...additional_data }, 483 | pancake: routeOn.pancake 484 | } 485 | return await this.dispatcher.dispatch(key, msg); 486 | } 487 | 488 | // ### 404 489 | this.log.debug(routeOn, '404 request'); 490 | return response.error({ statusCode: STATUS_NOT_FOUND }) 491 | 492 | } 493 | 494 | /** 495 | * prepare UID cookie 496 | * @param uid 497 | */ 498 | private prepareUidCookie(ro: RouteOn) { 499 | return Cookie.serialize( 500 | ro.uidParam, 501 | ro.uid || '0', 502 | { 503 | httpOnly: true, 504 | secure: true, 505 | expires: this.cookieExpires, 506 | path: this.identopts.cookiePath, 507 | domain: ro.td.host || this.cookieDomain, 508 | sameSite: 'none' 509 | } 510 | ) 511 | } 512 | 513 | 514 | /** 515 | * Helper for parse body when not GET request 516 | * @param routeOn 517 | * @param req 518 | */ 519 | private async parseBody(req: IncomingMessage, contentType?: string, contentEncoding?: string, dig?: number): Promise { 520 | 521 | let result: HTTPBodyParams = {}; 522 | let stream; 523 | let data; 524 | 525 | if (contentEncoding === 'gzip') { 526 | stream = req.pipe(zlib.createGunzip()); 527 | } else { 528 | stream = req; 529 | } 530 | 531 | try { 532 | // length: !stream && req.headers['content-length'], 533 | // https://github.com/expressjs/body-parser/blob/master/index.js#L80 534 | // https://github.com/expressjs/body-parser/tree/master 535 | 536 | data = await getRawBody(stream, { limit: '1mb', encoding: 'utf-8' }) 537 | } catch (e) { 538 | console.error(e) 539 | } 540 | 541 | if (!data) { 542 | this.log.warn('!data'); 543 | return; 544 | } 545 | 546 | if (dig) { 547 | const dig2 = cyrb53(data); 548 | if (dig !== dig2) { 549 | this.log.warn('!data'); 550 | this.log.info({ dig, dig2 }, 'DIGS not eq'); 551 | return; 552 | } 553 | } 554 | 555 | try { 556 | if (!contentType || !contentType.includes('json')) { 557 | result = parseQs(data); 558 | } else { 559 | result = JSON.parse(data); 560 | if (Array.isArray(result)) { 561 | result = { 562 | data: result 563 | } 564 | } 565 | } 566 | // console.log(result) 567 | } catch (e) { 568 | this.log.error('parse err', { e, data }); 569 | return result; 570 | } 571 | 572 | return result; 573 | } 574 | 575 | 576 | } 577 | 578 | -------------------------------------------------------------------------------- /src/http/index.ts: -------------------------------------------------------------------------------- 1 | export * from './http_server'; 2 | export * from './ws_server'; 3 | -------------------------------------------------------------------------------- /src/http/ws_server.ts: -------------------------------------------------------------------------------- 1 | import { createServer as createHTTPSServer, Server as HTTPSServer } from 'https'; 2 | import { IncomingMessage } from 'http'; 3 | // import { Container } from 'typedi'; 4 | import * as WebSocket from 'ws'; 5 | import { Logger, AppConfig, response, BandResponse, STATUS_BAD_REQUEST, RESP_DATA, Meter, getAppDeps } from "@rockstat/rock-me-ts"; 6 | import { Dispatcher } from '@app/Dispatcher'; 7 | import { 8 | WsConfig, 9 | BaseIncomingMessage, 10 | FrontierConfig, 11 | AnyStruct, 12 | WsHTTPParams, 13 | Dictionary, 14 | HTTPTransportData, 15 | } from '@app/types'; 16 | import { 17 | isObject, epglue, extractTransportData 18 | } from '@app/helpers'; 19 | import { parse as urlParse } from 'url'; 20 | import { 21 | OUT_WEBSOCK, 22 | CMD_WEBSOCK_ADD_GROUP, 23 | OUT_WEBSOCK_BROADCAST, 24 | CMD_WEBSOCK, 25 | IN_GENERIC, 26 | CHANNEL_WEBSOCK, 27 | ERROR_NOT_OBJECT, 28 | ERROR_ABSENT_DATA, 29 | } from '@app/constants'; 30 | 31 | interface SockState { 32 | uid: string; 33 | authorized: boolean; 34 | touch: number; 35 | groups: Set; 36 | td: HTTPTransportData; 37 | } 38 | 39 | interface SockLookup { 40 | socket: WebSocket; 41 | state: SockState 42 | } 43 | 44 | interface AddToGroupPayload { 45 | uid: string; 46 | group: string; 47 | } 48 | 49 | export class WebSocketServer { 50 | server: HTTPSServer; 51 | wss: WebSocket.Server; 52 | options: WsConfig; 53 | metrics: Meter; 54 | secureOptions: { cert: Buffer, key: Buffer }; 55 | socksState: WeakMap = new WeakMap(); 56 | log: Logger; 57 | dispatcher: Dispatcher; 58 | 59 | get httpOptions(): WsHTTPParams { 60 | return this.options.http; 61 | } 62 | 63 | constructor() { 64 | // this.options = Container.get>(AppConfig).ws; 65 | this.options = getAppDeps().getDep('config').ws; 66 | // this.dispatcher = Container.get(Dispatcher); 67 | this.dispatcher = getAppDeps().getDep('dispatcher'); 68 | // this.log = Container.get(Logger).for(this); 69 | this.log = getAppDeps().getDep('log').for(this); 70 | // this.metrics = Container.get(Meter); 71 | this.metrics = getAppDeps().getDep('meter'); 72 | } 73 | 74 | /** 75 | * Parse JSON and check is an object 76 | * @param raw raw data buffer or similar 77 | */ 78 | private parse(raw: WebSocket.Data): [Error, undefined] | [undefined, AnyStruct] { 79 | try { 80 | const data = JSON.parse(raw.toString()); 81 | if (!isObject(data)) { 82 | throw new Error(ERROR_NOT_OBJECT) 83 | } 84 | return [undefined, data] 85 | } catch (error) { 86 | return [error, undefined] 87 | } 88 | } 89 | 90 | /** 91 | * Encode message before send 92 | * @param msg message struct 93 | */ 94 | private encode(msg: any): string { 95 | return JSON.stringify(msg) 96 | } 97 | 98 | 99 | start() { 100 | const { host, port } = this.httpOptions; 101 | this.log.info(`Starting WS server on port ${host}:${port}`); 102 | const { perMessageDeflate, path } = this.options; 103 | const wssOptions = { host, port, path, perMessageDeflate }; 104 | this.wss = new WebSocket.Server(wssOptions); 105 | this.setup(); 106 | this.register(); 107 | } 108 | 109 | /** 110 | * Setup Websocket common message handling 111 | */ 112 | private setup() { 113 | this.wss.on('connection', (socket: WebSocket, req: IncomingMessage) => { 114 | this.log.debug('client connected'); 115 | this.metrics.tick('ws.connect'); 116 | if (req.url) { 117 | const parsedUrl = urlParse(req.url, true); 118 | const { uid } = parsedUrl.query; 119 | // accept connections only users with id 120 | if (uid && typeof uid === 'string' && uid.length) { 121 | this.socksState.set(socket, { 122 | uid: uid, 123 | authorized: false, 124 | touch: new Date().getTime(), 125 | groups: new Set(), 126 | td: extractTransportData(req) 127 | }); 128 | socket.on('close', (code: number, reason: string) => { 129 | this.log.debug(`closed ${code} ${reason}`); 130 | this.metrics.tick('ws.close') 131 | }) 132 | socket.on('message', (raw) => { 133 | const requestTime = this.metrics.timenote('ws.message') 134 | this.metrics.tick('ws.message') 135 | const state = this.socksState.get(socket); 136 | if (state) { 137 | state.touch = new Date().getTime(); 138 | this.handle(state, raw).then(resp => { 139 | if (socket.readyState === WebSocket.OPEN) { 140 | socket.send(this.encode(resp)); 141 | requestTime(); 142 | } 143 | }).catch(error => { 144 | this.log.error(error); 145 | }); 146 | } 147 | }); 148 | return; 149 | } 150 | } 151 | this.log.info('Connection without url or credentials'); 152 | socket.close(); 153 | }); 154 | } 155 | 156 | /** 157 | * Transform incoming data to Message struct 158 | */ 159 | private async handle(state: SockState, raw: WebSocket.Data): Promise { 160 | let [error, data] = this.parse(raw); 161 | const reqId = data && data.id__; 162 | const resp = await this.doHandle(state, data, error); 163 | resp.id__ = reqId; 164 | return resp; 165 | } 166 | 167 | private async doHandle(state: SockState, data?: Dictionary, error?: Error): Promise { 168 | if (error) { 169 | this.log.error(error); 170 | return response.error({ statusCode: STATUS_BAD_REQUEST }); 171 | } 172 | else if (!data) { 173 | this.log.error(ERROR_ABSENT_DATA); 174 | return response.error({ statusCode: STATUS_BAD_REQUEST, errorMessage: ERROR_ABSENT_DATA }); 175 | } 176 | else if (!data.service || !data.name) { 177 | return response.error({ statusCode: STATUS_BAD_REQUEST, errorMessage: 'Request must contains "service" and "name' }) 178 | } 179 | 180 | this.log.debug(`msg '${data.name}' received`); 181 | const msg: BaseIncomingMessage = { 182 | key: epglue(IN_GENERIC, data.service, data.name), 183 | name: String(data.name), 184 | service: String(data.service), 185 | channel: CHANNEL_WEBSOCK, 186 | data: data, 187 | uid: state.uid 188 | } 189 | return await this.dispatcher.dispatch(msg.key, msg); 190 | 191 | } 192 | 193 | /** 194 | * Register in Dispatcher as listener. 195 | */ 196 | register() { 197 | this.dispatcher.registerListener(OUT_WEBSOCK, async (key: string, data: any) => { 198 | switch (key) { 199 | case OUT_WEBSOCK_BROADCAST: return await this.sendBroadcast(data); 200 | } 201 | }); 202 | this.dispatcher.registerListener(CMD_WEBSOCK, async (key: string, msg: any) => { 203 | switch (key) { 204 | case CMD_WEBSOCK_ADD_GROUP: return await this.addToGroup(msg.data); 205 | } 206 | }); 207 | } 208 | 209 | /** 210 | * Find websocket assoociated with uid 211 | * @param uid user id 212 | */ 213 | async findUidSock(uid: string): Promise { 214 | for (const socket of this.wss.clients) { 215 | const state = this.socksState.get(socket); 216 | if (state && state.uid === uid) { 217 | return { socket, state }; 218 | } 219 | } 220 | } 221 | 222 | /** 223 | * Add user by uid to the group 224 | * @param param0 user and group 225 | */ 226 | async addToGroup({ uid, group }: AddToGroupPayload): Promise { 227 | this.log.info(`addtogroup user ${uid} to ${group}`); 228 | const result = await this.findUidSock(uid); 229 | if (result) { 230 | const { socket, state } = result; 231 | state.groups.add(group); 232 | } 233 | } 234 | 235 | /** 236 | * Sending broascast message to the group of users 237 | * @param param0 message and meta data 238 | */ 239 | async sendBroadcast({ name, data, group }: { group?: string, data: object, name: string }, ): Promise { 240 | const raw = JSON.stringify({ name, data }); 241 | for (const socket of this.wss.clients) { 242 | const state = this.socksState.get(socket); 243 | if (socket.readyState === WebSocket.OPEN && state && (!group || group && state.groups.has(group))) { 244 | socket.send(raw); 245 | } 246 | } 247 | } 248 | 249 | 250 | } 251 | -------------------------------------------------------------------------------- /src/start.ts: -------------------------------------------------------------------------------- 1 | import 'module-alias/register'; 2 | import { appServer } from '@app/AppServer' 3 | appServer.start(); 4 | export default appServer; 5 | -------------------------------------------------------------------------------- /src/stores/StubStore.ts: -------------------------------------------------------------------------------- 1 | // import { Service } from "typedi"; 2 | import { AppServer } from "@app/AppServer"; 3 | import { Logger } from '@rockstat/rock-me-ts'; 4 | 5 | // @Service() 6 | export class StubStore { 7 | 8 | log: Logger; 9 | 10 | constructor() { 11 | 12 | } 13 | 14 | push(data: any) { 15 | // this.log.debug(data, 'Received'); 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/transformers/common.ts: -------------------------------------------------------------------------------- 1 | 2 | export const flatten_dict = function(d:{ [k: string]: { [k: string]: string } | number | string | Array }, sep='_', prefix=''){ 3 | const res: { [k: string]: any} = {}; 4 | for (let [k, v] of Object.entries(d)){ 5 | k = k.replace('$', 's_'); 6 | if(typeof v === "object" && !Array.isArray(v)){ 7 | let nv = flatten_dict(v, sep, prefix + k + sep) 8 | for (let [k2, v2] of Object.entries(nv)){ 9 | res[k2] = v2; 10 | } 11 | } else { 12 | res[prefix + k] = v; 13 | } 14 | } 15 | return res; 16 | } -------------------------------------------------------------------------------- /src/transformers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mp_android'; 2 | export * from './mp_ios'; 3 | export * from './mp_ios_native'; 4 | export * from './ph_native'; -------------------------------------------------------------------------------- /src/transformers/mp_android.ts: -------------------------------------------------------------------------------- 1 | import { IN_GENERIC } from "@app/constants"; 2 | import { BaseIncomingMessage, IncomingMessage, BaseIncomingMessageWithBatch, BusBaseEnricher } from "@app/types"; 3 | import { flatten_dict } from './common' 4 | 5 | 6 | const new_name = 'events' 7 | const new_service = 'events_mp_android' 8 | const time2040 = 2536696935; 9 | type TypeBatchMsgData = {[s:string]: any}; 10 | 11 | export class MPAndroidTransformer implements BusBaseEnricher { 12 | 13 | handle = async (key: string, msg: IncomingMessage): Promise => { 14 | const batchMsgData:TypeBatchMsgData = {} 15 | const batch_msg = { 16 | id: msg.id, 17 | time: msg.time, 18 | key: `${IN_GENERIC}.batch.batch`, 19 | service: 'batch', 20 | name: 'batch', 21 | source_service: msg.service, 22 | source_name: msg.name, 23 | channel: msg.channel, 24 | projectId: msg.projectId, 25 | uid: msg.uid, 26 | td: msg.td, 27 | data: batchMsgData 28 | }; 29 | 30 | // console.log('batch_msg', batch_msg) 31 | 32 | const submsg_base = { 33 | name: new_name, 34 | service: new_service, 35 | key: `${IN_GENERIC}.${new_service}.${new_name}`, 36 | channel: msg.channel, 37 | td: msg.td, 38 | projectId: msg.projectId, 39 | uid: msg.uid, 40 | }; 41 | 42 | const new_recs: Array = []; 43 | 44 | if (msg.data && 'data' in msg.data) { 45 | let bufStr = '' 46 | try { 47 | let buf = Buffer.from(msg.data.data, 'base64'); 48 | bufStr = buf.toString() 49 | let recs = JSON.parse(bufStr); 50 | if (Array.isArray(recs)) { 51 | 52 | let i = 0; 53 | for (let rec of recs) { 54 | const data = flatten_dict(rec); 55 | data['src'] = msg.projectId; 56 | data['batch_timestamp'] = msg.time; 57 | data['batch_source_id'] = msg.id; 58 | data['batch_event_number'] = i; 59 | data['source_service'] = msg.service; 60 | data['source_name'] = msg.name; 61 | 62 | 63 | let new_client_time = msg.time; 64 | if ('properties_time' in data) { 65 | new_client_time = Number(data['properties_time']); 66 | } 67 | if (new_client_time < time2040) { 68 | new_client_time = new_client_time * 1000; 69 | } 70 | 71 | const msg_ext = { 72 | time: new_client_time, 73 | } 74 | 75 | const new_rec = { ...submsg_base, ...msg_ext, data } 76 | new_recs.push(new_rec); 77 | i++; 78 | } 79 | } else { 80 | console.error('Batch is not a batch') 81 | } 82 | } catch (e) { 83 | // console.error('Err during parsing batch', {bufStr, e}) 84 | batch_msg.data.err = String(e); 85 | return [batch_msg, []]; 86 | } 87 | } 88 | 89 | return [batch_msg, new_recs]; 90 | } 91 | } 92 | 93 | 94 | 95 | // export const RedirectHandler = () => { 96 | // return async (key: string, msg: BaseIncomingMessage): Promise => { 97 | // if (msg.data.to) { 98 | // return response.redirect({ location: msg.data.to }) 99 | // } else { 100 | // return response.error({ errorMessage: 'Parameter "to" is required', statusCode: STATUS_BAD_REQUEST }) 101 | // } 102 | // } 103 | 104 | // } 105 | -------------------------------------------------------------------------------- /src/transformers/mp_ios.ts: -------------------------------------------------------------------------------- 1 | import { IN_GENERIC } from "@app/constants"; 2 | import { BaseIncomingMessage, IncomingMessage, BaseIncomingMessageWithBatch, BusBaseEnricher } from "@app/types"; 3 | import { flatten_dict } from './common' 4 | 5 | 6 | const new_name = 'events' 7 | const new_service = 'events_mp_ios' 8 | const time2040 = 2536696935; 9 | 10 | export class MPIOSTransformer implements BusBaseEnricher { 11 | 12 | handle = async (key: string, msg: IncomingMessage): Promise => { 13 | 14 | type TypeBatchMsgData = {[s:string]: any}; 15 | const batchMsgData:TypeBatchMsgData = {} 16 | const batch_msg = { 17 | id: msg.id, 18 | time: msg.time, 19 | key: `${IN_GENERIC}.batch.batch`, 20 | service: 'batch', 21 | name: 'batch', 22 | source_service: msg.service, 23 | source_name: msg.name, 24 | channel: msg.channel, 25 | projectId: msg.projectId, 26 | uid: msg.uid, 27 | td: msg.td, 28 | data: batchMsgData 29 | }; 30 | 31 | // console.log('ios batch msg', batch_msg) 32 | 33 | const submsg_base = { 34 | name: new_name, 35 | service: new_service, 36 | key: `${IN_GENERIC}.${new_service}.${new_name}`, 37 | channel: msg.channel, 38 | td: msg.td, 39 | projectId: msg.projectId, 40 | uid: msg.uid, 41 | }; 42 | 43 | const new_recs: Array = []; 44 | let bufStr = ''; 45 | if (msg.data && 'data' in msg.data) { 46 | try { 47 | let buf = Buffer.from(msg.data.data, 'base64'); 48 | bufStr = buf.toString() 49 | let recs = JSON.parse(bufStr); 50 | if (Array.isArray(recs)) { 51 | 52 | let i = 0; 53 | for (let rec of recs) { 54 | const data = flatten_dict(rec); 55 | data['internal_id'] = data['id']; 56 | data['id'] = undefined; 57 | data['src'] = msg.projectId; 58 | data['batch_timestamp'] = msg.time; 59 | data['batch_source_id'] = msg.id; 60 | data['batch_event_number'] = i; 61 | data['source_service'] = msg.service; 62 | data['source_name'] = msg.name; 63 | 64 | let new_client_time = msg.time; 65 | if ('properties_time' in data) { 66 | new_client_time = Number(data['properties_time']); 67 | } 68 | if (new_client_time < time2040) { 69 | new_client_time = new_client_time * 1000; 70 | } 71 | const msg_ext = { 72 | time: new_client_time, 73 | } 74 | const new_rec = { ...submsg_base, ...msg_ext, data } 75 | new_recs.push(new_rec); 76 | i++; 77 | } 78 | } else { 79 | console.error('Batch is not a batch') 80 | } 81 | } catch (e) { 82 | 83 | // console.log('----') 84 | // console.error('Err during parsing batch', {bufStr, e}) 85 | console.error('Err during parsing batch') 86 | // console.log('msg.data', msg.data); 87 | // console.log('msg.data', msg); 88 | // console.log('----') 89 | 90 | batch_msg.data.err = String(e); 91 | return [batch_msg, []]; 92 | } 93 | } 94 | 95 | return [batch_msg, new_recs]; 96 | } 97 | } 98 | 99 | 100 | 101 | // export const RedirectHandler = () => { 102 | // return async (key: string, msg: BaseIncomingMessage): Promise => { 103 | // if (msg.data.to) { 104 | // return response.redirect({ location: msg.data.to }) 105 | // } else { 106 | // return response.error({ errorMessage: 'Parameter "to" is required', statusCode: STATUS_BAD_REQUEST }) 107 | // } 108 | // } 109 | 110 | // } 111 | -------------------------------------------------------------------------------- /src/transformers/mp_ios_native.ts: -------------------------------------------------------------------------------- 1 | import { IN_GENERIC } from "@app/constants"; 2 | import { BaseIncomingMessage, IncomingMessage, BaseIncomingMessageWithBatch, BusBaseEnricher } from "@app/types"; 3 | import { flatten_dict } from './common' 4 | 5 | 6 | const new_name = 'events' 7 | const new_service = 'events_mp_ios' 8 | const time2040 = 2536696935; 9 | 10 | export class MPIOSNativeTransformer implements BusBaseEnricher { 11 | 12 | handle = async (key: string, msg: IncomingMessage): Promise => { 13 | 14 | type TypeBatchMsgData = { [s: string]: any }; 15 | const batchMsgData: TypeBatchMsgData = {} 16 | const batch_msg = { 17 | id: msg.id, 18 | time: msg.time, 19 | key: `${IN_GENERIC}.batch.batch`, 20 | service: 'batch', 21 | name: 'batch', 22 | source_service: msg.service, 23 | source_name: msg.name, 24 | channel: msg.channel, 25 | projectId: msg.projectId, 26 | uid: msg.uid, 27 | td: msg.td, 28 | data: batchMsgData 29 | }; 30 | 31 | // console.log('ios batch msg', batch_msg) 32 | 33 | const submsg_base = { 34 | name: new_name, 35 | service: new_service, 36 | key: `${IN_GENERIC}.${new_service}.${new_name}`, 37 | channel: msg.channel, 38 | td: msg.td, 39 | projectId: msg.projectId, 40 | uid: msg.uid, 41 | }; 42 | 43 | const new_recs: Array = []; 44 | 45 | 46 | if (msg.data && msg.data.data && Array.isArray(msg.data.data)) { 47 | try { 48 | // let buf = Buffer.from(msg.data.data, 'base64'); 49 | // bufStr = buf.toString() 50 | // let recs = JSON.parse(bufStr); 51 | // if (Array.isArray(recs)) { 52 | 53 | let i = 0; 54 | 55 | for (let [num, rec] of Object.entries(msg.data.data)) { 56 | 57 | if (!rec.event) { 58 | console.error('MPIOSNativeTransformer: no event id'); 59 | console.log(num, rec) 60 | continue; 61 | 62 | } 63 | // console.log(rec); 64 | 65 | const data = flatten_dict(rec); 66 | data['internal_id'] = data['id']; 67 | data['id'] = undefined; 68 | data['src'] = msg.projectId; 69 | data['batch_timestamp'] = msg.time; 70 | data['batch_source_id'] = msg.id; 71 | data['batch_event_number'] = i; 72 | data['source_service'] = msg.service; 73 | data['source_name'] = msg.name; 74 | 75 | let new_client_time = msg.time; 76 | if ('properties_time' in data) { 77 | new_client_time = Number(data['properties_time']); 78 | } 79 | if (new_client_time < time2040) { 80 | new_client_time = new_client_time * 1000; 81 | } 82 | const msg_ext = { 83 | time: new_client_time, 84 | } 85 | const new_rec = { ...submsg_base, ...msg_ext, data } 86 | new_recs.push(new_rec); 87 | i++; 88 | } 89 | // } else { 90 | // console.error('Batch is not a batch') 91 | // } 92 | } catch (e) { 93 | 94 | // console.log('----') 95 | // console.error('Err during parsing batch', {bufStr, e}) 96 | console.error('Err during parsing batch') 97 | // console.log('msg.data', msg.data); 98 | // console.log('msg.data', msg); 99 | // console.log('----') 100 | 101 | batch_msg.data.err = String(e); 102 | return [batch_msg, []]; 103 | } 104 | } 105 | 106 | return [batch_msg, new_recs]; 107 | } 108 | } 109 | 110 | 111 | 112 | // export const RedirectHandler = () => { 113 | // return async (key: string, msg: BaseIncomingMessage): Promise => { 114 | // if (msg.data.to) { 115 | // return response.redirect({ location: msg.data.to }) 116 | // } else { 117 | // return response.error({ errorMessage: 'Parameter "to" is required', statusCode: STATUS_BAD_REQUEST }) 118 | // } 119 | // } 120 | 121 | // } 122 | -------------------------------------------------------------------------------- /src/transformers/ph_native.ts: -------------------------------------------------------------------------------- 1 | import { IN_GENERIC } from "@app/constants"; 2 | import { BaseIncomingMessage, IncomingMessage, BaseIncomingMessageWithBatch, BusBaseEnricher } from "@app/types"; 3 | import { flatten_dict } from './common' 4 | 5 | 6 | const new_name = 'events' 7 | const new_service = 'events_ph' 8 | 9 | export class PHNativeTransformer implements BusBaseEnricher { 10 | 11 | handle = async (key: string, msg: IncomingMessage): Promise => { 12 | 13 | type TypeBatchMsgData = { [s: string]: any }; 14 | const batchMsgData: TypeBatchMsgData = {} 15 | const batch_msg = { 16 | id: msg.id, 17 | time: msg.time, 18 | key: `${IN_GENERIC}.batch.batch`, 19 | service: 'batch', 20 | name: 'batch', 21 | source_service: msg.service, 22 | source_name: msg.name, 23 | channel: msg.channel, 24 | projectId: msg.projectId, 25 | uid: msg.uid, 26 | td: msg.td, 27 | data: batchMsgData 28 | }; 29 | 30 | // api_key: '', 31 | // sent_at: '2025-04-22T17:37:55.786Z' 32 | 33 | // console.log('ios batch msg', batch_msg) 34 | 35 | const submsg_base = { 36 | name: new_name, 37 | service: new_service, 38 | key: `${IN_GENERIC}.${new_service}.${new_name}`, 39 | channel: msg.channel, 40 | td: msg.td, 41 | projectId: msg.projectId, 42 | uid: msg.uid, 43 | }; 44 | 45 | const new_recs: Array = []; 46 | 47 | 48 | if (msg.data && msg.data.batch && Array.isArray(msg.data.batch)) { 49 | 50 | const send_at = Number(new Date(msg.data.sent_at)) 51 | const api_key = msg.data.api_key; 52 | 53 | batch_msg.data.api_key = api_key; 54 | batch_msg.data.sent_at = send_at; 55 | 56 | try { 57 | 58 | let i = 0; 59 | 60 | for (let [num, rec] of Object.entries(msg.data.batch)) { 61 | 62 | // console.log(rec) 63 | 64 | if (!rec.event) { 65 | console.error('MPIOSNativeTransformer: no event id'); 66 | console.log(num, rec) 67 | continue; 68 | } 69 | // console.log(rec); 70 | 71 | 72 | 73 | const data = flatten_dict(rec); 74 | 75 | const event_ts = data['timestamp']; 76 | data['timestamp'] = undefined; 77 | data['origin_timestamp'] = event_ts; 78 | 79 | data['batch_timestamp'] = msg.time; 80 | data['batch_source_id'] = msg.id; 81 | data['batch_event_number'] = i; 82 | data['source_service'] = msg.service; 83 | data['source_name'] = msg.name; 84 | data['sent_at'] = send_at; 85 | data['api_key'] = api_key; 86 | 87 | 88 | const new_rec = { ...submsg_base, data, time: Number(new Date(event_ts)) } 89 | new_recs.push(new_rec); 90 | i++; 91 | } 92 | // } else { 93 | // console.error('Batch is not a batch') 94 | // } 95 | } catch (e) { 96 | 97 | // console.log('----') 98 | // console.error('Err during parsing batch', {bufStr, e}) 99 | console.error('Err during parsing batch') 100 | // console.log('msg.data', msg.data); 101 | // console.log('msg.data', msg); 102 | // console.log('----') 103 | 104 | batch_msg.data.err = String(e); 105 | return [batch_msg, []]; 106 | } 107 | } 108 | // console.log(batch_msg) 109 | // console.log(new_recs) 110 | 111 | return [batch_msg, new_recs]; 112 | } 113 | } 114 | 115 | 116 | 117 | // export const RedirectHandler = () => { 118 | // return async (key: string, msg: BaseIncomingMessage): Promise => { 119 | // if (msg.data.to) { 120 | // return response.redirect({ location: msg.data.to }) 121 | // } else { 122 | // return response.error({ errorMessage: 'Parameter "to" is required', statusCode: STATUS_BAD_REQUEST }) 123 | // } 124 | // } 125 | 126 | // } 127 | -------------------------------------------------------------------------------- /src/transformers/track_batch.ts: -------------------------------------------------------------------------------- 1 | import { IN_GENERIC } from "@app/constants"; 2 | import { BaseIncomingMessage, IncomingMessage, BaseIncomingMessageWithBatch, BusBaseEnricher } from "@app/types"; 3 | import { flatten_dict } from './common' 4 | 5 | 6 | type TypeBatchMsgData = { [s: string]: any }; 7 | 8 | export class TrackBatchTransformer implements BusBaseEnricher { 9 | 10 | handle = async (key: string, msg: IncomingMessage): Promise => { 11 | const batchMsgData: TypeBatchMsgData = {} 12 | const batch_msg = { 13 | id: msg.id, 14 | time: msg.time, 15 | key: `${IN_GENERIC}.batch.batch`, 16 | service: 'batch', 17 | name: 'batch', 18 | source_service: msg.service, 19 | source_name: msg.name, 20 | channel: msg.channel, 21 | projectId: msg.projectId, 22 | uid: msg.uid, 23 | td: msg.td, 24 | data: batchMsgData 25 | }; 26 | 27 | if (!msg['data']) { 28 | return [batch_msg, []]; 29 | } 30 | 31 | const submsg_base = { 32 | service: msg.service, 33 | channel: msg.channel, 34 | td: msg.td, 35 | projectId: msg.projectId, 36 | uid: msg.uid, 37 | // i: msg. 38 | }; 39 | 40 | const new_recs: Array = []; 41 | 42 | if ('data' in msg.data) { 43 | let bufStr = '' 44 | try { 45 | let buf = Buffer.from(msg.data.data, 'base64'); 46 | bufStr = buf.toString() 47 | let recs = JSON.parse(bufStr); 48 | if (Array.isArray(recs)) { 49 | 50 | let i = 0; 51 | for (let rec of recs) { 52 | const data = flatten_dict(rec); 53 | 54 | data['src'] = msg.projectId; 55 | data['batch_timestamp'] = msg.time; 56 | data['batch_source_id'] = msg.id; 57 | data['batch_event_number'] = i; 58 | data['service'] = rec.service; 59 | 60 | const new_rec = { 61 | name: rec.name, 62 | key: `${IN_GENERIC}.${rec.service}.${rec.name}`, 63 | data, ...submsg_base, 64 | } 65 | new_recs.push(new_rec); 66 | i++; 67 | } 68 | } else { 69 | console.error('Batch is not a batch') 70 | } 71 | } catch (e) { 72 | // console.error('Err during parsing batch', {bufStr, e}) 73 | batch_msg.data.err = String(e); 74 | return [batch_msg, []]; 75 | } 76 | } 77 | 78 | return [batch_msg, new_recs]; 79 | } 80 | } 81 | 82 | 83 | 84 | // export const RedirectHandler = () => { 85 | // return async (key: string, msg: BaseIncomingMessage): Promise => { 86 | // if (msg.data.to) { 87 | // return response.redirect({ location: msg.data.to }) 88 | // } else { 89 | // return response.error({ errorMessage: 'Parameter "to" is required', statusCode: STATUS_BAD_REQUEST }) 90 | // } 91 | // } 92 | 93 | // } 94 | -------------------------------------------------------------------------------- /src/types/base.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage, ServerResponse, IncomingHttpHeaders } from "http"; 2 | 3 | export type Dictionary = Partial<{ [key: string]: T }>; 4 | 5 | export type AnyStruct = Partial<{ [key: string]: any | never }>; 6 | -------------------------------------------------------------------------------- /src/types/config.ts: -------------------------------------------------------------------------------- 1 | import { Envs } from '@rockstat/rock-me-ts'; 2 | 3 | export type MappedType = { [K in keyof T]: T[K] }; 4 | 5 | export { Envs }; 6 | 7 | // ##### HTTP ##### 8 | 9 | export interface IdentifyConfig { 10 | param: string; 11 | cookieMaxAge: number; 12 | cookieDomain?: string; 13 | cookiePath?: string; 14 | domain?: string; 15 | } 16 | 17 | export interface HTTPServiceMapParams { 18 | [k: string]: string 19 | } 20 | 21 | 22 | 23 | export interface HTTPActionParams { 24 | collect_all_cookies: boolean; 25 | jwt_decode: { [k:string]: {[k: string]: boolean} | boolean } 26 | remove_cookies: { [k:string]: boolean } 27 | 28 | } 29 | 30 | export interface HTTPServiceParams { 31 | alias_for: string; 32 | uid_param?: string; 33 | collect_cookies?: Array 34 | dig?: boolean 35 | url_check_websdk?: boolean 36 | action_params?: { [k:string]: HTTPActionParams } 37 | } 38 | 39 | export interface HttpConfig { 40 | host: string; 41 | port: number; 42 | url_mark: string; 43 | sevices_map: HTTPServiceMapParams; 44 | services_params: { [k:string]: HTTPServiceParams } 45 | } 46 | 47 | // ##### WEBSOCKET ##### 48 | 49 | 50 | export interface wsDeflateConfig { 51 | zlibDeflateOptions: { 52 | chunkSize: number; 53 | memLevel: number; 54 | level: number; 55 | }; 56 | zlibInflateOptions: { 57 | chunkSize: number; 58 | }; 59 | clientNoContextTakeover: boolean; 60 | serverNoContextTakeover: boolean; 61 | clientMaxWindowBits: number; 62 | serverMaxWindowBits: number; 63 | concurrencyLimit: number; 64 | threshold: number; 65 | } 66 | 67 | export interface WsHTTPParams { 68 | host: string; 69 | port: number; 70 | } 71 | 72 | export interface WsConfig { 73 | path: string; 74 | http: WsHTTPParams; 75 | perMessageDeflate: wsDeflateConfig; 76 | } 77 | 78 | // ##### CLIENT ##### 79 | 80 | export type StaticConfig = { 81 | lib: string; 82 | } 83 | 84 | export type ClientConfig = { 85 | trackClicks: boolean; 86 | trackForms: boolean; 87 | trackActivity: boolean 88 | cookieDomain: string 89 | allowSendBeacon: boolean 90 | allowHTTP: boolean 91 | allowXHR: boolean 92 | activateWs: boolean 93 | wsPort: number 94 | } 95 | 96 | 97 | // ##### MSG BUS ##### 98 | 99 | export interface MsgBusConfig { 100 | enrichers: { [k: string]: Array } 101 | handlers: { [k: string]: string } 102 | transformers: { [k: string]: Array } 103 | } 104 | 105 | 106 | // ##### CONFIG ROOT ##### 107 | 108 | export type FrontierConfig = { 109 | name: string; 110 | version: string; 111 | env: Envs; 112 | http: HttpConfig; 113 | websocket: WsConfig; 114 | identify: IdentifyConfig; 115 | static: StaticConfig; 116 | client: { 117 | common: ClientConfig; 118 | }; 119 | bus: MsgBusConfig; 120 | } 121 | 122 | -------------------------------------------------------------------------------- /src/types/http.ts: -------------------------------------------------------------------------------- 1 | export { HTTPHeaders, HTTPHeader } from '@rockstat/rock-me-ts'; 2 | import { HTTPTransportData } from './msg'; 3 | 4 | export type HTTPBodyParams = { [key: string]: any } 5 | 6 | // === Rounting based on 7 | export interface RouteOn { 8 | method: string; 9 | contentType: string; 10 | query: { [key: string]: any }; 11 | cookie: { [key: string]: any }; 12 | pancake: { [key: string]: any }; 13 | body: { [key: string]: any }; 14 | path: string; 15 | ext?: string; 16 | origin: string; 17 | uid?: string; 18 | uidParam: string; 19 | service: string; 20 | name: string; 21 | projectId?: number; 22 | td: HTTPTransportData 23 | } 24 | 25 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base' 2 | export * from './msg' 3 | export * from './config' 4 | export * from './http' 5 | -------------------------------------------------------------------------------- /src/types/msg.ts: -------------------------------------------------------------------------------- 1 | import { Dictionary } from "./base"; 2 | 3 | // ###### HTTP messages part 4 | 5 | 6 | export interface HTTPTransportData { 7 | ip: string; 8 | ua?: string; 9 | // Fingerprint based on ip address and browser user-agent 10 | fpid?: string; 11 | host?: string; 12 | // http referer 13 | ref?: string; 14 | // page path 15 | path?: string; 16 | } 17 | 18 | // ###### BASE MESSAGING 19 | 20 | export interface MessageIdTime { 21 | id: string; 22 | time: number; 23 | } 24 | 25 | export interface IncomingMsgData { 26 | [key: string]: any 27 | } 28 | 29 | export interface IncomingMessageProps { 30 | // dispatching key 31 | key: string; 32 | // service identifier 33 | service: string; 34 | // service event name identifier 35 | name: string; 36 | // extension 37 | ext?: string; 38 | // channel name 39 | channel: string; 40 | // hz 41 | projectId?: number; 42 | // user identifier 43 | uid?: string; 44 | // uid param 45 | uid_param?: string; 46 | // Transport-specific data 47 | td?: HTTPTransportData; 48 | // message payload 49 | data: IncomingMsgData; 50 | pancake?: IncomingMsgData; 51 | } 52 | 53 | 54 | export type BaseIncomingMessage = IncomingMessageProps & Partial; 55 | export type IncomingMessage = IncomingMessageProps & MessageIdTime; 56 | export type BaseIncomingMessageWithBatch = [BaseIncomingMessage, Array]; 57 | 58 | // ###### BUS 59 | 60 | export type BusMsgHdr = (key: string, msg: BaseIncomingMessage) => Promise; 61 | 62 | export interface BusBaseEnricher { 63 | handle: (key: string, msg: BaseIncomingMessage) => Promise>; 64 | } 65 | 66 | export type BusMsgHdrResult = PromiseLike 67 | export type BusMsgHdrsResult = PromiseLike 68 | 69 | export interface ServiceStatusStructRegisterOptions { 70 | keys: Array 71 | props: { [k: string]: string } 72 | alias?: string 73 | } 74 | 75 | 76 | export interface ServiceStatusStructRegisterItem { 77 | method: string, 78 | role: string, 79 | options: ServiceStatusStructRegisterOptions 80 | } 81 | 82 | export interface ServiceStatusStructData { 83 | name: string, 84 | app_started: number, 85 | app_uptime: number, 86 | app_state: string, 87 | register: Array 88 | } 89 | 90 | export interface ServiceStatusStruct { 91 | type__: 'data', 92 | statusCode: Number, 93 | data: ServiceStatusStructData 94 | } -------------------------------------------------------------------------------- /test/url_safe.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rockstat/front/d3c456850c5f529e80b8b42a1029b78781eb8833/test/url_safe.js -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "strictNullChecks": true, 9 | "declaration": true, 10 | "declarationMap": false, 11 | "sourceMap": true, 12 | "removeComments": true, 13 | "allowUnreachableCode": true, 14 | "noFallthroughCasesInSwitch": false, 15 | "allowSyntheticDefaultImports": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "noImplicitAny": true, 18 | "outDir": "./dist/", 19 | "incremental": true, 20 | "typeRoots": [ 21 | "node_modules/@types", 22 | "@types", 23 | "node_modules/@rockstat/rock-me-ts/src/types", 24 | "node_modules/@rockstat/rock-me-ts/@types" 25 | ], 26 | "baseUrl": "./", 27 | "paths": { 28 | "@app/*": [ "./src/*" ] 29 | } 30 | 31 | }, 32 | "compileOnSave": true 33 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:latest", 3 | "rules": { 4 | "indent": [true, "spaces"], 5 | "quotemark": [true, "single"], 6 | "object-literal-sort-keys": false, 7 | "trailing-comma": false, 8 | "class-name": true, 9 | "semicolon": [true, "always"], 10 | "triple-equals": [true, "allow-null-check"], 11 | "eofline": true, 12 | "jsdoc-format": true, 13 | "member-access": false, 14 | "whitespace": [true, 15 | "check-decl", 16 | "check-operator", 17 | "check-separator", 18 | "check-type" 19 | ], 20 | "no-require-imports": true, 21 | "no-reference": true, 22 | "ordered-imports": false, 23 | "no-trailing-whitespace": false 24 | }, 25 | "rulesDirectory": [] 26 | } 27 | -------------------------------------------------------------------------------- /var/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rockstat/front/d3c456850c5f529e80b8b42a1029b78781eb8833/var/.keep -------------------------------------------------------------------------------- /web-sdk-dist/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rockstat/front/d3c456850c5f529e80b8b42a1029b78781eb8833/web-sdk-dist/.keep --------------------------------------------------------------------------------