├── .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 | 
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
--------------------------------------------------------------------------------