yxorp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.editorconfig ADDED
@@ -0,0 +1,16 @@
1
+ # Editor configuration, see http://editorconfig.org
2
+ root = true
3
+
4
+ [*]
5
+ charset = utf-8
6
+ indent_style = space
7
+ indent_size = 2
8
+ insert_final_newline = true
9
+ trim_trailing_whitespace = true
10
+
11
+ [*.ts]
12
+ quote_type = single
13
+
14
+ [*.md]
15
+ max_line_length = off
16
+ trim_trailing_whitespace = false
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Kirill Suntsov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # Yxorp
2
+ This is a simple CLI reverse proxy utility for rewriting or mocking API data. It is in early development and may contain many bugs 😊
3
+
4
+ ## Installation
5
+ ```
6
+ npm i -g yxopr
7
+ ```
8
+ ## Usage
9
+ Just run yxopr in folder which contains yxopr.json
10
+
11
+ ## Config
12
+ Simple yxopr.json example:
13
+ ```json
14
+ {
15
+ "#": "The site that will be proxied"
16
+ "target": "http://example.com/",
17
+ "port": 3002,
18
+ "#": "Makes static path with files in directory",
19
+ "staticRules": [
20
+ {
21
+ "path": "/some-path-on-server",
22
+ "directory": "./some-path-with-static-files",
23
+ "disable": false
24
+ }
25
+ ],
26
+ "#": "Scripts may be using for set some data for mock scripts",
27
+ "scripts": [
28
+ "./scripts/some-data.js"
29
+ ],
30
+ "#": "For matching paths in mockRules and rewriteRules uses https://www.npmjs.com/package/path-to-regex",
31
+ "#": "This rules uses to modify API data",
32
+ "mockRules": [
33
+ {
34
+ "method": "GET",
35
+ "path": "/api/example-one(.*)",
36
+ "script": "./mock/api/example-one.js",
37
+ "disable": false
38
+ }, {
39
+ "method": "GET",
40
+ "path": "/api/example-two(.*)",
41
+ "file": "./mock/api/example-two.json",
42
+ "statusCode": 200,
43
+ "disable": false
44
+ }
45
+ ]
46
+ "rewriteRules": [
47
+ {
48
+ "method": "GET",
49
+ "path": "/api/example-one(.*)",
50
+ "script": "./rewrite/api/example-one.js",
51
+ "disable": false
52
+ }, {
53
+ "method": "GET",
54
+ "path": "/api/example-two(.*)",
55
+ "file": "./rewrite/api/example-two.json",
56
+ "statusCode": 200,
57
+ "disable": false
58
+ }
59
+ ]
60
+ }
61
+ ```
62
+
63
+ ## Mock script example
64
+ ```javascript
65
+ var data = {
66
+ error: "some error",
67
+ };
68
+
69
+ res.statusCode = 500;
70
+ res.end(Buffer.from(JSON.stringify(data)));
71
+ ```
72
+
73
+ ## Rewrite script example
74
+ ```javascript
75
+ const data = JSON.parse(body.toString());
76
+
77
+ data.modify = 'from js 123';
78
+
79
+ result = Buffer.from(JSON.stringify(data));
80
+
81
+ ```
package/bin/yxorp.js ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+
3
+ var path = require('path');
4
+ var nodemon = require('nodemon');
5
+ var nodemonDefaults = require('nodemon/lib/config/defaults');
6
+
7
+ var pathToTsNode = path.normalize(__dirname + './../node_modules/.bin/ts-node');
8
+ var filepath = path.normalize(__dirname + './../src/index.ts')
9
+
10
+ nodemonDefaults.execMap.ts = pathToTsNode;
11
+
12
+ nodemon({
13
+ script: filepath,
14
+ watch: ['yxopr.json']
15
+ });
16
+
17
+ nodemon.on('quit', function () {
18
+ console.log('Yxopr has quit');
19
+ process.exit();
20
+ }).on('restart', function (files) {
21
+ console.log('Yxopr restarts...');
22
+ });
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "yxorp",
3
+ "version": "0.1.0",
4
+ "description": "Yxorp is a reverse proxy for rewriting or mocking API data.",
5
+ "main": "src/index.ts",
6
+ "bin": {
7
+ "yxorp": "./bin/yxorp.js"
8
+ },
9
+ "scripts": {
10
+ "start": "nodemon src/index.ts"
11
+ },
12
+ "author": "Kirill Suntsov",
13
+ "license": "MIT",
14
+ "dependencies": {
15
+ "http-encoding": "^1.5.1",
16
+ "http-proxy": "^1.18.1",
17
+ "mime": "^3.0.0",
18
+ "nodemon": "^3.0.1",
19
+ "path-to-regexp": "^6.2.1",
20
+ "qs": "^6.11.2",
21
+ "reflect-metadata": "^0.1.13",
22
+ "typedi": "^0.10.0",
23
+ "yargs": "^17.7.2"
24
+ },
25
+ "devDependencies": {
26
+ "@types/http-proxy": "^1.17.11",
27
+ "@types/mime": "^3.0.1",
28
+ "@types/node": "^20.4.9",
29
+ "@types/qs": "^6.9.8",
30
+ "ts-node": "^10.9.1",
31
+ "tslib": "^2.6.1",
32
+ "typescript": "^5.1.6",
33
+ "winston": "^3.10.0"
34
+ },
35
+ "engines": {
36
+ "node": ">= 12"
37
+ }
38
+ }
package/src/index.ts ADDED
@@ -0,0 +1,48 @@
1
+ import 'reflect-metadata';
2
+
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { Container } from 'typedi';
6
+ import { ServerOptions } from 'http-proxy';
7
+ import { YxorpServer } from './services/yxorp-server.service';
8
+ import { ConfigFile } from './types/yxorp-config';
9
+ import { ProxyConfigToken } from './services/config.service';
10
+
11
+
12
+ let config: ConfigFile;
13
+
14
+ try {
15
+ config = JSON.parse(fs.readFileSync('./yxopr.json').toString()) as ConfigFile;
16
+ } catch(e) {
17
+ console.error(e);
18
+ throw 'Can\'t read yxopr.json'
19
+ }
20
+
21
+ globalThis.require = require;
22
+
23
+ const proxyOptions: ServerOptions = {
24
+ target: config.target,
25
+ changeOrigin: true,
26
+ followRedirects: true,
27
+ secure: false,
28
+ localAddress: '0.0.0.0',
29
+ ws: true,
30
+ selfHandleResponse: true,
31
+ };
32
+
33
+ const proxyConfig = {
34
+ ...config,
35
+ proxyOptions,
36
+ }
37
+
38
+ config?.scripts?.forEach(script => {
39
+ require(path.join(process.cwd(), script));
40
+ });
41
+
42
+ Container.set(ProxyConfigToken, proxyConfig);
43
+
44
+ const server = Container.get(YxorpServer);
45
+
46
+ server.listen(config.proxyPort, () => {
47
+ console.log(`Yxorp server started successfully on http://localhost:${config.proxyPort}`);
48
+ });
@@ -0,0 +1,87 @@
1
+ import { IncomingMessage } from 'http';
2
+ import { Service } from 'typedi';
3
+ import qs from 'qs';
4
+ import { Middleware } from '../services/pipeline.service';
5
+ import { RewriteRulesMatcher } from '../services/rules-matchers/rewrite-rules-matcher.service';
6
+ import { MockRulesMatcher } from '../services/rules-matchers/mock-rules-matcher.service';
7
+ import { LoggerService } from '../services/logger.service';
8
+
9
+
10
+ @Service({
11
+ global: true
12
+ })
13
+ export class BootstrapMiddleware<T extends any[]> implements Middleware<T> {
14
+ constructor(
15
+ private rewriteRulesMatcher: RewriteRulesMatcher,
16
+ private mockRulesMatcher: MockRulesMatcher,
17
+ private logger: LoggerService,
18
+ ) {
19
+ }
20
+
21
+ public use(...args: [...T, () => void]): void {
22
+ const req: IncomingMessage = args[0] as IncomingMessage;
23
+ const next: () => void = args[args.length - 1];
24
+
25
+ try {
26
+ this.setRewriteRule(req);
27
+ this.setMockRule(req);
28
+ this.setQueryParams(req);
29
+
30
+ next();
31
+ } catch (e) {
32
+ this.logger.error(e);
33
+ next();
34
+ }
35
+ }
36
+
37
+ private setRewriteRule(req: IncomingMessage): void {
38
+ if (!req.url || !req.method) {
39
+ return;
40
+ }
41
+
42
+ const rewriteRule = this.rewriteRulesMatcher.match(req.url, req.method);
43
+
44
+ if (rewriteRule) {
45
+ req.rewriteRule = rewriteRule;
46
+
47
+ const rewriteRuleParams = this.rewriteRulesMatcher.params(req.url, rewriteRule);
48
+
49
+ if (rewriteRuleParams) {
50
+ req.rewriteRuleParams = rewriteRuleParams || {};
51
+ }
52
+ }
53
+ }
54
+
55
+ private setMockRule(req: IncomingMessage): void {
56
+ if (!req.url || !req.method) {
57
+ return;
58
+ }
59
+
60
+ const mockRule = this.mockRulesMatcher.match(req.url, req.method);
61
+
62
+ if (mockRule) {
63
+ req.mockRule = mockRule;
64
+
65
+ const mockRuleParams = this.mockRulesMatcher.params(req.url, mockRule);
66
+
67
+ if (mockRuleParams) {
68
+ req.mockRuleParams = mockRuleParams || {};
69
+ }
70
+ }
71
+ }
72
+
73
+ private setQueryParams(req: IncomingMessage): void {
74
+ if (!req.url) {
75
+ return;
76
+ }
77
+
78
+ const url = new URL(req.url, 'http://fake.com');
79
+ const query = qs.parse(url.search, {
80
+ ignoreQueryPrefix: true,
81
+ });
82
+
83
+ req.query = query;
84
+ }
85
+
86
+ }
87
+
@@ -0,0 +1,61 @@
1
+ import { Service } from 'typedi';
2
+ import { ServerResponse, IncomingMessage } from 'http';
3
+ import mime from 'mime';
4
+ import fs from 'fs/promises';
5
+ import path from 'path';
6
+ import { Middleware } from '../services/pipeline.service';
7
+ import { LoggerService } from '../services/logger.service';
8
+
9
+ @Service({
10
+ global: true
11
+ })
12
+ export class MockMiddleware implements Middleware<[req: IncomingMessage, res: ServerResponse]> {
13
+ constructor(
14
+ private logger: LoggerService,
15
+ ) {
16
+ }
17
+
18
+ public async use(req: IncomingMessage, res: ServerResponse, next: () => void): Promise<void> {
19
+ try {
20
+ const mockRule = req?.mockRule;
21
+
22
+ if (!mockRule) {
23
+ next();
24
+ return;
25
+ }
26
+
27
+ if ('script' in mockRule) {
28
+ const file = await fs.readFile(path.resolve(mockRule.script));
29
+
30
+ const func = new Function('req, res', file.toString());
31
+ await func(req, res);
32
+
33
+ this.logger.info(`[MOCK BY SCRIPT] ${res.statusCode || 200} ${req.url}`);
34
+ return;
35
+ }
36
+
37
+ if ('file' in mockRule) {
38
+ const file = await fs.readFile(path.resolve(mockRule.file));
39
+ const mimeType = mime.getType(path.resolve(mockRule.file));
40
+
41
+ res.statusCode = mockRule.statusCode || res.statusCode;
42
+
43
+ if (mimeType) {
44
+ res.setHeader('content-type', mimeType);
45
+ }
46
+
47
+ res.setHeader('content-length', file.length);
48
+ res.end(file);
49
+
50
+ this.logger.info(`[MOCK BY FILE] ${res.statusCode} ${req.url}`);
51
+ return;
52
+ }
53
+
54
+ next();
55
+ } catch (e) {
56
+ this.logger.error(e);
57
+ next();
58
+ }
59
+ }
60
+
61
+ }
@@ -0,0 +1,42 @@
1
+ import { Service } from 'typedi';
2
+ import { IncomingMessage, ServerResponse } from 'http';
3
+ import { HttpProxy } from '../services/http-proxy.service';
4
+ import { RemoteRulesMatcher } from '../services/rules-matchers/remote-rules-matcher.service';
5
+ import { Config } from '../services/config.service';
6
+ import { Middleware } from '../services/pipeline.service';
7
+ import { LoggerService } from '../services/logger.service';
8
+
9
+
10
+ @Service({
11
+ global: true
12
+ })
13
+ export class ProxyMiddleware implements Middleware<[req: IncomingMessage, res: ServerResponse]> {
14
+ constructor(
15
+ private httpProxy: HttpProxy,
16
+ private remoteRulesMatcher: RemoteRulesMatcher,
17
+ private config: Config,
18
+ private logger: LoggerService,
19
+ ) {
20
+ }
21
+
22
+ public use(req: IncomingMessage, res: ServerResponse) {
23
+ const url = req.url || '';
24
+
25
+ const proxyOptions = this.config.get().proxyOptions;
26
+ const remoteRule = this.remoteRulesMatcher.match(url);
27
+ const target = remoteRule
28
+ ? this.remoteRulesMatcher.toPath(url, remoteRule)
29
+ : undefined;
30
+
31
+ const options = {
32
+ ...proxyOptions,
33
+ prependPath: !target,
34
+ target: target || proxyOptions.target,
35
+ };
36
+
37
+ this.httpProxy.web(
38
+ req, res, options, (error) => this.logger.error(error)
39
+ );
40
+ }
41
+
42
+ }
@@ -0,0 +1,34 @@
1
+ import { Service } from 'typedi';
2
+ import { IncomingMessage, ServerResponse } from 'http';
3
+ import { Middleware } from '../services/pipeline.service';
4
+ import { LoggerService } from '../services/logger.service';
5
+
6
+
7
+ @Service({
8
+ global: true
9
+ })
10
+ export class ProxyResMiddleware implements Middleware<[proxyRes: IncomingMessage, req: IncomingMessage, res: ServerResponse]> {
11
+ constructor(
12
+ private logger: LoggerService,
13
+ ) {
14
+ }
15
+
16
+ public use(proxyRes: IncomingMessage, req: IncomingMessage, res: ServerResponse): void {
17
+ try {
18
+ for (let key in proxyRes.headers) {
19
+ res.setHeader(key, proxyRes.headers[key] as any);
20
+ }
21
+
22
+ res.statusCode = proxyRes.statusCode as number;
23
+ res.statusMessage = proxyRes.statusMessage as string;
24
+
25
+ const response: Buffer = proxyRes.rawBody || Buffer.from('');
26
+
27
+ res.setHeader('content-length', response.length);
28
+ res.end(response);
29
+ } catch(e) {
30
+ this.logger.error(e);
31
+ }
32
+ }
33
+
34
+ }
@@ -0,0 +1,22 @@
1
+ import { Service } from 'typedi';
2
+ import { IncomingMessage, ServerResponse } from 'http';
3
+ import { Middleware } from '../services/pipeline.service';
4
+
5
+ @Service({
6
+ global: true
7
+ })
8
+ export class RawBodyMiddleware implements Middleware<[proxyRes: IncomingMessage, req: IncomingMessage, res: ServerResponse]> {
9
+ public use(proxyRes: IncomingMessage, req: IncomingMessage, res: ServerResponse, next: () => void): void {
10
+ const body: Uint8Array[] = [];
11
+
12
+ proxyRes.on('data', (chunk: Uint8Array) => {
13
+ body.push(chunk);
14
+ });
15
+
16
+ proxyRes.on('end', () => {
17
+ proxyRes.rawBody = Buffer.concat(body);
18
+ next();
19
+ });
20
+ }
21
+
22
+ }
@@ -0,0 +1,64 @@
1
+ import { Service } from 'typedi';
2
+ import { IncomingMessage, ServerResponse } from 'http';
3
+ import { SUPPORTED_ENCODING, encodeBuffer, decodeBuffer } from 'http-encoding'
4
+ import fs from 'fs/promises';
5
+ import path from 'path';
6
+ import { Middleware } from '../services/pipeline.service';
7
+ import { LoggerService } from '../services/logger.service';
8
+
9
+ type RewriteFunction = (body: Buffer, proxyRes: IncomingMessage, req: IncomingMessage, res: ServerResponse) => Promise<Buffer> | Buffer;
10
+
11
+ @Service({
12
+ global: true
13
+ })
14
+ export class RewriteMiddleware implements Middleware<[proxyRes: IncomingMessage, req: IncomingMessage, res: ServerResponse]> {
15
+ constructor(
16
+ private logger: LoggerService,
17
+ ) {
18
+ }
19
+
20
+ public async use(proxyRes: IncomingMessage, req: IncomingMessage, res: ServerResponse, next: () => void): Promise<void> {
21
+ const rewriteRule = req?.rewriteRule;
22
+
23
+ try {
24
+ if (!rewriteRule) {
25
+ next();
26
+ return;
27
+ }
28
+
29
+ const encoding = proxyRes.headers['content-encoding'] as SUPPORTED_ENCODING;
30
+ const response: Buffer = proxyRes.rawBody || Buffer.from('');
31
+
32
+ if ('script' in rewriteRule) {
33
+ const file = await fs.readFile(path.resolve(rewriteRule.script));
34
+ const encodedResponse = await decodeBuffer(response, encoding);
35
+ const func = new Function('body, proxyRes, req, res', 'var result;' + file + ';return result;') as RewriteFunction;
36
+
37
+ const rewritedResponse: Buffer = await func(encodedResponse, proxyRes, req, res);
38
+ proxyRes.rawBody = await encodeBuffer(rewritedResponse, encoding);
39
+
40
+ this.logger.info(`[REWRITE BY SCRIPT] ${proxyRes.statusCode} ${req.url}`);
41
+
42
+ next();
43
+ return;
44
+ }
45
+
46
+ if ('file' in rewriteRule) {
47
+ const file = await fs.readFile(path.resolve(rewriteRule.file));
48
+ proxyRes.rawBody = await encodeBuffer(file, encoding);
49
+ res.statusCode = rewriteRule.statusCode || res.statusCode;
50
+
51
+ this.logger.info(`[REWRITE BY FILE] ${proxyRes.statusCode} ${req.url}`);
52
+
53
+ next();
54
+ return;
55
+ }
56
+
57
+ next();
58
+ } catch (e) {
59
+ this.logger.error(e);
60
+ next();
61
+ }
62
+ }
63
+
64
+ }
@@ -0,0 +1,113 @@
1
+ import { Service } from 'typedi';
2
+ import { ServerResponse, IncomingMessage } from 'http';
3
+ import path from 'path';
4
+ import fs from 'fs/promises';
5
+ import mime from 'mime';
6
+ import { Dirent } from 'fs';
7
+ import { Config } from '../services/config.service';
8
+ import { Middleware } from '../services/pipeline.service';
9
+ import { LoggerService } from '../services/logger.service';
10
+
11
+
12
+ @Service({
13
+ global: true
14
+ })
15
+ export class StaticMiddleware implements Middleware<[req: IncomingMessage, res: ServerResponse]> {
16
+ constructor(
17
+ private config: Config,
18
+ private logger: LoggerService,
19
+ ) {
20
+ }
21
+
22
+ public async use(req: IncomingMessage, res: ServerResponse, next: () => void): Promise<void> {
23
+ try {
24
+ const staticRules = this.config.get().staticRules || [];
25
+
26
+ const url = new URL(req.url || '', 'http://fake.com');
27
+ const urlPath = url.pathname;
28
+
29
+ if (!staticRules) {
30
+ next();
31
+ return;
32
+ }
33
+
34
+ const currentStaticRule = staticRules.filter((staticRule) => {
35
+ return urlPath.startsWith(staticRule.path);
36
+ })[0];
37
+
38
+ if (!currentStaticRule) {
39
+ next();
40
+ return;
41
+ }
42
+
43
+ const pathToDirectory = path.resolve(currentStaticRule.directory);
44
+
45
+ const dirents = await this.readdir(pathToDirectory);
46
+
47
+ const filePath = dirents
48
+ .filter(dirent => !dirent.isDirectory())
49
+ .map(dirent => ({
50
+ urlPath: path.join(
51
+ currentStaticRule.path,
52
+ path.join(dirent.path, dirent.name).replace(pathToDirectory, '')
53
+ ).replace(/\\/g, '/'),
54
+ path: path.join(dirent.path, dirent.name).replace(/\\/g, '/'),
55
+ }
56
+ ))
57
+ .filter(dirent => {
58
+ const pathname = path.extname(urlPath)
59
+ ? urlPath
60
+ : path.join(urlPath, currentStaticRule.directoryIndex || '').replace(/\\/g, '/');
61
+
62
+ if (currentStaticRule.caseInsensitive) {
63
+ return dirent.urlPath.toLocaleLowerCase() === pathname.toLowerCase();
64
+ } else {
65
+ return dirent.urlPath === pathname;
66
+ }
67
+ })
68
+ .map(dirent => dirent.path)[0];
69
+
70
+
71
+ if (!filePath) {
72
+ next();
73
+ return;
74
+ }
75
+
76
+ const file = await fs.readFile(path.resolve(filePath));
77
+ const mimeType = mime.getType(path.resolve(filePath));
78
+
79
+ if (mimeType) {
80
+ res.setHeader('content-type', mimeType);
81
+ }
82
+
83
+ res.setHeader('content-length', file.length);
84
+ res.end(file);
85
+
86
+ this.logger.info(`[STATIC] ${req.url}`);
87
+ } catch (e) {
88
+ this.logger.error(e);
89
+ next();
90
+ }
91
+ }
92
+
93
+ private async readdir(pathname: string, subpathname?: string): Promise<Dirent[]> {
94
+ const files: Dirent[] = [];
95
+ const fullpath = path.join(pathname, subpathname || '');
96
+
97
+ const dirents = await fs.readdir(fullpath, {
98
+ withFileTypes: true,
99
+ });
100
+
101
+ for (let dirent of dirents) {
102
+ dirent.path = fullpath;
103
+ files.push(dirent);
104
+
105
+ if (dirent.isDirectory()) {
106
+ files.push(...await this.readdir(fullpath, dirent.name));
107
+ }
108
+ }
109
+
110
+ return files;
111
+ }
112
+
113
+ }
@@ -0,0 +1,26 @@
1
+ import { Service, Token, Inject } from 'typedi';
2
+ import { YxorpConfig } from "../types/yxorp-config";
3
+
4
+
5
+ export const ProxyConfigToken = new Token<YxorpConfig>('ProxyConfigToken');
6
+
7
+ @Service({
8
+ global: true
9
+ })
10
+ export class Config {
11
+ private config!: YxorpConfig;
12
+
13
+ constructor(
14
+ @Inject(ProxyConfigToken) private proxyConfig: YxorpConfig,
15
+ ) {
16
+ this.config = this.proxyConfig;
17
+ }
18
+
19
+ public set(config: YxorpConfig): void {
20
+ this.config = config;
21
+ }
22
+
23
+ public get(): YxorpConfig {
24
+ return this.config;
25
+ }
26
+ }
@@ -0,0 +1,25 @@
1
+ import { Service } from 'typedi';
2
+ import { IncomingMessage, ServerResponse} from 'http';
3
+ import httpProxy from 'http-proxy';
4
+ import { Pipeline } from "./pipeline.service";
5
+
6
+ @Service({
7
+ global: true
8
+ })
9
+ export class HttpProxy {
10
+ private httpProxy = httpProxy.createProxyServer({});
11
+
12
+ constructor(
13
+ private pipeline: Pipeline<[proxyRes: IncomingMessage, req: IncomingMessage, res: ServerResponse]>,
14
+ ) {
15
+ }
16
+
17
+ public use = this.pipeline.use.bind(this.pipeline);
18
+ public execute = this.pipeline.execute.bind(this.pipeline);
19
+ public on = this.httpProxy.on.bind(this.httpProxy);
20
+ public addListener = this.httpProxy.addListener.bind(this.httpProxy);
21
+ public removeListener = this.httpProxy.removeListener.bind(this.httpProxy);
22
+ public web = this.httpProxy.web.bind(this.httpProxy);
23
+ public ws = this.httpProxy.ws.bind(this.httpProxy);
24
+
25
+ }
@@ -0,0 +1,24 @@
1
+ import { Service } from 'typedi';
2
+ import http, { IncomingMessage, Server, ServerResponse} from 'http';
3
+ import { Pipeline } from './pipeline.service';
4
+
5
+ @Service({
6
+ global: true
7
+ })
8
+ export class HttpServer {
9
+ private server: Server = http.createServer((req: IncomingMessage, res: ServerResponse) => {
10
+ this.pipeline.execute(req, res);
11
+ });
12
+
13
+ constructor(
14
+ private pipeline: Pipeline<[req: IncomingMessage, res: ServerResponse]>,
15
+ ) {
16
+ }
17
+
18
+ public use = this.pipeline.use.bind(this.pipeline);
19
+ public on = this.server.on.bind(this.server);
20
+ public addListener = this.server.addListener.bind(this.server);
21
+ public removeListener = this.server.removeListener.bind(this.server);
22
+ public listen = this.server.listen.bind(this.server);
23
+
24
+ }
@@ -0,0 +1,21 @@
1
+ import { Service } from 'typedi';
2
+ import { createLogger, format, transports } from 'winston';
3
+
4
+
5
+ @Service({
6
+ global: true
7
+ })
8
+ export class LoggerService {
9
+ private logger = createLogger({
10
+ format: format.combine(
11
+ format.colorize(),
12
+ format.simple(),
13
+ ),
14
+ transports: [new transports.Console],
15
+ });
16
+
17
+ public info = this.logger.info.bind(this.logger);
18
+ public error = this.logger.error.bind(this.logger);
19
+ public warn = this.logger.warn.bind(this.logger);
20
+ public debug = this.logger.debug.bind(this.logger);
21
+ }
@@ -0,0 +1,44 @@
1
+ import { Container, Service } from 'typedi';
2
+
3
+
4
+ export interface Middleware<T extends any[]> {
5
+ use(...args: [...T, () => void]): void;
6
+ [k: string]: any;
7
+ }
8
+
9
+ export interface MiddlewareCtor<T extends any[]> {
10
+ new(...args: any): Middleware<T>;
11
+ }
12
+ export type Middlewares<T extends any[]> = MiddlewareCtor<T>[];
13
+
14
+ @Service({
15
+ transient: true,
16
+ })
17
+ export class Pipeline<T extends any[]> {
18
+ private stack: Middlewares<T> = [];
19
+
20
+ public use(...middlewares: Middlewares<T>) {
21
+ this.stack.push(...middlewares);
22
+ }
23
+
24
+ public async execute(...args: [...T]) {
25
+ let prevIndex = -1;
26
+
27
+ const runner = async (index: number) => {
28
+ if (index === prevIndex) {
29
+ throw new Error('next() called multiple times');
30
+ }
31
+
32
+ prevIndex = index;
33
+
34
+ const middleware = Container.get(this.stack[index]);
35
+
36
+ await middleware.use(...args, () => {
37
+ return runner(index + 1);
38
+ });
39
+ }
40
+
41
+ await runner(0);
42
+ }
43
+
44
+ }
@@ -0,0 +1,48 @@
1
+ import { Service } from 'typedi';
2
+ import { Config } from '../config.service';
3
+ import { match } from 'path-to-regexp';
4
+ import { MockRule } from '../../types/yxorp-config';
5
+
6
+
7
+ @Service({
8
+ global: true
9
+ })
10
+ export class MockRulesMatcher {
11
+ constructor(
12
+ private config: Config
13
+ ) {
14
+ }
15
+
16
+ public match(url: string, method: string): MockRule | undefined {
17
+ const mockRules = this.config.get().mockRules || [];
18
+
19
+ for (let mockRule of mockRules) {
20
+ if (mockRule.disable) {
21
+ continue;
22
+ }
23
+
24
+ if (mockRule.method.toLowerCase() !== method.toLowerCase()) {
25
+ continue;
26
+ }
27
+
28
+ const matchResult = match<Record<string, string>>(mockRule.path, {
29
+ decode: decodeURIComponent,
30
+ })(url);
31
+
32
+ if (matchResult) {
33
+ return mockRule;
34
+ }
35
+ }
36
+ }
37
+
38
+ public params(url: string, mockRule: MockRule): Object | undefined {
39
+ const matchResult = match(mockRule.path, {
40
+ decode: decodeURIComponent,
41
+ })(url);
42
+
43
+ if (matchResult) {
44
+ return matchResult.params;
45
+ }
46
+ }
47
+
48
+ }
@@ -0,0 +1,49 @@
1
+ import { Service } from 'typedi';
2
+ import { Config } from '../config.service';
3
+ import { match, compile } from 'path-to-regexp';
4
+ import { RemoteRule } from '../../types/yxorp-config';
5
+
6
+
7
+ @Service({
8
+ global: true
9
+ })
10
+ export class RemoteRulesMatcher {
11
+ constructor(
12
+ private config: Config
13
+ ) {
14
+ }
15
+
16
+ public match(url: string, ws: boolean = false): RemoteRule | undefined {
17
+ const rules = this.config.get().remoteRules || [];
18
+
19
+ for (let rule of rules) {
20
+ if (rule.disable) {
21
+ continue;
22
+ }
23
+
24
+ if (!!rule.ws !== ws) {
25
+ continue;
26
+ }
27
+
28
+ const matchResult = match(rule.path, {
29
+ decode: decodeURIComponent
30
+ })(url);
31
+
32
+ if (matchResult) {
33
+ return rule;
34
+ }
35
+ }
36
+ }
37
+
38
+ public toPath(url: string, remoteRule: RemoteRule): string | undefined {
39
+ const matchResult = match(remoteRule.path, {
40
+ decode: decodeURIComponent
41
+ })(url);
42
+
43
+ if (matchResult) {
44
+ const toPath = compile(remoteRule.target, { validate: false });
45
+ return toPath(matchResult.params);
46
+ }
47
+ }
48
+
49
+ }
@@ -0,0 +1,48 @@
1
+ import { Service } from 'typedi';
2
+ import { Config } from '../config.service';
3
+ import { match } from 'path-to-regexp';
4
+ import { RewriteRule } from '../../types/yxorp-config';
5
+
6
+
7
+ @Service({
8
+ global: true
9
+ })
10
+ export class RewriteRulesMatcher {
11
+ constructor(
12
+ private config: Config
13
+ ) {
14
+ }
15
+
16
+ public match(url: string, method: string): RewriteRule | undefined {
17
+ const rewriteRules = this.config.get().rewriteRules || [];
18
+
19
+ for (let rewriteRule of rewriteRules) {
20
+ if (rewriteRule.disable) {
21
+ continue;
22
+ }
23
+
24
+ if (rewriteRule.method.toLowerCase() !== method.toLowerCase()) {
25
+ continue;
26
+ }
27
+
28
+ const matchResult = match<Record<string, string>>(rewriteRule.path, {
29
+ decode: decodeURIComponent,
30
+ })(url);
31
+
32
+ if (matchResult) {
33
+ return rewriteRule;
34
+ }
35
+ }
36
+ }
37
+
38
+ public params(url: string, rewriteRule: RewriteRule): Object | undefined {
39
+ const matchResult = match(rewriteRule.path, {
40
+ decode: decodeURIComponent,
41
+ })(url);
42
+
43
+ if (matchResult) {
44
+ return matchResult.params;
45
+ }
46
+ }
47
+
48
+ }
@@ -0,0 +1,73 @@
1
+ import { Service } from 'typedi';
2
+ import { IncomingMessage, ServerResponse } from 'http';
3
+ import { Duplex } from 'stream';
4
+ import { BootstrapMiddleware } from '../middleware/bootstrap.middleware';
5
+ import { ProxyMiddleware } from '../middleware/proxy.middleware';
6
+ import { RawBodyMiddleware } from '../middleware/rawBody.middleware';
7
+ import { ProxyResMiddleware } from '../middleware/proxyRes.middleware';
8
+ import { RewriteMiddleware } from '../middleware/rewrite.middleware';
9
+ import { MockMiddleware } from '../middleware/mock.middleware';
10
+ import { StaticMiddleware } from '../middleware/static.middleware';
11
+ import { HttpServer } from './http-server.service';
12
+ import { HttpProxy } from './http-proxy.service';
13
+ import { RemoteRulesMatcher } from './rules-matchers/remote-rules-matcher.service';
14
+ import { Config } from './config.service';
15
+ import { LoggerService } from './logger.service';
16
+
17
+
18
+ @Service({
19
+ global: true
20
+ })
21
+ export class YxorpServer {
22
+ constructor(
23
+ private httpServer: HttpServer,
24
+ private httpProxy: HttpProxy,
25
+ private remoteRulesMatcher: RemoteRulesMatcher,
26
+ private config: Config,
27
+ private logger: LoggerService,
28
+ ) {
29
+ this.httpServer.use(
30
+ StaticMiddleware,
31
+ BootstrapMiddleware,
32
+ MockMiddleware,
33
+ ProxyMiddleware
34
+ );
35
+ this.httpProxy.use(
36
+ BootstrapMiddleware,
37
+ RawBodyMiddleware,
38
+ RewriteMiddleware,
39
+ ProxyResMiddleware,
40
+ );
41
+
42
+ this.httpProxy.on('proxyRes', this.onProxyRes);
43
+ this.httpServer.addListener('upgrade', this.onServerUpgrade);
44
+ }
45
+
46
+ private onProxyRes = (proxyRes: IncomingMessage, req: IncomingMessage, res: ServerResponse) => {
47
+ this.httpProxy.execute(proxyRes, req, res);
48
+ }
49
+
50
+ private onServerUpgrade = (req: IncomingMessage, socket: Duplex, head: Buffer): void => {
51
+ const url = req.url || '';
52
+
53
+ const proxyOptions = this.config.get().proxyOptions;
54
+ const remoteRule = this.remoteRulesMatcher.match(url, true);
55
+ const target = remoteRule
56
+ ? this.remoteRulesMatcher.toPath(url, remoteRule)
57
+ : undefined;
58
+
59
+ const options = {
60
+ ...proxyOptions,
61
+ prependPath: !target,
62
+ target: target || proxyOptions.target,
63
+ }
64
+
65
+ this.httpProxy.ws(
66
+ req, socket, head, options, (error) => this.logger.info(error)
67
+ );
68
+ }
69
+
70
+ public listen = this.httpServer.listen.bind(this.httpServer) as typeof this.httpServer.listen;
71
+
72
+ }
73
+
@@ -0,0 +1,10 @@
1
+ declare module 'http' {
2
+ export interface IncomingMessage {
3
+ rawBody?: Buffer;
4
+ rewriteRule?: import("./yxorp-config").RewriteRule;
5
+ rewriteRuleParams?: Object;
6
+ mockRule?: import("./yxorp-config").MockRule;
7
+ mockRuleParams?: Object;
8
+ query?: Record<string, any>;
9
+ }
10
+ }
@@ -0,0 +1,67 @@
1
+ import { ServerOptions } from 'http-proxy';
2
+
3
+ export interface YxorpConfig extends ConfigFile {
4
+ proxyOptions: ServerOptions;
5
+ }
6
+
7
+ export interface ConfigFile {
8
+ target: string;
9
+ proxyPort: string | number;
10
+ scripts?: string[];
11
+ remoteRules?: RemoteRule[];
12
+ staticRules?: StaticRule[];
13
+ mockRules?: MockRule[];
14
+ rewriteRules?: RewriteRule[];
15
+ }
16
+
17
+ export interface RemoteRule {
18
+ path: string;
19
+ target: string;
20
+ ws?: boolean;
21
+ disable?: boolean;
22
+ }
23
+
24
+ export type StaticRule = {
25
+ path: string;
26
+ directory: string;
27
+ caseInsensitive?: boolean;
28
+ directoryIndex?: string;
29
+ disable?: boolean;
30
+ }
31
+
32
+ export type MockFileRule = {
33
+ method: string;
34
+ path: string;
35
+ file: string;
36
+ statusCode?: number;
37
+ disable?: boolean;
38
+ }
39
+
40
+ export type MockScriptRule = {
41
+ method: string;
42
+ path: string;
43
+ script: string;
44
+ disable?: boolean;
45
+ }
46
+
47
+ export type MockRule = MockFileRule | MockScriptRule;
48
+
49
+
50
+ export type RewriteFileRule = {
51
+ method: string;
52
+ path: string;
53
+ file: string;
54
+ statusCode?: number;
55
+ disable?: boolean;
56
+ }
57
+
58
+ export type RewriteScriptRule = {
59
+ method: string;
60
+ path: string;
61
+ script: string;
62
+ disable?: boolean;
63
+ }
64
+
65
+ export type RewriteRule = RewriteFileRule | RewriteScriptRule;
66
+
67
+
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ // This is an alias to @tsconfig/node16: https://github.com/tsconfig/bases
3
+ "extends": "ts-node/node12/tsconfig.json",
4
+ "ts-node": {
5
+ // It is faster to skip typechecking.
6
+ // Remove if you want ts-node to do typechecking.
7
+ // "transpileOnly": true,
8
+ "files": true,
9
+ "compilerOptions": {
10
+ // compilerOptions specified here will override those declared below,
11
+ // but *only* in ts-node. Useful if you want ts-node and tsc to use
12
+ // different options with a single tsconfig.json.
13
+ }
14
+ },
15
+ "compilerOptions": {
16
+ // typescript options here
17
+ "resolveJsonModule": true,
18
+ "emitDecoratorMetadata": true,
19
+ "experimentalDecorators": true
20
+ },
21
+ }