diff --git a/node-proxy/app.ts b/node-proxy/app.ts index fac3b85..12e87ee 100644 --- a/node-proxy/app.ts +++ b/node-proxy/app.ts @@ -2,7 +2,6 @@ import path from 'path' import http from 'http' import Koa from 'koa' -import serve from 'koa-static' import { port } from '@/config' import { logger } from '@/common/logger' @@ -11,13 +10,12 @@ import otherRouter from '@/router/other' import webdavRouter from '@/router/webdav' import staticRouter from '@/router/static' import webuiRouter from '@/router/webui' -import { exceptionMiddleware, loggerMiddleware } from '@/middleware/common' +import { exceptionMiddleware, loggerMiddleware } from './src/utils/middlewares' const app = new Koa() app.use(loggerMiddleware) app.use(exceptionMiddleware) -app.use(serve(path.dirname(process.argv[1]), { root: 'public' })) app.use(alisRouter.routes()) app.use(webuiRouter.routes()) app.use(webdavRouter.routes()) diff --git a/node-proxy/src/dao/fileDao.ts b/node-proxy/src/dao/fileDao.ts index c0ed0f3..cf53c27 100644 --- a/node-proxy/src/dao/fileDao.ts +++ b/node-proxy/src/dao/fileDao.ts @@ -26,5 +26,5 @@ export async function getFileInfo(path: string): Promise { + await this.datastore.removeMany({ key }, {}) + } } const nedb = new Nedb() diff --git a/node-proxy/src/middleware/common.ts b/node-proxy/src/middleware/common.ts deleted file mode 100644 index 08dcb89..0000000 --- a/node-proxy/src/middleware/common.ts +++ /dev/null @@ -1,212 +0,0 @@ -import path from 'path' -import type { Transform } from 'stream' - -import bodyparser from 'koa-bodyparser' -import type { Middleware, Next, ParameterizedContext } from 'koa' - -import FlowEnc from '@/utils/flowEnc' -import { flat } from '@/utils/common' -import { logger } from '@/common/logger' -import { httpFlowClient } from '@/utils/httpClient' -import { getWebdavFileInfo } from '@/utils/webdavClient' -import { cacheFileInfo, getFileInfo } from '@/dao/fileDao' -import { encodeName, pathFindPasswd } from '@/utils/cryptoUtil' - -export const compose = (...middlewares: Middleware[]): Middleware => { - if (!Array.isArray(middlewares)) throw new TypeError('Middleware stack must be an array!') - for (const fn of middlewares) { - if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!') - } - - // 最后一个中间件需要调用 next() 来确保响应可以结束 - return function (context, next) { - // 创建一个新的函数,这个函数会依次调用中间件 - let index = -1 - - function dispatch(i: number) { - if (i <= index) return Promise.reject(new Error('next() called multiple times')) - index = i - - let fn = middlewares[i] - if (i === middlewares.length) fn = next // 如果没有更多中间件,则调用原始的 next() - - if (!fn) return Promise.resolve() - - try { - return Promise.resolve(fn(context, dispatch.bind(null, i + 1))) - } catch (err) { - return Promise.reject(err) - } - } - - return dispatch(0) // 开始执行第一个中间件 - } -} - -export const emptyMiddleware: Middleware = async (_, next) => { - await next() -} - -export const loggerMiddleware: Middleware = async (ctx, next) => { - const reqPath = decodeURIComponent(ctx.path) - - let show = true - if (reqPath.startsWith('/public')) { - show = false - } - - if (['.html', '.js', '.css'].includes(path.extname(reqPath))) { - show = false - } - - if (show) { - logger.info(`-------------------开始 ${reqPath}-------------------`) - } - - await next() - - if (show) { - logger.info(`-------------------结束 ${reqPath}-------------------`) - } -} - -export const bodyParserMiddleware = bodyparser({ enableTypes: ['json', 'form', 'text'] }) - -export const exceptionMiddleware: Middleware = async ( - ctx: ParameterizedContext< - EmptyObj, - EmptyObj, - { - code: number - success: boolean - message: string - } - >, - next: Next -) => { - try { - await next() - if (ctx.status === 404) { - if (ctx.state?.isWebdav) { - logger.warn(ctx.state?.urlAddr, '404') - } else { - ctx.throw(404, '请求资源未找到!') - } - } - } catch (err) { - logger.error(err) - - const status = err.status || 500 - // 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息 - const error = status === 500 && process.env.RUN_MODE === 'DEV' ? err.message : 'Internal Server Error' - // 从 error 对象上读出各个属性,设置到响应中 - ctx.body = { - success: false, - message: error, - code: status, // 服务端自身的处理逻辑错误(包含框架错误500 及 自定义业务逻辑错误533开始 ) 客户端请求参数导致的错误(4xx开始),设置不同的状态码 - } - // 406 是能让用户看到的错误,参数校验失败也不能让用户看到(一般不存在参数校验失败) - if (status === 403 || status === 406) { - ctx.body.message = error - } - ctx.status = 200 - } -} - -export const proxyHandler: Middleware = async (ctx: ParameterizedContext>) => { - const state = ctx.state - const { method, headers } = ctx.req - // 要定位请求文件的位置 bytes=98304- - const range = headers.range - const start = range ? Number(range.replace('bytes=', '').split('-')[0]) : 0 - const urlPath = new URL(state.urlAddr).pathname - // 检查路径是否满足加密要求,要拦截的路径可能有中文 - const { passwdInfo } = pathFindPasswd(state.serverConfig.passwdList, decodeURIComponent(urlPath)) - - logger.info('匹配密码信息', passwdInfo === null ? '无密码' : passwdInfo.password) - - let encryptTransform: Transform, decryptTransform: Transform - // fix webdav move file - if (method.toLocaleUpperCase() === 'MOVE' && headers.destination) { - let destination = flat(headers.destination) - destination = state.serverAddr + destination.substring(destination.indexOf(path.dirname(urlPath)), destination.length) - headers.destination = destination - } - - // 如果是上传文件,那么进行流加密,目前只支持webdav上传,如果alist页面有上传功能,那么也可以兼容进来 - if (method.toLocaleUpperCase() === 'PUT' && passwdInfo) { - // 兼容macos的webdav客户端x-expected-entity-length - // 需要知道文件长度,等于0 说明不用加密,这个来自webdav奇怪的请求 - if (ctx.state.fileSize !== 0) { - encryptTransform = new FlowEnc(passwdInfo.password, passwdInfo.encType, ctx.state.fileSize).encryptTransform() - } - } - - // 如果是下载文件,那么就进行判断是否解密 - if ('GET,HEAD,POST'.includes(method.toLocaleUpperCase()) && passwdInfo) { - // 根据文件路径来获取文件的大小 - let filePath = ctx.req.url.split('?')[0] - // 如果是alist的话,那么必然有这个文件的size缓存(进过list就会被缓存起来) - state.fileSize = 0 - // 这里需要处理掉/p 路径 - if (filePath.indexOf('/p/') === 0) { - filePath = filePath.replace('/p/', '/') - } - if (filePath.indexOf('/d/') === 0) { - filePath = filePath.replace('/d/', '/') - } - // 尝试获取文件信息,如果未找到相应的文件信息,则对文件名进行加密处理后重新尝试获取文件信息 - let fileInfo = await getFileInfo(filePath) - - if (fileInfo === null) { - const rawFileName = decodeURIComponent(path.basename(filePath)) - const ext = path.extname(rawFileName) - const encodedRawFileName = encodeURIComponent(rawFileName) - const encFileName = encodeName(passwdInfo.password, passwdInfo.encType, rawFileName) - const newFileName = encFileName + ext - - filePath = filePath.replace(encodedRawFileName, newFileName) - state.urlAddr = state.urlAddr.replace(encodedRawFileName, newFileName) - - fileInfo = await getFileInfo(filePath) - } - - logger.info('获取文件信息:', filePath, JSON.stringify(fileInfo)) - - if (fileInfo) { - state.fileSize = fileInfo.size - } else if (headers.authorization) { - // 这里要判断是否webdav进行请求, 这里默认就是webdav请求了 - const authorization = headers.authorization - const webdavFileInfo = await getWebdavFileInfo(state.urlAddr, authorization) - logger.info('@@webdavFileInfo:', filePath, webdavFileInfo) - if (webdavFileInfo) { - webdavFileInfo.path = filePath - // 某些get请求返回的size=0,不要缓存起来 - if (webdavFileInfo.size > 0) { - await cacheFileInfo(webdavFileInfo) - } - state.fileSize = webdavFileInfo.size - } - } - - // logger.info('@@@@request.filePath ', request.filePath, result) - if (state.fileSize !== 0) { - const flowEnc = new FlowEnc(passwdInfo.password, passwdInfo.encType, state.fileSize) - if (start) { - await flowEnc.setPosition(start) - } - decryptTransform = flowEnc.decryptTransform() - } - } - - await httpFlowClient({ - urlAddr: state.urlAddr, - passwdInfo, - fileSize: state.fileSize, - request: ctx.req, - response: ctx.res, - encryptTransform, - decryptTransform, - }) -} diff --git a/node-proxy/src/router/alist/index.ts b/node-proxy/src/router/alist/index.ts index 1470abb..bb2d6ef 100644 --- a/node-proxy/src/router/alist/index.ts +++ b/node-proxy/src/router/alist/index.ts @@ -12,7 +12,7 @@ import { alistServer } from '@/config' import { copyOrMoveFileMiddleware } from '@/router/alist/utils' import { cacheFileInfo, getFileInfo } from '@/dao/fileDao' import { httpClient, httpFlowClient } from '@/utils/httpClient' -import { bodyParserMiddleware, emptyMiddleware } from '@/middleware/common' +import { bodyParserMiddleware, emptyMiddleware } from '@/utils/middlewares' import { convertRealName, convertShowName, encodeName, pathFindPasswd } from '@/utils/cryptoUtil' import type { alist } from '@/@types/alist' diff --git a/node-proxy/src/router/other/index.ts b/node-proxy/src/router/other/index.ts index 09feae7..2045bc3 100644 --- a/node-proxy/src/router/other/index.ts +++ b/node-proxy/src/router/other/index.ts @@ -1,27 +1,20 @@ import Router from 'koa-router' -import { flat, preProxy } from '@/utils/common' -import { alistServer, version } from '@/config' -import levelDB from '@/dao/levelDB' + +import nedb from '@/dao/levelDB' import FlowEnc from '@/utils/flowEnc' +import { logger } from '@/common/logger' import { pathExec } from '@/utils/cryptoUtil' +import { flat, preProxy } from '@/utils/common' +import { alistServer, version } from '@/config' import { httpClient, httpFlowClient } from '@/utils/httpClient' -import { logger } from '@/common/logger' -import { bodyParserMiddleware, compose, proxyHandler } from '@/middleware/common' -import { downloadMiddleware } from '@/router/other/utils' +import { bodyParserMiddleware, compose } from '@/utils/middlewares' +import { downloadMiddleware } from '@/router/other/middlewares' const otherRouter = new Router() -otherRouter.get, EmptyObj>( - /^\/d\/*/, - compose(bodyParserMiddleware, preProxy(alistServer, false), downloadMiddleware), - proxyHandler -) +otherRouter.get, EmptyObj>(/^\/d\/*/, compose(bodyParserMiddleware, preProxy(alistServer, false)), downloadMiddleware) -otherRouter.get, EmptyObj>( - /^\/p\/*/, - compose(bodyParserMiddleware, preProxy(alistServer, false), downloadMiddleware), - proxyHandler -) +otherRouter.get, EmptyObj>(/^\/p\/*/, compose(bodyParserMiddleware, preProxy(alistServer, false)), downloadMiddleware) // 修复alist 图标不显示的问题 otherRouter.all(/^\/images\/*/, compose(bodyParserMiddleware, preProxy(alistServer, false)), async (ctx) => { @@ -41,7 +34,7 @@ otherRouter.all('/redirect/:key', async (ctx) => { const request = ctx.req const response = ctx.res // 这里还是要encodeURIComponent ,因为http服务器会自动对url进行decodeURIComponent - const data = await levelDB.getValue(ctx.params.key) + const data = await nedb.getValue(ctx.params.key) if (data === null) { ctx.body = 'no found' diff --git a/node-proxy/src/router/other/middlewares.ts b/node-proxy/src/router/other/middlewares.ts new file mode 100644 index 0000000..fd3c0fc --- /dev/null +++ b/node-proxy/src/router/other/middlewares.ts @@ -0,0 +1,85 @@ +import path from 'path' +import type { Transform } from 'stream' + +import type { Middleware, ParameterizedContext } from 'koa' + +import FlowEnc from '@/utils/flowEnc' +import { logger } from '@/common/logger' +import { getFileInfo } from '@/dao/fileDao' +import { httpFlowClient } from '@/utils/httpClient' +import { convertRealName, encodeName, pathFindPasswd } from '@/utils/cryptoUtil' + +export const downloadMiddleware: Middleware = async (ctx: ParameterizedContext>) => { + const state = ctx.state + let urlAddr = state.urlAddr + let urlPath = new URL(urlAddr).pathname + // 如果是alist的话,那么必然有这个文件的size缓存(进过list就会被缓存起来) + let fileSize = 0 + + // 这里需要处理掉/p 路径 + if (urlPath.indexOf('/d/') === 0) { + urlPath = urlPath.replace('/d/', '/') + } + + // 这个不需要处理 + if (urlPath.indexOf('/p/') === 0) { + urlPath = urlPath.replace('/p/', '/') + } + + // 要定位请求文件的位置 bytes=98304- + const range = ctx.req.headers.range + const start = range ? Number(range.replace('bytes=', '').split('-')[0]) : 0 + const { passwdInfo } = pathFindPasswd(state.serverConfig.passwdList, urlPath) + + logger.info('匹配密码信息', passwdInfo === null ? '无密码' : passwdInfo.password) + + let decryptTransform: Transform + + // 如果是下载文件,那么就进行判断是否解密 + if (passwdInfo && passwdInfo.encName) { + delete ctx.req.headers['content-length'] + // check fileName is not enc or it is dir + const fileName = path.basename(urlPath) + const realName = convertRealName(passwdInfo.password, passwdInfo.encType, fileName) + logger.info(`转换为原始文件名: ${fileName} -> ${realName}`) + + urlPath = path.dirname(urlPath) + '/' + realName + urlAddr = path.dirname(urlAddr) + '/' + realName + logger.info('准备获取文件', urlAddr) + + // 尝试获取文件信息,如果未找到相应的文件信息,则对文件名进行加密处理后重新尝试获取文件信息 + let fileInfo = await getFileInfo(urlPath) + + if (fileInfo === null) { + const rawFileName = decodeURIComponent(path.basename(urlPath)) + const ext = path.extname(rawFileName) + const encodedRawFileName = encodeURIComponent(rawFileName) + const encFileName = encodeName(passwdInfo.password, passwdInfo.encType, rawFileName) + const newFileName = encFileName + ext + const newFilePath = urlPath.replace(encodedRawFileName, newFileName) + + urlAddr = urlAddr.replace(encodedRawFileName, newFileName) + fileInfo = await getFileInfo(newFilePath) + } + + logger.info('获取文件信息:', urlPath, JSON.stringify(fileInfo)) + + if (fileInfo) fileSize = fileInfo.size + + if (fileSize !== 0) { + const flowEnc = new FlowEnc(passwdInfo.password, passwdInfo.encType, fileSize) + if (start) await flowEnc.setPosition(start) + decryptTransform = flowEnc.decryptTransform() + } + } + + await httpFlowClient({ + urlAddr, + passwdInfo, + fileSize, + request: ctx.req, + response: ctx.res, + encryptTransform: undefined, + decryptTransform, + }) +} diff --git a/node-proxy/src/router/other/utils.ts b/node-proxy/src/router/other/utils.ts deleted file mode 100644 index 4812eaa..0000000 --- a/node-proxy/src/router/other/utils.ts +++ /dev/null @@ -1,42 +0,0 @@ -import path from 'path' - -import type { Middleware } from 'koa' - -import { logger } from '@/common/logger' -import { convertRealName, pathFindPasswd } from '@/utils/cryptoUtil' - -export const downloadMiddleware: Middleware> = async (ctx, next) => { - const state = ctx.state - - let filePath = ctx.req.url.split('?')[0] - // 如果是alist的话,那么必然有这个文件的size缓存(进过list就会被缓存起来) - state.fileSize = 0 - // 这里需要处理掉/p 路径 - if (filePath.indexOf('/d/') === 0) { - filePath = filePath.replace('/d/', '/') - } - - // 这个不需要处理 - if (filePath.indexOf('/p/') === 0) { - filePath = filePath.replace('/p/', '/') - } - - const { passwdInfo } = pathFindPasswd(state.serverConfig.passwdList, filePath) - if (passwdInfo && passwdInfo.encName) { - // reset content-length length - delete ctx.req.headers['content-length'] - // check fileName is not enc or it is dir - const fileName = path.basename(filePath) - const realName = convertRealName(passwdInfo.password, passwdInfo.encType, fileName) - logger.info(`转换为原始文件名: ${fileName} -> ${realName}`) - - ctx.req.url = path.dirname(ctx.req.url) + '/' + realName - ctx.state.urlAddr = path.dirname(ctx.state.urlAddr) + '/' + realName - logger.info('准备获取文件', ctx.req.url) - await next() - - return - } - - await next() -} diff --git a/node-proxy/src/router/static/index.ts b/node-proxy/src/router/static/index.ts index 2f52954..d22c2e1 100644 --- a/node-proxy/src/router/static/index.ts +++ b/node-proxy/src/router/static/index.ts @@ -1,7 +1,15 @@ +import path from 'path' + +import serve from 'koa-static' import Router from 'koa-router' -const staticRouter = new Router({ prefix: '/index' }) +const staticServer = serve(path.join(path.dirname(process.argv[1]), 'public')) +const staticRouter = new Router() -staticRouter.redirect('/', '/public/index.html', 302) +staticRouter.redirect('/index', '/public/index.html', 302) +staticRouter.all(/\/public\/*/, async (ctx, next) => { + ctx.path = ctx.path.replace('/public', '') + await staticServer(ctx, next) +}) export default staticRouter diff --git a/node-proxy/src/router/webdav/middlewares.ts b/node-proxy/src/router/webdav/middlewares.ts index 4578507..e2cb1a1 100644 --- a/node-proxy/src/router/webdav/middlewares.ts +++ b/node-proxy/src/router/webdav/middlewares.ts @@ -6,11 +6,10 @@ import type { Context, Middleware } from 'koa' import FlowEnc from '@/utils/flowEnc' import { flat } from '@/utils/common' import { logger } from '@/common/logger' -import { getWebdavFileInfo } from '@/utils/webdavClient' import { httpClient, httpFlowClient } from '@/utils/httpClient' import { cacheFileInfo, deleteFileInfo, getFileInfo } from '@/dao/fileDao' import { convertRealName, convertShowName, pathFindPasswd } from '@/utils/cryptoUtil' -import { cacheWebdavFileInfo, getFileNameForShow } from '@/router/webdav/utils' +import { cacheWebdavFileInfo, getFileNameForShow, getWebdavFileInfo } from '@/router/webdav/utils' import { webdav } from '@/@types/webdav' const parser = new XMLParser({ removeNSPrefix: true }) @@ -191,7 +190,7 @@ const copyOrMoveHook = async (ctx: Context, state: ProxiedState> => { + logger.info(`webdav获取文件信息: ${decodeURIComponent(urlAddr)}`) + + let result = '' + + return new Promise((resolve, reject) => { + const request = (~urlAddr.indexOf('https') ? https : http).request( + urlAddr, + { + method: 'PROPFIND', + headers: { + depth: '1', + authorization, + }, + agent: ~urlAddr.indexOf('https') ? httpsAgent : httpAgent, + rejectUnauthorized: false, + }, + async (message) => { + logger.info('webdav接收文件信息') + + if (message.statusCode === 404) { + reject('404') + } + + message.on('data', (chunk) => { + result += chunk + }) + + message.on('end', () => { + resolve(parser.parse(result)) + }) + } + ) + + request.end() + }) +} + +// get file info from webdav +export async function getWebdavFileInfo(urlAddr: string, authorization: string): Promise { + try { + const respBody = await webdavPropfind(urlAddr, authorization) + const res = respBody.multistatus.response + + const size = res.propstat.prop.getcontentlength + const name = res.propstat.prop.displayname + + return { size, name, is_dir: size === undefined, path: res.href } + } catch (e) { + if (e === '404') return null + } +} export const cacheWebdavFileInfo = async (fileInfo: webdav.FileInfo) => { let contentLength = -1 diff --git a/node-proxy/src/router/webui/index.ts b/node-proxy/src/router/webui/index.ts index 84f18fd..3685c40 100644 --- a/node-proxy/src/router/webui/index.ts +++ b/node-proxy/src/router/webui/index.ts @@ -7,7 +7,7 @@ import { logger } from '@/common/logger' import { userInfoMiddleware } from '@/router/webui/middlewares' import { encryptFile, searchFile } from '@/utils/convertFile' import { encodeFolderName, decodeFolderName } from '@/utils/cryptoUtil' -import { emptyMiddleware, bodyParserMiddleware } from '@/middleware/common' +import { emptyMiddleware, bodyParserMiddleware } from '@/utils/middlewares' import { RawPasswdInfo, response, splitEncPath } from '@/router/webui/utils' import { getUserInfo, cacheUserToken, updateUserInfo } from '@/dao/userDao' import { alistServer, webdavServer, port, initAlistConfig, version } from '@/config' diff --git a/node-proxy/src/utils/convertFile.ts b/node-proxy/src/utils/convertFile.ts index 8604b23..99b6da1 100644 --- a/node-proxy/src/utils/convertFile.ts +++ b/node-proxy/src/utils/convertFile.ts @@ -2,9 +2,9 @@ import fs from 'fs' import path from 'path' import mkdirp from 'mkdirp' -import FlowEnc from './flowEnc' +import FlowEnc from '@/utils/flowEnc' import { logger } from '@/common/logger' -import { encodeName, decodeName } from './cryptoUtil' +import { encodeName, decodeName } from '@/utils/cryptoUtil' //找出文件夹下的文件 export function searchFile(filePath: string) { diff --git a/node-proxy/src/utils/cryptoUtil.ts b/node-proxy/src/utils/cryptoUtil.ts index e32c1b2..7e9c08a 100644 --- a/node-proxy/src/utils/cryptoUtil.ts +++ b/node-proxy/src/utils/cryptoUtil.ts @@ -2,10 +2,10 @@ import path from 'path' import { pathToRegexp } from 'path-to-regexp' -import Crcn from './crypto/crc6-8' -import MixBase64 from './crypto/mixBase64' +import Crcn from '@/utils/crypto/crc6-8' +import MixBase64 from '@/utils/crypto/mixBase64' import { logger } from '@/common/logger' -import { getPassWdOutward } from './flowEnc' +import { getPassWdOutward } from '@/utils/flowEnc' const crc6 = new Crcn(6) const origPrefix = 'orig_' diff --git a/node-proxy/src/utils/httpClient.ts b/node-proxy/src/utils/httpClient.ts index fa902cc..8c2eec6 100644 --- a/node-proxy/src/utils/httpClient.ts +++ b/node-proxy/src/utils/httpClient.ts @@ -4,8 +4,8 @@ import https from 'node:https' import crypto from 'crypto' import type { Transform } from 'stream' -import { flat } from '@/utils/common' import nedb from '@/dao/levelDB' +import { flat } from '@/utils/common' import { logger } from '@/common/logger' import { decodeName } from '@/utils/cryptoUtil' @@ -105,7 +105,7 @@ export async function httpClient({ reqBody?: string request: http.IncomingMessage response: http.ServerResponse - onMessage?: typeof defaultOnMessageCallback | null + onMessage?: typeof defaultOnMessageCallback }): Promise { //如果传入ctx.res,那么当代理请求返回响应头时,ctx.res也会立即返回响应头。若不传入则当代理请求完全完成时再手动处理ctx.res const { method, headers } = request @@ -126,7 +126,7 @@ export async function httpClient({ logger.info(`代理http响应 接收: 状态码 ${message.statusCode}`) //默认的回调函数,设置response的响应码和响应头 - onMessage && (await onMessage({ urlAddr, reqBody, request, response, message })) + await onMessage({ urlAddr, reqBody, request, response, message }) message.on('data', (chunk) => { result += chunk diff --git a/node-proxy/src/utils/middlewares.ts b/node-proxy/src/utils/middlewares.ts new file mode 100644 index 0000000..539f4bb --- /dev/null +++ b/node-proxy/src/utils/middlewares.ts @@ -0,0 +1,107 @@ +import path from 'path' + +import bodyparser from 'koa-bodyparser' +import type { Middleware, Next, ParameterizedContext } from 'koa' + +import { logger } from '@/common/logger' + +export const compose = (...middlewares: Middleware[]): Middleware => { + if (!Array.isArray(middlewares)) throw new TypeError('Middleware stack must be an array!') + for (const fn of middlewares) { + if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!') + } + + // 最后一个中间件需要调用 next() 来确保响应可以结束 + return function (context, next) { + // 创建一个新的函数,这个函数会依次调用中间件 + let index = -1 + + function dispatch(i: number) { + if (i <= index) return Promise.reject(new Error('next() called multiple times')) + index = i + + let fn = middlewares[i] + if (i === middlewares.length) fn = next // 如果没有更多中间件,则调用原始的 next() + + if (!fn) return Promise.resolve() + + try { + return Promise.resolve(fn(context, dispatch.bind(null, i + 1))) + } catch (err) { + return Promise.reject(err) + } + } + + return dispatch(0) // 开始执行第一个中间件 + } +} + +export const emptyMiddleware: Middleware = async (_, next) => { + await next() +} + +export const loggerMiddleware: Middleware = async (ctx, next) => { + const reqPath = decodeURIComponent(ctx.path) + + let show = true + if (reqPath.startsWith('/public')) { + show = false + } + + if (['.html', '.js', '.css', '.json'].includes(path.extname(reqPath))) { + show = false + } + + if (show) { + logger.info(`-------------------开始 ${reqPath}-------------------`) + } + + await next() + + if (show) { + logger.info(`-------------------结束 ${reqPath}-------------------`) + } +} + +export const bodyParserMiddleware = bodyparser({ enableTypes: ['json', 'form', 'text'] }) + +export const exceptionMiddleware: Middleware = async ( + ctx: ParameterizedContext< + EmptyObj, + EmptyObj, + { + code: number + success: boolean + message: string + } + >, + next: Next +) => { + try { + await next() + if (ctx.status === 404) { + if (ctx.state?.isWebdav) { + logger.warn(ctx.state?.urlAddr, '404') + } else { + ctx.throw(404, '请求资源未找到!') + } + } + } catch (err) { + logger.error(err) + + const status = err.status || 500 + // 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息 + const error = status === 500 && process.env.RUN_MODE === 'DEV' ? err.message : 'Internal Server Error' + // 从 error 对象上读出各个属性,设置到响应中 + ctx.body = { + success: false, + message: error, + code: status, // 服务端自身的处理逻辑错误(包含框架错误500 及 自定义业务逻辑错误533开始 ) 客户端请求参数导致的错误(4xx开始),设置不同的状态码 + } + // 406 是能让用户看到的错误,参数校验失败也不能让用户看到(一般不存在参数校验失败) + if (status === 403 || status === 406) { + ctx.body.message = error + } + ctx.status = 200 + } +} diff --git a/node-proxy/src/utils/webdavClient.ts b/node-proxy/src/utils/webdavClient.ts deleted file mode 100644 index 69b4a4e..0000000 --- a/node-proxy/src/utils/webdavClient.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { XMLParser } from 'fast-xml-parser' - -import https from 'node:https' -import http from 'http' -import { webdav } from '@/@types/webdav' -import { logger } from '@/common/logger' - -const httpsAgent = new https.Agent({ keepAlive: true }) -const httpAgent = new http.Agent({ keepAlive: true }) -const parser = new XMLParser({ removeNSPrefix: true }) - -const webdavPropfind = async (urlAddr: string, authorization: string): Promise> => { - logger.info(`webdav获取文件信息: ${decodeURIComponent(urlAddr)}`) - - let result = '' - - return new Promise((resolve, reject) => { - const request = (~urlAddr.indexOf('https') ? https : http).request( - urlAddr, - { - method: 'PROPFIND', - headers: { - depth: '1', - authorization, - }, - agent: ~urlAddr.indexOf('https') ? httpsAgent : httpAgent, - rejectUnauthorized: false, - }, - async (message) => { - logger.info('webdav接收文件信息') - - if (message.statusCode === 404) { - reject('404') - } - - message.on('data', (chunk) => { - result += chunk - }) - - message.on('end', () => { - resolve(parser.parse(result)) - }) - } - ) - - request.end() - }) -} - -// get file info from webdav -export async function getWebdavFileInfo(urlAddr: string, authorization: string): Promise { - try { - const respBody = await webdavPropfind(urlAddr, authorization) - const res = respBody.multistatus.response - - const size = res.propstat.prop.getcontentlength - const name = res.propstat.prop.displayname - - return { size, name, is_dir: size === undefined, path: res.href } - } catch (e) { - if (e === '404') return null - } -}