Skip to content

Commit

Permalink
feat: add http proxy component (#1843)
Browse files Browse the repository at this point in the history
  • Loading branch information
echosoar committed Mar 23, 2022
1 parent e3adda0 commit 5281e31
Show file tree
Hide file tree
Showing 24 changed files with 870 additions and 0 deletions.
35 changes: 35 additions & 0 deletions packages/http-proxy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
## HTTP 代理组件

HTTP 代理组件

适用于 `@midwayjs/faas``@midwayjs/web``@midwayjs/koa``@midwayjs/express` 多种框架的 HTTP 代理组件,支持GET、POST等多种请求方法。

### Usage

1. 安装依赖
```shell
tnpm i @midwayjs/http-proxy --save
```
2. 在 configuration 中引入组件,
```ts
import * as proxy from '@midwayjs/http-proxy';
@Configuration({
imports: [
// ...other components
proxy
],
})
export class AutoConfiguration {}
```

### 配置

```ts
export const httpProxy = [
{
host: 'http://127.0.0.1',
match: /\/assets\/(.*)/,
target: 'http://127.0.0.1/$1',
}
]
```
8 changes: 8 additions & 0 deletions packages/http-proxy/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { HttpProxyConfig } from './dist/index';
export * from './dist/index';

declare module '@midwayjs/core/dist/interface' {
interface MidwayConfig {
httpProxy?: Partial<HttpProxyConfig> | Partial<HttpProxyConfig>[];
}
}
7 changes: 7 additions & 0 deletions packages/http-proxy/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ['<rootDir>/test/fixtures'],
coveragePathIgnorePatterns: ['<rootDir>/test/'],
setupFilesAfterEnv: ['./jest.setup.js'],
};
2 changes: 2 additions & 0 deletions packages/http-proxy/jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
process.env.MIDWAY_TS_MODE = 'true';
jest.setTimeout(30000);
37 changes: 37 additions & 0 deletions packages/http-proxy/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@midwayjs/http-proxy",
"version": "3.1.1",
"description": "Midway Component for http proxy",
"main": "dist/index.js",
"typings": "index.d.ts",
"scripts": {
"build": "tsc",
"test": "node --require=ts-node/register ../../node_modules/.bin/jest --runInBand",
"cov": "node --require=ts-node/register ../../node_modules/.bin/jest --runInBand --coverage --forceExit",
"ci": "npm run test"
},
"keywords": [],
"author": "",
"files": [
"dist/**/*.js",
"dist/**/*.d.ts",
"index.d.ts"
],
"engines": {
"node": ">=12"
},
"license": "MIT",
"dependencies": {
"axios": "^0.26.0"
},
"devDependencies": {
"@midwayjs/core": "^3.1.1",
"@midwayjs/decorator": "^3.0.10",
"@midwayjs/express": "^3.1.1",
"@midwayjs/faas": "^3.1.1",
"@midwayjs/koa": "^3.1.1",
"@midwayjs/mock": "^3.1.1",
"@midwayjs/serverless-app": "^3.1.1",
"@midwayjs/web": "^3.1.1"
}
}
2 changes: 2 additions & 0 deletions packages/http-proxy/src/config/config.default.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { HttpProxyConfig } from '..';
export const httpProxy: HttpProxyConfig = null;
29 changes: 29 additions & 0 deletions packages/http-proxy/src/configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Config, Configuration, Inject } from '@midwayjs/decorator';
import * as DefaultConfig from './config/config.default';
import { MidwayApplicationManager } from '@midwayjs/core';
import { HttpProxyMiddleware } from './middleware';
@Configuration({
namespace: 'upload',
importConfigs: [
{
default: DefaultConfig,
},
],
})
export class HttpProxyConfiguration {
@Inject()
applicationManager: MidwayApplicationManager;

@Config('httpProxy')
httpProxy;

async onReady() {
if (this.httpProxy) {
this.applicationManager
.getApplications(['koa', 'faas', 'express', 'egg'])
.forEach(app => {
app.useMiddleware(HttpProxyMiddleware);
});
}
}
}
3 changes: 3 additions & 0 deletions packages/http-proxy/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { HttpProxyConfiguration as Configuration } from './configuration';
export * from './interface';
export * from './middleware';
8 changes: 8 additions & 0 deletions packages/http-proxy/src/interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface HttpProxyConfig {
match: RegExp;
host?: string;
target?: string;
ignoreHeaders?: {
[key: string]: boolean;
}
}
133 changes: 133 additions & 0 deletions packages/http-proxy/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import {
Config,
Logger,
Middleware,
MidwayFrameworkType,
} from '@midwayjs/decorator';
import { IMiddleware, IMidwayLogger } from '@midwayjs/core';
import { HttpProxyConfig } from './interface';
import axios from 'axios';

@Middleware()
export class HttpProxyMiddleware implements IMiddleware<any, any> {
@Config('httpProxy')
httpProxy: HttpProxyConfig | HttpProxyConfig[];

@Logger()
logger: IMidwayLogger;

resolve(app) {
if (app.getFrameworkType() === MidwayFrameworkType.WEB_EXPRESS) {
return async (req: any, res: any, next: any) => {
return this.execProxy(req, req, res, next, true);
};
} else {
return async (ctx, next) => {
const req = ctx.request?.req || ctx.request;
return this.execProxy(ctx, req, ctx, next, false);
};
}
}

async execProxy(ctx, req, res, next, isExpress) {
const proxyInfo = this.getProxyList(ctx.url);
if (!proxyInfo) {
return next();
}
const { proxy, url } = proxyInfo;
const reqHeaders = {};
for (const key of Object.keys(req.headers)) {
if (proxy.ignoreHeaders?.[key] || ctx.header[key] === undefined) {
continue;
}
reqHeaders[key.toLowerCase()] = ctx.header[key];
}
// X-Forwarded-For
const forwarded = req.headers['x-forwarded-for'];
reqHeaders['x-forwarded-for'] = forwarded
? `${forwarded}, ${ctx.ip}`
: ctx.ip;
reqHeaders['host'] = url.host;

const method = req.method.toUpperCase();

const targetRes = res.res || res;
const isStream = targetRes.on && targetRes.writable;

const reqOptions: any = {
method,
url: url.href,
headers: reqHeaders,
responseType: isStream ? 'stream' : 'arrayBuffer',
};
if (method === 'POST' || method === 'PUT') {
reqOptions.data = req.body ?? ctx.request?.body;
if (
req.headers['content-type'] === 'application/x-www-form-urlencoded' &&
typeof reqOptions.data !== 'string'
) {
reqOptions.data = Object.keys(reqOptions.data)
.map(
key =>
`${encodeURIComponent(key)}=${encodeURIComponent(
reqOptions.data[key]
)}`
)
.join('&');
}
}

const proxyResponse = await axios(reqOptions).catch(err => {
if (!err || !err.response) {
throw err || new Error('proxy unknown error');
}
return err.response;
});
res.type = proxyResponse.headers['content-type'];
Object.keys(proxyResponse.headers).forEach(key => {
res.set(key, proxyResponse.headers[key]);
});
res.status = proxyResponse.status;
if (isStream) {
await new Promise(resolve => {
proxyResponse.data.on('finish', resolve);
proxyResponse.data.pipe(targetRes);
});
} else {
res.body = proxyResponse.data;
}
}

getProxyList(url): undefined | { proxy: HttpProxyConfig; url: URL } {
if (!this.httpProxy) {
return;
}
const proxyList = [].concat(this.httpProxy);
for (const proxy of proxyList) {
if (!proxy.match) {
continue;
}
if (!proxy.match.test(url)) {
continue;
}

if (proxy.host) {
if (url[0] === '/') {
url = proxy.host + url;
}

const urlObj = new URL(url);
return {
proxy,
url: urlObj,
};
} else if (proxy.target) {
const newURL = url.replace(proxy.match, proxy.target);
return {
proxy,
url: new URL(newURL),
};
}
}
}
}
78 changes: 78 additions & 0 deletions packages/http-proxy/test/express.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { createApp, createHttpRequest, close } from '@midwayjs/mock';
import { join } from 'path';
import * as assert from 'assert';

describe('test/express.test.ts', function () {
let app;
beforeAll(async () => {
const appDir = join(__dirname, 'fixtures/express');
app = await createApp(appDir);
})

afterAll(async () => {
await close(app);
});

it('get image by host', async () => {
const request = await createHttpRequest(app);

await request.get('/tfs/TB1.1EzoBBh1e4jSZFhXXcC9VXa-48-48.png?version=123')
.expect(200)
.then(async response => {
assert(response.status === 200)
assert(response.headers['content-type'] === 'image/png')
assert(response.body.length);
});
});

it('get javascript by target', async () => {
const request = await createHttpRequest(app);
await request.get('/bdimg/static/wiseindex/amd_modules/@searchfe/assert_3ed54c3.js')
.expect(200)
.then(async response => {
assert(response.status === 200)
assert(response.headers['content-type'] === 'application/x-javascript')
});
});

it('get to httpbin', async () => {
const request = await createHttpRequest(app);
await request.get('/httpbin/get?name=midway')
.expect(200)
.then(async response => {
assert(response.status === 200)
assert(response.body.url === 'https://httpbin.org/get?name=midway');
assert(response.body.args.name === 'midway');
assert(response.body.headers['Host'] === 'httpbin.org');
});
});

it('post json to httpbin', async () => {
const request = await createHttpRequest(app);
await request.post('/httpbin/post')
.send({name: 'midway'})
.set('Accept', 'application/json')
.expect(200)
.then(async response => {
assert(response.status === 200)
assert(response.body.url === 'https://httpbin.org/post');
assert(response.body.headers['Content-Type'] === 'application/json');
assert(response.body.data === JSON.stringify({ name: 'midway'}));
});
});

it('post x-www-form-urlencoded to httpbin', async () => {
const request = await createHttpRequest(app);
await request.post('/httpbin/post')
.send('name=midway')
.set('Accept', 'application/json')
.expect(200)
.then(async response => {

assert(response.status === 200)
assert(response.body.url === 'https://httpbin.org/post');
assert(response.body.headers['Content-Type'] === 'application/x-www-form-urlencoded');
assert(response.body.form.name === 'midway');
});
});
});
Loading

0 comments on commit 5281e31

Please sign in to comment.