import { NativeRequest } from "../../server-engine/native/request" import { ENGINE_TYPE } from "../../utils/engine" import { MiddlewareCreater } from "../interface" import { ProxyItemRendered } from "./interface" import * as http from 'node:http' import * as https from 'node:https' import { renderItem } from "./renderItem" import { commonWriteHeaders, getHttpHeaders } from "../../utils/resp" import { logger, safeJsonParse, toBuffer } from "../../utils" import * as fs from 'node:fs' import * as path from 'node:path' const middleware_proxy: MiddlewareCreater = { name: 'proxy', mode: ['dev', 'prod'], execute: (conf) => { const { proxies = [] } = conf if (proxies.length === 0) { return } const items: ProxyItemRendered[] = proxies.map(renderItem) return { beforeRoute(pathname, ctx) { const { req, resp } = ctx const url = req.getUrl() const search = req.getQuery() const item = items.find(item => { return item.match(url, ctx) }) if (!item) { return } const origin = item.getOrigin(url, ctx) const { timeout = 5000, requestOptions = {}, requestHeaders = h => h, responseHeaders = h => h, responseRender = h => h, } = item const newPath = new URL( typeof item.pathname === 'function' ? url.replace(item.location, item.pathname) : url.replace(item.location, item.pathname || (typeof item.location === 'string' ? item.location : '')), origin) if (search) { newPath.search = search } const isStream = item.stream || false const saver = item.saver const _pathname = saver?.pathFixer ? saver.pathFixer(pathname, ctx) : pathname const dataPath = saver?.pathBodyDir ? path.join(saver.pathBodyDir, _pathname) : null if (saver && dataPath) { if (!fs.existsSync(saver.pathBodyDir)) { fs.mkdirSync(saver.pathBodyDir, { recursive: true }) } if (!fs.existsSync(saver.pathHeaders)) { fs.mkdirSync(path.dirname(saver.pathHeaders), { recursive: true }) fs.writeFileSync(saver.pathHeaders, JSON.stringify({})) } const proxyHeadersMap = safeJsonParse(fs.readFileSync(saver.pathHeaders, 'utf-8')) || {} if (fs.existsSync(dataPath)) { const stats = fs.statSync(dataPath) if (stats.isFile()) { resp.cork(() => { resp.writeStatus('200 OK') commonWriteHeaders(resp, proxyHeadersMap[_pathname] || {}) resp.write(fs.readFileSync(dataPath)) resp.end() }) return false } } } const isSSL = /^https/i.test(newPath.protocol) const httpAgent = new (isSSL ? https : http).Agent({ keepAlive: true, maxSockets: 50, maxFreeSockets: 10, timeout: 60000, }) try { const creq = (isSSL ? https : http).request(newPath, { agent: httpAgent, method: req.getMethod(), headers: { ...requestHeaders(getHttpHeaders(req), ctx), host: newPath.host, }, timeout: isStream ? 86400000 : timeout, ...requestOptions, }, function (res) { resp.cork(() => { resp.writeStatus(res.statusCode + ' ' + res.statusMessage) commonWriteHeaders(resp, responseHeaders(getHttpHeaders(res), res, ctx)) }) const chunks: Buffer[] = [] res .on('data', function (data) { if (isStream) { resp.cork(() => { resp.write(data) }) } else { chunks.push(data) } }).on('end', function () { if (isStream) { resp.end() return } const result = responseRender(Buffer.concat(chunks), res, ctx) const headers = responseHeaders(getHttpHeaders(res), res, ctx) if (saver && dataPath) { const proxyHeadersMap = safeJsonParse(fs.readFileSync(saver.pathHeaders, 'utf-8')) || {} proxyHeadersMap[_pathname] = headers fs.writeFileSync(saver.pathHeaders, JSON.stringify(proxyHeadersMap, null, 2)) if (!fs.existsSync(dataPath)) { fs.mkdirSync(path.dirname(dataPath), { recursive: true }) fs.writeFileSync(dataPath, result) } else if (!fs.statSync(dataPath).isDirectory()) { fs.writeFileSync(dataPath, result) } } resp.cork(() => { resp.write(result) resp.end() }) }) }).on('timeout', function () { resp.cork(() => { resp.writeStatus('504 Gateway Timeout') commonWriteHeaders(resp, {}) resp.end('504 Gateway Timeout') }) }).on('error', function (err) { logger.error(`[proxy error]`, newPath, err) }) switch (ENGINE_TYPE) { case "node": case "bun": case "deno": (req as NativeRequest).request.pipe(creq) break; case "uws": resp.onData(function (data, finished) { data && data.byteLength > 0 && creq.write(toBuffer(data)) if (finished) { creq.end() } }) resp.onAborted(() => { creq.destroy() }) } } catch (e) { resp.cork(() => { resp.writeStatus('502 Bad Gateway') commonWriteHeaders(resp, {}) resp.end(e + '') }) } return false }, } } } export default middleware_proxy