-
Notifications
You must be signed in to change notification settings - Fork 574
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add http proxy component (#1843)
- Loading branch information
Showing
24 changed files
with
870 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
} | ||
] | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>[]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
process.env.MIDWAY_TS_MODE = 'true'; | ||
jest.setTimeout(30000); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
import { HttpProxyConfig } from '..'; | ||
export const httpProxy: HttpProxyConfig = null; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
}; | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.