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 +16 -0
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/bin/yxorp.js +22 -0
- package/package.json +38 -0
- package/src/index.ts +48 -0
- package/src/middleware/bootstrap.middleware.ts +87 -0
- package/src/middleware/mock.middleware.ts +61 -0
- package/src/middleware/proxy.middleware.ts +42 -0
- package/src/middleware/proxyRes.middleware.ts +34 -0
- package/src/middleware/rawBody.middleware.ts +22 -0
- package/src/middleware/rewrite.middleware.ts +64 -0
- package/src/middleware/static.middleware.ts +113 -0
- package/src/services/config.service.ts +26 -0
- package/src/services/http-proxy.service.ts +25 -0
- package/src/services/http-server.service.ts +24 -0
- package/src/services/logger.service.ts +21 -0
- package/src/services/pipeline.service.ts +44 -0
- package/src/services/rules-matchers/mock-rules-matcher.service.ts +48 -0
- package/src/services/rules-matchers/remote-rules-matcher.service.ts +49 -0
- package/src/services/rules-matchers/rewrite-rules-matcher.service.ts +48 -0
- package/src/services/yxorp-server.service.ts +73 -0
- package/src/types/http.d.ts +10 -0
- package/src/types/yxorp-config.ts +67 -0
- package/tsconfig.json +21 -0
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
|
+
}
|