├── src ├── template │ └── .gitkeep ├── config.ts ├── util │ ├── template.ts │ ├── model.ts │ └── core.ts └── main.ts ├── entrypoint.sh ├── .github └── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml ├── doc ├── catalog.md ├── config.md └── template.md ├── tsconfig.json ├── Dockerfile ├── docker-compose.yml ├── vercel.json ├── README.md ├── package.json └── LICENSE /src/template/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | npm run run -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /doc/catalog.md: -------------------------------------------------------------------------------- 1 | # 目录 2 | ## [配置](./config.md) 3 | ## [模板](./template.md) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "target": "ESNext", 5 | "esModuleInterop": true, 6 | "outDir": "./dist/" 7 | } 8 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22.13.0-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY ./package.json ./ 6 | RUN npm set registry https://registry.npmmirror.com/ 7 | RUN npm install 8 | 9 | COPY ./ ./ -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import type {Proxy} from './util/model'; 2 | 3 | export let PORT = 377; // 端口 4 | 5 | export let PROXY_SECRET = 'Easy-Reverse-Proxy'; // 代理密钥 6 | // 代理 7 | export let PROXIES: Proxy[] = []; -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | build: ./ 4 | ports: 5 | - '377:377' 6 | entrypoint: ['sh', './entrypoint.sh'] 7 | volumes: 8 | - ./src/config.ts:/app/src/config.ts 9 | - ./src/template:/app/src/template -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "./src/main.ts", 6 | "use": "@vercel/node" 7 | } 8 | ], 9 | "routes": [ 10 | { 11 | "src": "/(.*)", 12 | "dest": "src/main.ts" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /src/util/template.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | import {PROXY_SECRET} from '../config'; 3 | 4 | export function generateProxyUrl(url: string): string { 5 | url = btoa(url); 6 | return `${url}_${crypto.createHmac('sha256', PROXY_SECRET).update(url).digest('hex')}`; 7 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Release](https://img.shields.io/badge/Release-0.2.3-blue) 2 | --- 3 | ## 介绍 4 | 一款基于Node.js Express框架,开发的反向代理程序. 5 | ## 需求 6 | 1. 操作系统: Linux/Windows/MacOS. 7 | 2. 语言: Node.js. 8 | ## 部署 9 | 1. Linux/Windows/MacOS: 执行`npm run run`命令. 10 | 2. Docker: 执行`docker-compose up -d`命令. 11 | ## Vercel部署 12 | 1. GitHub Fork本项目. 13 | 2. 修改配置. 14 | 3. Vercel导入Fork的项目. 15 | 4. Vercel Fork项目的Settings-Domains添加域名. 16 | ## [文档](./doc/catalog.md) -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 功能建议 2 | description: 功能建议 3 | title: '[Feature]: ' 4 | body: 5 | - type: checkboxes 6 | attributes: 7 | label: 在提交前请确保以下这些 8 | options: 9 | - label: Easy-Reverse-Proxy为最新版本 10 | required: true 11 | - label: 在Issues找不到它 12 | required: true 13 | - type: textarea 14 | attributes: 15 | label: 描述 16 | validations: 17 | required: true -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "easy-reverse-proxy", 3 | "version": "0.2.3", 4 | "private": true, 5 | "scripts": { 6 | "dev": "ts-node ./src/main.ts", 7 | "build": "tsc", 8 | "run": "tsc && node ./dist/main.js" 9 | }, 10 | "dependencies": { 11 | "express": "^4.19.2", 12 | "http-proxy-middleware": "^3.0.0" 13 | }, 14 | "devDependencies": { 15 | "@types/express": "^4.17.21", 16 | "ts-node": "^10.9.2", 17 | "typescript": "^5.5.4" 18 | } 19 | } -------------------------------------------------------------------------------- /doc/config.md: -------------------------------------------------------------------------------- 1 | # 配置 2 | ## config.ts 3 | 查看config.ts文件. 4 | ```Typescript 5 | interface Proxy { 6 | domain: string; // 域名 7 | url: string; // 网址 8 | enable: boolean; // 开启 9 | template?: Template; // 模板 10 | } 11 | ``` 12 | ## 示例 13 | ```TypeScript 14 | import type {Proxy} from './util/model'; 15 | 16 | export let PORT = 377; // 端口 17 | 18 | export let PROXY_SECRET = 'Easy-Reverse-Proxy'; // 代理密钥 19 | // 代理 20 | export let PROXIES: Proxy[] = [ 21 | { 22 | domain: 'www.example.com', 23 | url: 'https://github.com/', 24 | enable: true 25 | } 26 | ]; 27 | ``` 28 | 说明: www.example.com 代理 https://github.com/. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug提交 2 | description: Bug提交 3 | title: '[Bug]: ' 4 | body: 5 | - type: checkboxes 6 | attributes: 7 | label: 在提交前请确保以下这些 8 | options: 9 | - label: Easy-Reverse-Proxy为最新版本 10 | required: true 11 | - label: 在Issues找不到它 12 | required: true 13 | - type: dropdown 14 | attributes: 15 | label: 操作系统 16 | multiple: true 17 | options: 18 | - Linux 19 | - Windows 20 | - MacOS 21 | - Other 22 | validations: 23 | required: true 24 | - type: textarea 25 | attributes: 26 | label: 描述 27 | validations: 28 | required: true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Ykaiqx 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. -------------------------------------------------------------------------------- /src/util/model.ts: -------------------------------------------------------------------------------- 1 | import type {RequestHandler} from 'http-proxy-middleware'; 2 | import type {ProxyReqCallback, ProxyResCallback} from 'http-proxy'; 3 | import type {Response} from 'express'; 4 | 5 | export interface Proxy { 6 | domain: string; 7 | url: string; 8 | enable: boolean; 9 | template?: Template; 10 | } 11 | 12 | export interface Middleware { 13 | domain: string; 14 | middleware: RequestHandler; 15 | } 16 | 17 | export interface MiddlewareConfig { 18 | target: string; 19 | changeOrigin: boolean; 20 | selfHandleResponse?: boolean; 21 | on: { 22 | proxyReq: ProxyReqCallback; 23 | proxyRes?: ProxyResCallback; 24 | } 25 | } 26 | 27 | export interface TemplateRequest { 28 | header: object; 29 | body: object; 30 | } 31 | 32 | export interface TemplateResponse { 33 | header: object; 34 | body: Buffer; 35 | } 36 | 37 | export interface Template { 38 | request?: (option: TemplateRequest) => TemplateRequest; 39 | response?: (option: TemplateResponse) => Promise; 40 | } 41 | 42 | export interface GenerateResponseCallback { 43 | (response: Response): void; 44 | } -------------------------------------------------------------------------------- /src/util/core.ts: -------------------------------------------------------------------------------- 1 | import type {Response} from 'express'; 2 | 3 | import type {GenerateResponseCallback} from './model'; 4 | 5 | export enum ExceptionResponseCode { 6 | SYSTEM = 100 7 | } 8 | 9 | export class ExceptionResponse extends Error { 10 | readonly code: ExceptionResponseCode; 11 | 12 | constructor(code: ExceptionResponseCode, message: string) { 13 | super(message); 14 | this.code = code; 15 | } 16 | } 17 | 18 | export class GenerateResponse { 19 | private static generate(httpCode: number, serviceCode: number, message: string, data: any): GenerateResponseCallback { 20 | return function(response: Response): void { 21 | response.status(httpCode).json({ 22 | code: serviceCode, 23 | message: message, 24 | data: data 25 | }); 26 | } 27 | } 28 | 29 | static error(serviceCode: number, message: string, httpCode: number=200): GenerateResponseCallback { 30 | return GenerateResponse.generate(httpCode, serviceCode, message, null); 31 | } 32 | 33 | static success(data: any): GenerateResponseCallback { 34 | return GenerateResponse.generate(200, 200, 'success', data); 35 | } 36 | } -------------------------------------------------------------------------------- /doc/template.md: -------------------------------------------------------------------------------- 1 | # 模板 2 | ## 工具 3 | ### generateProxyUrl 4 | ```TypeScript 5 | function generateProxyUrl(url: string): string 6 | ``` 7 | 1. 参数: `url`: 网址. 8 | 2. 说明: 生成原始网址的代理网址. 9 | ## 模板方法 10 | ### request 11 | ```typescript 12 | interface TemplateRequest { 13 | header: object; // 请求头 14 | body: object; // 请求体,当Content-Type为x-www-form-urlencoded或json时存在 15 | } 16 | ``` 17 | ```TypeScript 18 | request?: (option: TemplateRequest) => TemplateRequest 19 | ``` 20 | 说明: 请求原始网址时调用. 21 | ### response 22 | ```typescript 23 | interface TemplateResponse { 24 | header: object; // 响应头 25 | body: Buffer; // 响应体 26 | } 27 | ``` 28 | ```TypeScript 29 | response?: (option: TemplateResponse) => Promise 30 | ``` 31 | 说明: 原始网址响应时调用,此方法会阻塞. 32 | ## 示例 33 | ```TypeScript 34 | import type {Template} from '../util/model'; 35 | import {generateProxyUrl} from '../util/template'; 36 | 37 | let template: Template = { 38 | async response (option) { 39 | let body = option.body.toString(); 40 | body = body.replaceAll('Product', 'Easy-Reverse-Proxy'); 41 | let avatarUrl = 'https://avatars.githubusercontent.com/u/56395004?v=4'; 42 | body = body.replaceAll(avatarUrl, generateProxyUrl(avatarUrl)); 43 | option.body = Buffer.from(body); 44 | return option; 45 | } 46 | }; 47 | export default template; 48 | ``` 49 | 1. Product替换为Easy-Reverse-Proxy. 50 | 2. 生成 https://avatars.githubusercontent.com/u/56395004?v=4 的代理网址并替换. -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import type {Request, Response, NextFunction} from 'express'; 3 | import {fixRequestBody, responseInterceptor, createProxyMiddleware} from 'http-proxy-middleware'; 4 | import bodyParser from 'body-parser'; 5 | 6 | import {PROXIES, PORT} from './config'; 7 | import type {Middleware, MiddlewareConfig} from './util/model'; 8 | import {generateProxyUrl} from './util/template'; 9 | import {ExceptionResponse, ExceptionResponseCode, GenerateResponse} from './util/core'; 10 | 11 | let APP = express(); 12 | 13 | let MIDDLEWARES = PROXIES.filter((proxy): boolean => {return proxy.enable;}).map((proxy): Middleware => { 14 | let config: MiddlewareConfig = { 15 | target: proxy.url, 16 | changeOrigin: true, 17 | on: { 18 | proxyReq: fixRequestBody 19 | } 20 | }; 21 | 22 | if (proxy.template) { 23 | if (proxy.template.request) { 24 | config.on.proxyReq = (targetRequest, request: Request): void => { 25 | let oldHeader = {}; 26 | for (let headerName in targetRequest.getHeaders()) { 27 | oldHeader[headerName] = targetRequest.getHeader(headerName); 28 | targetRequest.removeHeader(headerName); 29 | } 30 | 31 | let result = proxy.template.request({header: oldHeader, body: request.body}); 32 | for (let headerName in result.header) { 33 | targetRequest.setHeader(headerName, result.header[headerName]); 34 | } 35 | request.body = result.body; 36 | fixRequestBody(targetRequest, request); 37 | } 38 | } 39 | if (proxy.template.response) { 40 | config.on.proxyRes = responseInterceptor(async (targetResponseBuffer, targetResponse, request, response): Promise => { 41 | let oldHeader = {}; 42 | for (let headerName in response.getHeaders()) { 43 | oldHeader[headerName] = response.getHeader(headerName); 44 | response.removeHeader(headerName); 45 | } 46 | 47 | let result = await proxy.template.response({header: oldHeader, body: targetResponseBuffer}); 48 | for (let headerName in result.header) { 49 | response.setHeader(headerName, result.header[headerName]); 50 | } 51 | return result.body; 52 | }); 53 | config.selfHandleResponse = true; 54 | } 55 | } 56 | 57 | return { 58 | domain: proxy.domain, 59 | middleware: createProxyMiddleware(config) 60 | } 61 | }); 62 | 63 | APP.use('/:sign(.+_[0-9a-zA-Z]{64})', (request, response, next): void | Promise => { 64 | let sign = request.params.sign; 65 | let url = atob(sign.split('_')[0]); 66 | 67 | if (generateProxyUrl(url) !== sign) { 68 | return next(); 69 | } 70 | 71 | return createProxyMiddleware({ 72 | target: url, 73 | changeOrigin: true 74 | })(request, response, next); 75 | }); 76 | 77 | APP.use(bodyParser.json(), bodyParser.urlencoded({extended: true}), (request, response, next): Promise => { 78 | let domain = request.hostname; 79 | 80 | let proxy = PROXIES.find((proxy): boolean => {return proxy.domain === domain;}); 81 | if (!proxy) { 82 | throw new ExceptionResponse(ExceptionResponseCode.SYSTEM, '代理不存在'); 83 | } 84 | 85 | if (proxy.enable) { 86 | return MIDDLEWARES.find((middleware): boolean => {return middleware.domain === proxy.domain;}).middleware(request, response, next); 87 | } else { 88 | throw new ExceptionResponse(ExceptionResponseCode.SYSTEM, '代理已关闭'); 89 | } 90 | }); 91 | 92 | APP.use((error: Error, request: Request, response: Response, next: NextFunction): void => { 93 | if (error instanceof ExceptionResponse) { 94 | return GenerateResponse.error(error.code, error.message)(response); 95 | } 96 | 97 | return GenerateResponse.error(500, '未知错误', 500)(response); 98 | }); 99 | 100 | APP.listen(PORT); --------------------------------------------------------------------------------