diff --git a/node-proxy/.env b/node-proxy/.env index 923c1f0..fed1294 100644 --- a/node-proxy/.env +++ b/node-proxy/.env @@ -1 +1,2 @@ -RUN_MODE=DEV \ No newline at end of file +RUN_MODE=DEV +LOG_LEVEL=debug diff --git a/node-proxy/.eslintrc.js b/node-proxy/.eslintrc.js index f7e1a0d..589fbd0 100644 --- a/node-proxy/.eslintrc.js +++ b/node-proxy/.eslintrc.js @@ -23,11 +23,10 @@ module.exports = { function prettierrc() { const prettierrc_id = require.resolve('./.prettierrc') - var stat = fs.statSync(prettierrc_id) + const stat = fs.statSync(prettierrc_id) if (stat.mtimeMs > (process.prettierrc_file_mtimeMs || 0)) { process.prettierrc_file_mtimeMs = stat.mtimeMs require.cache[prettierrc_id] = undefined } - const conf = require('./.prettierrc') - return conf + return require('./.prettierrc') } diff --git a/node-proxy/app.js b/node-proxy/app.js deleted file mode 100644 index cf2bf9b..0000000 --- a/node-proxy/app.js +++ /dev/null @@ -1,344 +0,0 @@ -'use strict' -import { convertFile } from '@/utils/convertFile' -const arg = process.argv.slice(2) -if (arg.length > 1) { - // convertFile command - convertFile(...arg) - return -} - -import Koa from 'koa' -import Router from 'koa-router' -import http from 'http' -import crypto from 'crypto' -import path from 'path' -import { httpProxy, httpClient } from '@/utils/httpClient' -import bodyparser from 'koa-bodyparser' -import FlowEnc from '@/utils/flowEnc' -import levelDB from '@/utils/levelDB' -import { webdavServer, alistServer, port, version } from '@/config' -import { pathExec, pathFindPasswd } from '@/utils/commonUtil' -import globalHandle from '@/middleware/globalHandle' -import encApiRouter from '@/router' -import encNameRouter from '@/encNameRouter' -import encDavHandle from '@/encDavHandle' - -import { cacheFileInfo, getFileInfo } from '@/dao/fileDao' -import { getWebdavFileInfo } from '@/utils/webdavClient' -import staticServer from 'koa-static' -import { logger } from '@/common/logger' -import { encodeName } from '@/utils/commonUtil' - -async function sleep(time) { - return new Promise((resolve) => { - setTimeout(() => { - resolve() - }, time || 3000) - }) -} - -const proxyRouter = new Router() -const app = new Koa() -// compatible ncc and pkg -const pkgDirPath = path.dirname(process.argv[1]) - -app.use(staticServer(pkgDirPath, 'public')) -app.use(globalHandle) -// bodyparser解析body -const bodyparserMw = bodyparser({ enableTypes: ['json', 'form', 'text'] }) - -// ======================/proxy是实现本服务的业务============================== -// 短地址 -encApiRouter.redirect('/index', '/public/index.html', 302) -app.use(encApiRouter.routes()).use(encApiRouter.allowedMethods()) - -// ======================下面是实现webdav代理的业务============================== - -// 可能是302跳转过来的下载的,/redirect?key=34233&decode=0 -proxyRouter.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) - if (data === null) { - ctx.body = 'no found' - return - } - const { passwdInfo, redirectUrl, fileSize } = data - // 要定位请求文件的位置 bytes=98304- - const range = request.headers.range - const start = range ? range.replace('bytes=', '').split('-')[0] * 1 : 0 - const flowEnc = new FlowEnc(passwdInfo.password, passwdInfo.encType, fileSize) - if (start) { - await flowEnc.setPosition(start) - } - // 设置请求地址和是否要解密 - const decode = ctx.query.decode - // 修改百度头 - if (~redirectUrl.indexOf('baidupcs.com')) { - request.headers['User-Agent'] = 'pan.baidu.com' - } - request.url = decodeURIComponent(ctx.query.lastUrl) - request.urlAddr = redirectUrl - delete request.headers.host - // aliyun不允许这个referer,不然会出现403 - delete request.headers.referer - request.passwdInfo = passwdInfo - // 123网盘和天翼网盘多次302 - request.fileSize = fileSize - // authorization 是alist网页版的token,不是webdav的,这里修复天翼云无法获取资源的问题 - delete request.headers.authorization - - // 默认判断路径来识别是否要解密,如果有decode参数,那么则按decode来处理,这样可以让用户手动处理是否解密?(那还不如直接在alist下载) - let decryptTransform = passwdInfo.enable && pathExec(passwdInfo.encPath, request.url) ? flowEnc.decryptTransform() : null - if (decode) { - decryptTransform = decode !== '0' ? flowEnc.decryptTransform() : null - } - // 请求实际服务资源 - await httpProxy(request, response, null, decryptTransform) - logger.info('----finish redirect---', decode, request.urlAddr, decryptTransform === null) -}) - -// 预处理 request,处理地址,加密钥匙等 -function preProxy(webdavConfig, isWebdav) { - // 必包变量 - // let authorization = isWebdav - return async (ctx, next) => { - const { serverHost, serverPort, https } = webdavConfig - const request = ctx.req - if (isWebdav) { - // 不能把authorization缓存起来,单线程 - request.isWebdav = isWebdav - // request.headers.authorization = request.headers.authorization ? (authorization = request.headers.authorization) : authorization - } - // 原来的host保留,以后可能会用到 - request.selfHost = request.headers.host - request.origin = request.headers.origin - request.headers.host = serverHost + ':' + serverPort - const protocol = https ? 'https' : 'http' - request.urlAddr = `${protocol}://${request.headers.host}${request.url}` - request.serverAddr = `${protocol}://${request.headers.host}` - request.webdavConfig = webdavConfig - await next() - } -} -// webdav or http handle -async function proxyHandle(ctx, next) { - const request = ctx.req - const response = ctx.res - const { passwdList } = request.webdavConfig - const { headers } = request - // 要定位请求文件的位置 bytes=98304- - const range = headers.range - const start = range ? range.replace('bytes=', '').split('-')[0] * 1 : 0 - // 检查路径是否满足加密要求,要拦截的路径可能有中文 - const { passwdInfo, pathInfo } = pathFindPasswd(passwdList, decodeURIComponent(request.url)) - logger.debug('@@@@passwdInfo', pathInfo) - // fix webdav move file - if (request.method.toLocaleUpperCase() === 'MOVE' && headers.destination) { - let destination = headers.destination - destination = request.serverAddr + destination.substring(destination.indexOf(path.dirname(request.url)), destination.length) - request.headers.destination = destination - } - // 如果是上传文件,那么进行流加密,目前只支持webdav上传,如果alist页面有上传功能,那么也可以兼容进来 - if (request.method.toLocaleUpperCase() === 'PUT' && passwdInfo) { - // 兼容macos的webdav客户端x-expected-entity-length - const contentLength = headers['content-length'] || headers['x-expected-entity-length'] || 0 - request.fileSize = contentLength * 1 - // 需要知道文件长度,等于0 说明不用加密,这个来自webdav奇怪的请求 - if (request.fileSize === 0) { - return await httpProxy(request, response) - } - const flowEnc = new FlowEnc(passwdInfo.password, passwdInfo.encType, request.fileSize) - return await httpProxy(request, response, flowEnc.encryptTransform()) - } - // 如果是下载文件,那么就进行判断是否解密 - if ('GET,HEAD,POST'.includes(request.method.toLocaleUpperCase()) && passwdInfo) { - // 根据文件路径来获取文件的大小 - const urlPath = ctx.req.url.split('?')[0] - let filePath = urlPath - // 如果是alist的话,那么必然有这个文件的size缓存(进过list就会被缓存起来) - request.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); - request.urlAddr = request.urlAddr.replace(encodedRawFileName, newFileName); - - fileInfo = await getFileInfo(filePath); - } - logger.info('@@getFileInfo:', filePath, fileInfo, request.urlAddr) - if (fileInfo) { - request.fileSize = fileInfo.size * 1 - } else if (request.headers.authorization) { - // 这里要判断是否webdav进行请求, 这里默认就是webdav请求了 - const authorization = request.headers.authorization - const webdavFileInfo = await getWebdavFileInfo(request.urlAddr, authorization) - logger.info('@@webdavFileInfo:', filePath, webdavFileInfo) - if (webdavFileInfo) { - webdavFileInfo.path = filePath - // 某些get请求返回的size=0,不要缓存起来 - if (webdavFileInfo.size * 1 > 0) { - cacheFileInfo(webdavFileInfo) - } - request.fileSize = webdavFileInfo.size * 1 - } - } - request.passwdInfo = passwdInfo - // logger.info('@@@@request.filePath ', request.filePath, result) - if (request.fileSize === 0) { - // 说明不用加密 - return await httpProxy(request, response) - } - const flowEnc = new FlowEnc(passwdInfo.password, passwdInfo.encType, request.fileSize) - if (start) { - await flowEnc.setPosition(start) - } - return await httpProxy(request, response, null, flowEnc.decryptTransform()) - } - await httpProxy(request, response) -} - -// 初始化webdav路由,这里可以优化成动态路由,只不过没啥必要,修改配置后直接重启就好了 -webdavServer.forEach((webdavConfig) => { - if (webdavConfig.enable) { - proxyRouter.all(new RegExp(webdavConfig.path), preProxy(webdavConfig, true), encDavHandle, proxyHandle) - } -}) - -/* =================================== 单独处理alist的逻辑 ====================================== */ - -// 单独处理alist的所有/dav -proxyRouter.all(/^\/dav\/*/, preProxy(alistServer, true), encDavHandle, proxyHandle) - -// 其他的代理request预处理,处理要跳转的路径等 -proxyRouter.all(/\/*/, preProxy(alistServer, false)) -// check enc filename -proxyRouter.use(encNameRouter.routes()).use(encNameRouter.allowedMethods()) - -// 处理文件下载的302跳转 -proxyRouter.get(/^\/d\/*/, proxyHandle) -// 文件直接下载 -proxyRouter.get(/^\/p\/*/, proxyHandle) - -// 处理在线视频播放的问题,修改它的返回播放地址 为本代理的地址。 -proxyRouter.all('/api/fs/get', bodyparserMw, async (ctx, next) => { - const { path } = ctx.request.body - // 判断打开的文件是否要解密,要解密则替换url,否则透传 - ctx.req.reqBody = JSON.stringify(ctx.request.body) - - const respBody = await httpClient(ctx.req) - const result = JSON.parse(respBody) - const { headers } = ctx.req - const { passwdInfo } = pathFindPasswd(alistServer.passwdList, path) - - if (passwdInfo) { - // 修改返回的响应,匹配到要解密,就302跳转到本服务上进行代理流量 - logger.info('@@getFile ', path, ctx.req.reqBody, result) - const key = crypto.randomUUID() - await levelDB.setExpire(key, { redirectUrl: result.data.raw_url, passwdInfo, fileSize: result.data.size }, 60 * 60 * 72) // 缓存起来,默认3天,足够下载和观看了 - result.data.raw_url = `${ - headers.origin || (headers['x-forwarded-proto'] || ctx.protocol) + '://' + ctx.req.selfHost - }/redirect/${key}?decode=1&lastUrl=${encodeURIComponent(path)}` - if (result.data.provider === 'AliyundriveOpen') result.data.provider = 'Local' - } - ctx.body = result -}) - -// 缓存alist的文件信息 -proxyRouter.all('/api/fs/list', bodyparserMw, async (ctx, next) => { - const { path } = ctx.request.body - // 判断打开的文件是否要解密,要解密则替换url,否则透传 - ctx.req.reqBody = JSON.stringify(ctx.request.body) - const respBody = await httpClient(ctx.req) - // logger.info('@@@respBody', respBody) - const result = JSON.parse(respBody) - if (!result.data) { - ctx.body = result - return - } - const content = result.data.content - if (!content) { - ctx.body = result - return - } - for (let i = 0; i < content.length; i++) { - const fileInfo = content[i] - fileInfo.path = path + '/' + fileInfo.name - // 这里要注意闭包问题,mad - // logger.debug('@@cacheFileInfo', fileInfo.path) - cacheFileInfo(fileInfo) - } - // waiting cacheFileInfo a moment - if (content.length > 100) { - await sleep(50) - } - logger.info('@@@fs/list', content.length) - ctx.body = result -}) - -// that is not work when upload txt file if enable encName -proxyRouter.put('/api/fs/put-back', async (ctx, next) => { - const request = ctx.req - const { headers, webdavConfig } = request - const contentLength = headers['content-length'] || 0 - request.fileSize = contentLength * 1 - - const uploadPath = headers['file-path'] ? decodeURIComponent(headers['file-path']) : '/-' - const { passwdInfo } = pathFindPasswd(webdavConfig.passwdList, uploadPath) - if (passwdInfo) { - const flowEnc = new FlowEnc(passwdInfo.password, passwdInfo.encType, request.fileSize) - return await httpProxy(ctx.req, ctx.res, flowEnc.encryptTransform()) - } - return await httpProxy(ctx.req, ctx.res) -}) - -// 修复alist 图标不显示的问题 -proxyRouter.all(/^\/images\/*/, async (ctx, next) => { - delete ctx.req.headers.host - return await httpProxy(ctx.req, ctx.res) -}) - -// 初始化alist的路由 -proxyRouter.all(new RegExp(alistServer.path), async (ctx, next) => { - let respBody = await httpClient(ctx.req, ctx.res) - respBody = respBody.replace( - '', - ` -
- -
- -
- V.${version} -
-
-
-
` - ) - ctx.body = respBody -}) -// 使用路由控制 -app.use(proxyRouter.routes()).use(proxyRouter.allowedMethods()) - -// 配置创建好了,就启动 else { -const server = http.createServer(app.callback()) -server.maxConnections = 1000 -server.listen(port, () => logger.info('服务启动成功: ' + port)) -setInterval(() => { - logger.debug('server_connections', server._connections, Date.now()) -}, 600 * 1000) diff --git a/node-proxy/app.ts b/node-proxy/app.ts new file mode 100644 index 0000000..388f408 --- /dev/null +++ b/node-proxy/app.ts @@ -0,0 +1,32 @@ +import path from 'path' +import http from 'http' + +import Koa from 'koa' +import serve from 'koa-static' + +import { logger } from '@/common/logger' +import alisRouter from '@/router/alist' +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' + +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()) +app.use(staticRouter.routes()) +app.use(otherRouter.routes()) + +const server = http.createServer(app.callback()) +server.maxConnections = 1000 +server.listen(5343, () => logger.info('服务启动成功: ' + 5343)) + +setInterval(() => { + logger.debug('server_connections', server.connections, Date.now()) +}, 600 * 1000) diff --git a/node-proxy/btest.js b/node-proxy/btest.js deleted file mode 100644 index 6d40b63..0000000 --- a/node-proxy/btest.js +++ /dev/null @@ -1,34 +0,0 @@ -import crypto from 'crypto' -import path from 'path' -import { logger } from '@/common/logger' -import ChaCha20Poly from '@/utils/chaCha20Poly' -import { chownSync, copyFileSync } from 'fs' -import CRCN from '@/utils/crc6-8' -import fs from 'fs' -import { encodeName, decodeName } from '@/utils/commonUtil' -import { getWebdavFileInfo } from '@/utils/webdavClient' - -getWebdavFileInfo( - 'http://192.168.8.240:5244/dav/aliyun%E4%BA%91%E7%9B%98/atest/d%E5%AF%B9%E6%96%B9%E6%88%91testrclone/kline_d%2Bata12342%E6%AD%A3%E6%96%87%E7%9A%84%E7%9A%84%E5%89%AF%E6%9C%AC.txt', - 'Basic YWRtaW46WWl1Tkg3bHk=' -).then((res) => { - console.log(res) -}) - -console.log('@@dd', path.isAbsolute('/ddf')) -const content = 'fileInfoTable_/dav/aliyun%Evfnnz%BA%91%E7%9B%98/atest/12%E5%A4%A7%E5%A4%B4%E7%9A%84%E6%97%8F%E6%96%87%E4%BB%B6_8Xn78oZjs7VSr~qjdzVH4/4' - -console.log('@@content', decodeURIComponent(content)) -const reg = 'test' - -const enw = content.replace(new RegExp(reg, 'g'), '@@') -console.log(enw) - -const ext = ''.trim() || path.extname('/dfdf.df') - -const encname = encodeName('123456', 'aesctr', '3wd.tex') - -const decname = decodeName('123456', 'aesctr', encname) -console.log('##', ext, decname) - -logger.debug('dfeeeef') diff --git a/node-proxy/package-lock.json b/node-proxy/package-lock.json index 104d790..850b1ec 100644 --- a/node-proxy/package-lock.json +++ b/node-proxy/package-lock.json @@ -23,6 +23,9 @@ "typescript": "^5.1.6" }, "devDependencies": { + "@types/koa-bodyparser": "^4.3.12", + "@types/koa-router": "^7.4.8", + "@types/koa-static": "^4.0.4", "@typescript-eslint/eslint-plugin": "^5.55.0", "@typescript-eslint/parser": "^5.55.0", "copy-webpack-plugin": "^11.0.0", @@ -359,6 +362,52 @@ "resolved": "https://registry.npmmirror.com/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" }, + "node_modules/@types/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmmirror.com/@types/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmmirror.com/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmmirror.com/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/content-disposition": { + "version": "0.5.8", + "resolved": "https://registry.npmmirror.com/@types/content-disposition/-/content-disposition-0.5.8.tgz", + "integrity": "sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==", + "dev": true + }, + "node_modules/@types/cookies": { + "version": "0.9.0", + "resolved": "https://registry.npmmirror.com/@types/cookies/-/cookies-0.9.0.tgz", + "integrity": "sha512-40Zk8qR147RABiQ7NQnBzWzDcjKzNrntB5BAmeGCb2p/MIyOE+4BVvc17wumsUqUw00bJYqoXFHYygQnEFh4/Q==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/express": "*", + "@types/keygrip": "*", + "@types/node": "*" + } + }, "node_modules/@types/eslint": { "version": "8.44.2", "resolved": "https://registry.npmmirror.com/@types/eslint/-/eslint-8.44.2.tgz", @@ -385,23 +434,166 @@ "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", "dev": true }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.5", + "resolved": "https://registry.npmmirror.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", + "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-assert": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@types/http-assert/-/http-assert-1.5.5.tgz", + "integrity": "sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g==", + "dev": true + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, "node_modules/@types/json-schema": { "version": "7.0.12", "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.12.tgz", "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", "dev": true }, + "node_modules/@types/keygrip": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/@types/keygrip/-/keygrip-1.0.6.tgz", + "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==", + "dev": true + }, + "node_modules/@types/koa": { + "version": "2.15.0", + "resolved": "https://registry.npmmirror.com/@types/koa/-/koa-2.15.0.tgz", + "integrity": "sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==", + "dev": true, + "dependencies": { + "@types/accepts": "*", + "@types/content-disposition": "*", + "@types/cookies": "*", + "@types/http-assert": "*", + "@types/http-errors": "*", + "@types/keygrip": "*", + "@types/koa-compose": "*", + "@types/node": "*" + } + }, + "node_modules/@types/koa-bodyparser": { + "version": "4.3.12", + "resolved": "https://registry.npmmirror.com/@types/koa-bodyparser/-/koa-bodyparser-4.3.12.tgz", + "integrity": "sha512-hKMmRMVP889gPIdLZmmtou/BijaU1tHPyMNmcK7FAHAdATnRcGQQy78EqTTxLH1D4FTsrxIzklAQCso9oGoebQ==", + "dev": true, + "dependencies": { + "@types/koa": "*" + } + }, + "node_modules/@types/koa-compose": { + "version": "3.2.8", + "resolved": "https://registry.npmmirror.com/@types/koa-compose/-/koa-compose-3.2.8.tgz", + "integrity": "sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA==", + "dev": true, + "dependencies": { + "@types/koa": "*" + } + }, + "node_modules/@types/koa-router": { + "version": "7.4.8", + "resolved": "https://registry.npmmirror.com/@types/koa-router/-/koa-router-7.4.8.tgz", + "integrity": "sha512-SkWlv4F9f+l3WqYNQHnWjYnyTxYthqt8W9az2RTdQW7Ay8bc00iRZcrb8MC75iEfPqnGcg2csEl8tTG1NQPD4A==", + "dev": true, + "dependencies": { + "@types/koa": "*" + } + }, + "node_modules/@types/koa-send": { + "version": "4.1.6", + "resolved": "https://registry.npmmirror.com/@types/koa-send/-/koa-send-4.1.6.tgz", + "integrity": "sha512-vgnNGoOJkx7FrF0Jl6rbK1f8bBecqAchKpXtKuXzqIEdXTDO6dsSTjr+eZ5m7ltSjH4K/E7auNJEQCAd0McUPA==", + "dev": true, + "dependencies": { + "@types/koa": "*" + } + }, + "node_modules/@types/koa-static": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/@types/koa-static/-/koa-static-4.0.4.tgz", + "integrity": "sha512-j1AUzzl7eJYEk9g01hNTlhmipFh8RFbOQmaMNLvLcNNAkPw0bdTs3XTa3V045XFlrWN0QYnblbDJv2RzawTn6A==", + "dev": true, + "dependencies": { + "@types/koa": "*", + "@types/koa-send": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmmirror.com/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, "node_modules/@types/node": { "version": "20.5.6", "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.5.6.tgz", "integrity": "sha512-Gi5wRGPbbyOTX+4Y2iULQ27oUPrefaB0PxGQJnfyWN3kvEDGM3mIB5M/gQLmitZf7A9FmLeaqxD3L1CXpm3VKQ==" }, + "node_modules/@types/qs": { + "version": "6.9.15", + "resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.9.15.tgz", + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, "node_modules/@types/semver": { "version": "7.5.0", "resolved": "https://registry.npmmirror.com/@types/semver/-/semver-7.5.0.tgz", "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", "dev": true }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmmirror.com/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmmirror.com/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/@types/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/@types/strip-bom/-/strip-bom-3.0.0.tgz", diff --git a/node-proxy/package.json b/node-proxy/package.json index 0fa8065..bb67455 100644 --- a/node-proxy/package.json +++ b/node-proxy/package.json @@ -2,10 +2,10 @@ "name": "alist-encrypt", "version": "1.0.0", "description": "", - "main": "app.js", + "main": "app.ts", "scripts": { - "dev": "ts-node-dev -r tsconfig-paths/register app.js", - "serve": "ts-node -r tsconfig-paths/register app.js", + "dev": "ts-node-dev -r tsconfig-paths/register app.ts", + "serve": "ts-node -r tsconfig-paths/register app.ts", "test": "echo \"Error: no test specified\" && exit 1", "webpack": "npx webpack", "build": "npm run webpack && pkg --compress GZip dist", @@ -31,6 +31,9 @@ "typescript": "^5.1.6" }, "devDependencies": { + "@types/koa-bodyparser": "^4.3.12", + "@types/koa-router": "^7.4.8", + "@types/koa-static": "^4.0.4", "@typescript-eslint/eslint-plugin": "^5.55.0", "@typescript-eslint/parser": "^5.55.0", "copy-webpack-plugin": "^11.0.0", diff --git a/node-proxy/src/@types/alist.d.ts b/node-proxy/src/@types/alist.d.ts new file mode 100644 index 0000000..7a68152 --- /dev/null +++ b/node-proxy/src/@types/alist.d.ts @@ -0,0 +1,87 @@ +declare namespace alist { + //alist中的文件类型 + export enum FileType { + UNKNOWN, + FOLDER, + // OFFICE, + VIDEO, + AUDIO, + TEXT, + IMAGE, + } + + //alist中的文件属性 + export interface FileInfo { + name: string + size: number + is_dir: boolean + modified: string + created: string + sign: string + thumb: string + type: FileType + hash_info: string | null + + path?: string //非原生属性,由proxy添加 + } + + //alist中的 /api/fs/get 的请求体 + type FsGetRequestBody = { + path: string + password: string + } + + //alist中的 /api/fs/list 的请求体 + type FsListRequestBody = { + path: string + password: string + page: number + per_page: number + refresh: boolean + } + + //alist中的 /api/fs/remove 的请求体 + type FsRemoveRequestBody = { + dir: string + names: string[] + } + + //alist中的 /api/fs/move 的请求体 + type FsMoveRequestBody = { + src_dir: string + dst_dir: string + names: string[] + } + + //alist中的 /api/fs/copy 的请求体 + type FsCopyRequestBody = { + src_dir: string + dst_dir: string + names: string[] + } + + //alist中的 /api/fs/rename 的请求体 + type FsRenameRequestBody = { + path: string + name: string + } + + //alist中的响应结构 + export interface Resp { + code: number + message: string + data: T + } + + //alist中的 /api/fs/list 的响应体 + type FsListResponseBody = Resp<{ + content: FileInfo[] | null + total: number + readme: string + header: string + write: boolean + provider: string + }> +} + +export { alist } diff --git a/node-proxy/src/@types/index.d.ts b/node-proxy/src/@types/index.d.ts index 0fd1847..bee15c1 100644 --- a/node-proxy/src/@types/index.d.ts +++ b/node-proxy/src/@types/index.d.ts @@ -1,9 +1,102 @@ +import { Transform } from 'stream' + export {} declare global { - interface PasswdInfo { - encPath: string - enable: string + //使得对象中的属性R的类型变为U + type ModifyPropType = { + [K in keyof T]: K extends R ? U : T[K] + } + + //用到的加解密类型 + type EncryptType = 'mix' | 'rc4' | 'aesctr' + + //具有加解密功能类的接口 + interface EncryptMethod { password: string - encType: string + passwdOutward: string + + encrypt: (arg1: Buffer) => Buffer + decrypt: (arg1: Buffer) => Buffer + } + + //具有流式解密功能类的接口 + interface EncryptFlow extends EncryptMethod { + encryptTransform: () => Transform + decryptTransform: () => Transform + setPositionAsync: (arg1: number) => Promise + } + + //webui中设置的密码信息 + type PasswdInfo = { + password: string + describe: string + enable: boolean + encType: EncryptType + encName: boolean + encSuffix: string + encPath: string[] + } + + //getWebdavFileInfo的返回值 + type WebdavFileInfo = { + size: number + name: string + is_dir: boolean + path: string + } + + //定义空对象,相当于{} + type EmptyObj = Record + + //经过bodyParserMiddleware处理后的ParsedContext,ctx.request.body变为对象 + type ParsedContext = { + request: { + body: T + } + } + + //webui中设置的单个webdav配置信息 + type WebdavServer = { + id: string + name: string + path: string + describe: string + serverHost: string + serverPort: number + https: boolean + enable: boolean + passwdList: PasswdInfo[] + } + + //webui中设置的alist配置信息 + type AlistServer = { + name: string + path: string + describe: string + serverHost: string + serverPort: number + https: boolean + passwdList: PasswdInfo[] + _snapshot?: { + name: string + path: string + describe: string + serverHost: string + serverPort: number + https: boolean + passwdList: PasswdInfo[] + } + } + + //preProxy后将以下属性添加到ctx.state中供middleware使用 + type ProxiedState = { + isWebdav: boolean + urlAddr: string + serverAddr: string + serverConfig: T + selfHost: string + origin: string + fileSize: number + passwdInfo?: PasswdInfo } } diff --git a/node-proxy/src/@types/webui.d.ts b/node-proxy/src/@types/webui.d.ts new file mode 100644 index 0000000..b08c702 --- /dev/null +++ b/node-proxy/src/@types/webui.d.ts @@ -0,0 +1,24 @@ +declare namespace webui { + //webui中登录后用户的信息 + type UserInfo = { + username: string + headImgUrl: string + password: string | null + roleId: string + } + + //通过userInfoMiddleware将用户信息添加到ctx.state中 + type State = { + userInfo: UserInfo + } + + //webui中响应结果 + type ResponseBody = { + flag: boolean //是否请求成功 + msg?: string //提示消息 + code: number //响应码 + data?: any //返回数据 + } +} + +export { webui } diff --git a/node-proxy/src/common/logger.js b/node-proxy/src/common/logger.ts similarity index 95% rename from node-proxy/src/common/logger.js rename to node-proxy/src/common/logger.ts index f2763ce..18ed792 100644 --- a/node-proxy/src/common/logger.js +++ b/node-proxy/src/common/logger.ts @@ -1,6 +1,7 @@ import log4js from 'log4js' import dotenv from 'dotenv' -dotenv.config('./env') + +dotenv.config() log4js.configure({ appenders: { diff --git a/node-proxy/src/config.js b/node-proxy/src/config.ts similarity index 73% rename from node-proxy/src/config.js rename to node-proxy/src/config.ts index 0f45c20..2a26dd1 100644 --- a/node-proxy/src/config.js +++ b/node-proxy/src/config.ts @@ -1,28 +1,24 @@ import fs from 'fs' -import { addUserInfo, getUserInfo } from './dao/userDao' -import nedb from './utils/levelDB' +import path from 'path' -// inti config, fix ncc get local conf -function getConfPath() { - return process.cwd() + '/conf' -} +import nedb from '@/dao/levelDB' +import { logger } from '@/common/logger' +import { addUserInfo, getUserInfo } from '@/dao/userDao' + +let serverHost = '192.168.1.100' +let serverPort = 5244 -// 初始化目录 -if (!fs.existsSync(getConfPath())) { - // fs.mkdirSync(path.resolve('conf')) - fs.mkdirSync(process.cwd() + '/conf') -} // 从环境变量上读取配置信息,docker首次启动时候可以直接进行配置 const serverAddr = process.env.ALIST_HOST -const serverHost = '192.168.1.100' -const serverPort = 5244 if (serverAddr && serverAddr.indexOf(':') > 6) { serverHost = serverAddr.split(':')[0] - serverPort = serverAddr.split(':')[1] + serverPort = Number(serverAddr.split(':')[1]) } -console.log('@@serverAddr:', serverAddr) + +logger.info(`Alist地址: ${serverHost}:${serverPort}`) + /** 全局代理alist,包括它的webdav和http服务,要配置上 */ -const alistServerTemp = { +const alistServerTemp: AlistServer = { name: 'alist', path: '/*', // 默认就是代理全部,保留字段 describe: 'alist 配置', @@ -33,8 +29,8 @@ const alistServerTemp = { { password: '123456', describe: 'my video', // 加密内容描述 - encType: 'aesctr', // 算法类型,可选mix,rc4,默认aesctr enable: true, // enable encrypt + encType: 'aesctr', // 算法类型,可选mix,rc4,默认aesctr encName: false, // encrypt file name encSuffix: '', // encPath: ['encrypt_folder/*', 'movie_encrypt/*'], // 路径支持正则表达式,常用的就是 尾巴带*,此目录的所文件都加密 @@ -43,7 +39,7 @@ const alistServerTemp = { } /** 支持其他普通的webdav,当然也可以挂载alist的webdav,但是上面配置更加适合 */ -const webdavServerTemp = [ +const webdavServerTemp: WebdavServer[] = [ { id: 'abcdefg', name: 'other-webdav', @@ -56,11 +52,11 @@ const webdavServerTemp = [ passwdList: [ { password: '123456', - encType: 'aesctr', // 密码类型,mix:速度更快适合电视盒子之类,rc4: 更安全,速度比mix慢一点,几乎无感知。 describe: 'my video', enable: false, + encType: 'aesctr', // 密码类型,mix:速度更快适合电视盒子之类,rc4: 更安全,速度比mix慢一点,几乎无感知。 encName: false, // encrypt file name - encNameSuffix: '', // + encSuffix: '', // encPath: ['encrypt_folder/*', '/dav/189cloud/*'], // 子路径 }, ], @@ -68,19 +64,23 @@ const webdavServerTemp = [ ] // inti config, fix ncc get local conf -function getConfFilePath() { - return process.cwd() + '/conf/config.json' +const confPath = path.join(process.cwd(), 'conf') +const confFile = path.join(confPath, 'config.json') + +nedb.init(path.join(confPath, ``, 'nedb', 'datafile')) + +// 初始化目录 +if (!fs.existsSync(confPath)) { + fs.mkdirSync(confPath) } -const exist = fs.existsSync(getConfFilePath()) -if (!exist) { - // 把默认数据写入到config.json +if (!fs.existsSync(confFile)) { const configData = { alistServer: alistServerTemp, webdavServer: webdavServerTemp, port: 5344 } - fs.writeFileSync(getConfFilePath(), JSON.stringify(configData, '', '\t')) + fs.writeFileSync(confFile, JSON.stringify(configData, undefined, '\t')) } + // 读取配置文件 -const configJson = fs.readFileSync(getConfFilePath(), 'utf8') -const configData = JSON.parse(configJson) +const configData = JSON.parse(fs.readFileSync(confFile, 'utf8')) // 兼容之前的数据进来,保留2个版 if (configData.alistServer.flowPassword) { @@ -95,11 +95,11 @@ if (configData.alistServer.flowPassword) { delete alistServer.encryptType delete alistServer.encPath configData.webdavServer = webdavServerTemp - fs.writeFileSync(process.cwd() + '/conf/config.json', JSON.stringify(configData, '', '\t')) + fs.writeFileSync(confFile, JSON.stringify(configData, undefined, '\t')) } /** 初始化用户的数据库 */ -async function init() { +;(async () => { try { await nedb.load() let admin = await getUserInfo('admin') @@ -108,14 +108,14 @@ async function init() { admin = { username: 'admin', headImgUrl: '/public/logo.svg', password: '123456', roleId: '[13]' } await addUserInfo(admin) } - console.log('@@init', admin) + logger.info('管理员信息: ', admin) } catch (e) {} -} -init() +})().then() // 副本用于前端更新, Object.assign({}, configData.alistServer) 只有第一层的拷贝 configData.alistServer._snapshot = JSON.parse(JSON.stringify(configData.alistServer)) -export function initAlistConfig(alistServerConfig) { + +export function initAlistConfig(alistServerConfig: AlistServer) { // 初始化alist的路由,新增/d/* 路由 let downloads = [] for (const passwdData of alistServerConfig.passwdList) { @@ -130,6 +130,7 @@ export function initAlistConfig(alistServerConfig) { } return alistServerConfig } + /** 初始化alist的一些路径 */ initAlistConfig(configData.alistServer) @@ -138,8 +139,8 @@ export const port = configData.port || 5344 export const version = '0.3.0' -export const alistServer = configData.alistServer || alistServerTemp +export const alistServer: AlistServer = configData.alistServer || alistServerTemp -export const webdavServer = configData.webdavServer || webdavServerTemp +export const webdavServer: WebdavServer[] = configData.webdavServer || webdavServerTemp -console.log('configData ', configData) +logger.info('代理配置 ', configData) diff --git a/node-proxy/src/dao/configDao.js b/node-proxy/src/dao/configDao.js deleted file mode 100644 index a87d937..0000000 --- a/node-proxy/src/dao/configDao.js +++ /dev/null @@ -1,29 +0,0 @@ -import levelDB from '@/utils/levelDB' - -export const configTable = 'configTable' - -export async function initConfigTable() { -} -// alist配置 -const alistConfigKey = '_alist_config_key' -export async function updateAlistConfig(config) { - await levelDB.setValue(alistConfigKey, config) -} -export async function getAlistConfig() { - return await levelDB.getValue(alistConfigKey) -} - -// 缓存文件信息 -export async function addOrUpdateWebdav(config) { - const id = config.id - const value = await levelDB.getValue(configTable) - value[id] = config - await levelDB.setValue(configTable, value) -} - -export async function delWebdavConfig(config) { - const id = config.id - const value = await levelDB.getValue(configTable) - delete value[id] - await levelDB.setValue(configTable, value) -} diff --git a/node-proxy/src/dao/fileDao.js b/node-proxy/src/dao/fileDao.js deleted file mode 100644 index dbc546c..0000000 --- a/node-proxy/src/dao/fileDao.js +++ /dev/null @@ -1,30 +0,0 @@ -import levelDB from '@/utils/levelDB' -import crypto from 'crypto' - -export const fileInfoTable = 'fileInfoTable_' - -// 缓存多少分钟 -const cacheTime = 60 * 24 - -export async function initFileTable() {} - -// 缓存文件信息 -export async function cacheFileInfo(fileInfo) { - fileInfo.path = decodeURIComponent(fileInfo.path) - const pathKey = fileInfoTable + fileInfo.path - fileInfo.table = fileInfoTable - await levelDB.setExpire(pathKey, fileInfo, 1000 * 60 * cacheTime) -} - -// 获取文件信息,偶尔要清理一下缓存 -export async function getFileInfo(path) { - const pathKey = decodeURIComponent(fileInfoTable + path) - const value = await levelDB.getValue(pathKey) - return value -} - -// 获取文件信息 -export async function getAllFileInfo() { - const value = await levelDB.getValue({ table: fileInfoTable }) - return value -} diff --git a/node-proxy/src/dao/fileDao.ts b/node-proxy/src/dao/fileDao.ts new file mode 100644 index 0000000..8fddcfb --- /dev/null +++ b/node-proxy/src/dao/fileDao.ts @@ -0,0 +1,21 @@ +import nedb from '@/dao/levelDB' + +import type { alist } from '@/@types/alist' + +export const fileInfoTable = 'fileInfoTable_' + +// 缓存多少分钟 +const cacheTime = 60 * 24 + +// 缓存文件信息 +export async function cacheFileInfo(fileInfo: alist.FileInfo | WebdavFileInfo) { + fileInfo.path = decodeURIComponent(fileInfo.path) + const pathKey = fileInfoTable + fileInfo.path + await nedb.setExpire(pathKey, fileInfo, 1000 * 60 * cacheTime) +} + +// 获取文件信息,偶尔要清理一下缓存 +export async function getFileInfo(path: string) { + const pathKey = decodeURIComponent(fileInfoTable + path) + return await nedb.getValue(pathKey) +} diff --git a/node-proxy/src/dao/levelDB.ts b/node-proxy/src/dao/levelDB.ts new file mode 100644 index 0000000..7421859 --- /dev/null +++ b/node-proxy/src/dao/levelDB.ts @@ -0,0 +1,81 @@ +import Datastore from 'nedb-promises' + +import { logger } from '@/common/logger' + +type Value = any + +interface Document { + _id: string //NeDB 自动添加 _id + key: string + value: Value + expire?: number +} + +/** + * 封装新方法 + */ +class Nedb { + datastore: Datastore + + init(dbFile: string) { + this.datastore = Datastore.create(dbFile) + } + + async load() { + if (!this.datastore) { + logger.error('请先init Nedb') + } + + await this.datastore.load() + + setInterval(async () => { + const allData = await nedb.datastore.find({}) + for (const data of allData) { + const { key, expire } = data + if (expire && expire > 0 && expire < Date.now()) { + logger.info('删除过期键值', key, expire, Date.now()) + await nedb.datastore.remove({ key }, {}) + } + } + }, 30 * 1000) + } + + //存值,无过期时间 + async setValue(key: string, value: Value) { + await this.datastore.removeMany({ key }, {}) + logger.info('存储键值(无过期时间)', key, JSON.stringify(value)) + await this.datastore.insert({ key, expire: -1, value }) + } + + // 存值,有过期时间 + async setExpire(key: string, value: Value, second = 6 * 10) { + await this.datastore.removeMany({ key }, {}) + const expire = Date.now() + second * 1000 + logger.info(`存储键值(过期时间${expire})`, key, JSON.stringify(value)) + await this.datastore.insert({ key, expire, value }) + } + + // 取值 + async getValue(key: string): Promise { + try { + const { expire, value } = await this.datastore.findOne({ key }) + // 没有限制时间 + if (expire < 0) { + return value + } + + if (expire && expire > Date.now()) { + return value + } + + await this.datastore.remove({ key }, {}) + return null + } catch (e) { + return null + } + } +} + +const nedb = new Nedb() + +export default nedb diff --git a/node-proxy/src/dao/userDao.js b/node-proxy/src/dao/userDao.js deleted file mode 100644 index 5c77bbe..0000000 --- a/node-proxy/src/dao/userDao.js +++ /dev/null @@ -1,50 +0,0 @@ -import levelDB from '@/utils/levelDB' - -export const userTable = 'userTable' - -export async function initUserTable() { - // const value = await levelDB.getValue(userTable) - // if (value == null) { - // await levelDB.setValue(userTable, {}) - // } -} -// 获取用户信息 -export async function getUserInfo(username) { - const value = await levelDB.getValue(userTable) - if (value == null) { - return null - } - return value[username] -} - -// 获取用户信息 -export async function getUserByToken(token) { - return levelDB.getValue(token) -} -// 缓存用户信息 -export async function cacheUserToken(token, userInfo) { - const value = await levelDB.setExpire(token, userInfo, 60 * 60 * 24) - return value -} - -export async function addUserInfo(userInfo) { - const value = await levelDB.getValue(userTable) || {} - value[userInfo.username] = userInfo - await levelDB.setValue(userTable, value) -} - -export async function updateUserInfo(userInfo) { - const value = await levelDB.getValue(userTable) - if (value[userInfo.username]) { - value[userInfo.username] = userInfo - levelDB.setValue(userTable, value) - } -} - -export async function delectUserInfo(userInfo) { - const value = await levelDB.getValue(userTable) - if (value[userInfo.username]) { - delete value[userInfo.username] - await levelDB.setValue(userTable, value) - } -} diff --git a/node-proxy/src/dao/userDao.ts b/node-proxy/src/dao/userDao.ts new file mode 100644 index 0000000..78a8480 --- /dev/null +++ b/node-proxy/src/dao/userDao.ts @@ -0,0 +1,48 @@ +import nedb from '@/dao/levelDB' + +import type { webui } from '@/@types/webui' + +export const userTable = 'userTable' + +// 获取用户信息 +export async function getUserInfo(username: string): Promise { + const value = await nedb.getValue(userTable) + + if (value == null) { + return null + } + + return value[username] +} + +// 获取用户信息 +export async function getUserByToken(token: string): Promise { + return await nedb.getValue(token) +} + +// 缓存用户信息 +export async function cacheUserToken(token: string, userInfo: webui.UserInfo) { + return await nedb.setExpire(token, userInfo, 60 * 60 * 24) +} + +export async function addUserInfo(userInfo: webui.UserInfo) { + const value = (await nedb.getValue(userTable)) || {} + value[userInfo.username] = userInfo + await nedb.setValue(userTable, value) +} + +export async function updateUserInfo(userInfo: webui.UserInfo) { + const value = await nedb.getValue(userTable) + if (value[userInfo.username]) { + value[userInfo.username] = userInfo + await nedb.setValue(userTable, value) + } +} + +export async function deleteUserInfo(userInfo: webui.UserInfo) { + const value = await nedb.getValue(userTable) + if (value[userInfo.username]) { + delete value[userInfo.username] + await nedb.setValue(userTable, value) + } +} diff --git a/node-proxy/src/encNameRouter.js b/node-proxy/src/encNameRouter.js deleted file mode 100644 index 0a762b0..0000000 --- a/node-proxy/src/encNameRouter.js +++ /dev/null @@ -1,247 +0,0 @@ -'use strict' - -import Router from 'koa-router' -import bodyparser from 'koa-bodyparser' -import { encodeName, pathFindPasswd, convertShowName, convertRealName } from './utils/commonUtil' -import path from 'path' -import { httpClient, httpProxy } from './utils/httpClient' -import FlowEnc from './utils/flowEnc' -import { logger } from './common/logger' -import { getFileInfo } from './dao/fileDao' - -// bodyparser解析body -const bodyparserMw = bodyparser({ enableTypes: ['json', 'form', 'text'] }) - -const encNameRouter = new Router() -const origPrefix = 'orig_' - -// 拦截全部 -encNameRouter.all('/api/fs/list', async (ctx, next) => { - console.log('@@encrypt file name ', ctx.req.url) - await next() - const result = ctx.body - const { passwdList } = ctx.req.webdavConfig - if (result.code === 200 && result.data) { - const content = result.data.content - if (!content) { - return - } - for (let i = 0; i < content.length; i++) { - const fileInfo = content[i] - if (fileInfo.is_dir) { - continue - } - // Check path if the file name needs to be encrypted - const { passwdInfo } = pathFindPasswd(passwdList, decodeURI(fileInfo.path)) - if (passwdInfo && passwdInfo.encName) { - fileInfo.name = convertShowName(passwdInfo.password, passwdInfo.encType, fileInfo.name) - } - } - - const coverNameMap = {} //根据不含后缀的视频文件名找到对应的含后缀的封面文件名 - const omitNames = [] //用于隐藏封面文件 - const { path } = JSON.parse(ctx.req.reqBody) - result.data.content.forEach((fileInfo) => { - if (fileInfo.is_dir) { - return - } - if (fileInfo.type === 5) { - coverNameMap[fileInfo.name.split('.')[0]] = fileInfo.name - } - }) - result.data.content.forEach((fileInfo) => { - if (fileInfo.is_dir) { - return - } - const coverName = coverNameMap[fileInfo.name.split('.')[0]] - if (fileInfo.type === 2 && coverName) { - omitNames.push(coverName) - fileInfo.thumb = `/d${path}/${coverName}` - } - }) - //不展示封面文件,也许可以添加个配置让用户选择是否展示封面源文件 - result.data.content = result.data.content.filter((fileInfo) => !omitNames.includes(fileInfo.name)) - } -}) - -// 处理网页上传文件 -encNameRouter.put('/api/fs/put', async (ctx, next) => { - const request = ctx.req - const { headers, webdavConfig } = request - const contentLength = headers['content-length'] || 0 - request.fileSize = contentLength * 1 - - const uploadPath = headers['file-path'] ? decodeURIComponent(headers['file-path']) : '/-' - const { passwdInfo } = pathFindPasswd(webdavConfig.passwdList, uploadPath) - if (passwdInfo) { - const fileName = path.basename(uploadPath) - // you can custom Suffix - if (passwdInfo.encName) { - const ext = passwdInfo.encSuffix || path.extname(fileName) - const encName = encodeName(passwdInfo.password, passwdInfo.encType, fileName) - const filePath = path.dirname(uploadPath) + '/' + encName + ext - console.log('@@@encfileName', fileName, uploadPath, filePath) - headers['file-path'] = encodeURIComponent(filePath) - } - const flowEnc = new FlowEnc(passwdInfo.password, passwdInfo.encType, request.fileSize) - return await httpProxy(ctx.req, ctx.res, flowEnc.encryptTransform()) - } - return await httpProxy(ctx.req, ctx.res) -}) - -// remove -encNameRouter.all('/api/fs/remove', bodyparserMw, async (ctx, next) => { - const { dir, names } = ctx.request.body - const { webdavConfig } = ctx.req - const { passwdInfo } = pathFindPasswd(webdavConfig.passwdList, dir) - // maybe a folder,remove anyway the name - const fileNames = Object.assign([], names) - if (passwdInfo && passwdInfo.encName) { - for (const name of names) { - // is not enc name - const realName = convertRealName(passwdInfo.password, passwdInfo.encType, name) - fileNames.push(realName) - } - } - const reqBody = { dir, names: fileNames } - logger.info('@@reqBody remove', reqBody) - ctx.req.reqBody = JSON.stringify(reqBody) - // reset content-length length - delete ctx.req.headers['content-length'] - const respBody = await httpClient(ctx.req) - ctx.body = respBody -}) - -const copyOrMoveFile = async (ctx, next) => { - const { dst_dir: dstDir, src_dir: srcDir, names } = ctx.request.body - const { webdavConfig } = ctx.req - const { passwdInfo } = pathFindPasswd(webdavConfig.passwdList, srcDir) - let fileNames = [] - if (passwdInfo && passwdInfo.encName) { - logger.info('@@move encName', passwdInfo.encName) - for (const name of names) { - // is not enc name - if (name.indexOf(origPrefix) === 0) { - const origName = name.replace(origPrefix, '') - fileNames.push(origName) - break - } - const fileName = path.basename(name) - // you can custom Suffix - const ext = passwdInfo.encSuffix || path.extname(fileName) - const encName = encodeName(passwdInfo.password, passwdInfo.encType, fileName) - const newFileName = encName + ext - fileNames.push(newFileName) - } - } else { - fileNames = Object.assign([], names) - } - const reqBody = { dst_dir: dstDir, src_dir: srcDir, names: fileNames } - ctx.req.reqBody = JSON.stringify(reqBody) - logger.info('@@move reqBody', ctx.req.reqBody) - // reset content-length length - delete ctx.req.headers['content-length'] - const respBody = await httpClient(ctx.req) - ctx.body = respBody -} - -encNameRouter.all('/api/fs/move', bodyparserMw, copyOrMoveFile) -encNameRouter.all('/api/fs/copy', bodyparserMw, copyOrMoveFile) - -encNameRouter.all('/api/fs/get', bodyparserMw, async (ctx, next) => { - const { path: filePath } = ctx.request.body - const { webdavConfig } = ctx.req - const { passwdInfo } = pathFindPasswd(webdavConfig.passwdList, filePath) - if (passwdInfo && passwdInfo.encName) { - // reset content-length length - delete ctx.req.headers['content-length'] - // check fileName is not enc - const fileName = path.basename(filePath) - const fileInfo = await getFileInfo(encodeURIComponent(filePath)) - if (fileInfo && fileInfo.is_dir) { - await next() - return - } - // Check if it is a directory - const realName = convertRealName(passwdInfo.password, passwdInfo.encType, fileName) - const fpath = path.dirname(filePath) + '/' + realName - console.log('@@@getFilePath', fpath) - ctx.request.body.path = fpath - } - await next() - if (passwdInfo && passwdInfo.encName) { - // return showName - const showName = convertShowName(passwdInfo.password, passwdInfo.encType, ctx.body.data.name) - ctx.body.data.name = showName - } -}) - -encNameRouter.all('/api/fs/rename', bodyparserMw, async (ctx, next) => { - const { path: filePath, name } = ctx.request.body - const { webdavConfig } = ctx.req - const { passwdInfo } = pathFindPasswd(webdavConfig.passwdList, filePath) - const reqBody = { path: filePath, name } - ctx.req.reqBody = reqBody - // reset content-length length - delete ctx.req.headers['content-length'] - - let fileInfo = await getFileInfo(encodeURIComponent(filePath)) - if (fileInfo == null && passwdInfo && passwdInfo.encName) { - // mabay a file - const realName = convertRealName(passwdInfo.password, passwdInfo.encType, filePath) - const realFilePath = path.dirname(filePath) + '/' + realName - fileInfo = await getFileInfo(encodeURIComponent(realFilePath)) - } - if (passwdInfo && passwdInfo.encName && fileInfo && !fileInfo.is_dir) { - // reset content-length length - // you can custom Suffix - const ext = passwdInfo.encSuffix || path.extname(name) - const realName = convertRealName(passwdInfo.password, passwdInfo.encType, filePath) - const fpath = path.dirname(filePath) + '/' + realName - const newName = encodeName(passwdInfo.password, passwdInfo.encType, name) - reqBody.path = fpath - reqBody.name = newName + ext - } - ctx.req.reqBody = reqBody - console.log('@@@rename', reqBody) - const respBody = await httpClient(ctx.req) - ctx.body = respBody -}) - -const handleDownload = async (ctx, next) => { - const request = ctx.req - const { webdavConfig } = ctx.req - - const urlPath = ctx.req.url.split('?')[0] - let filePath = urlPath - // 如果是alist的话,那么必然有这个文件的size缓存(进过list就会被缓存起来) - request.fileSize = 0 - // 这里需要处理掉/p 路径 - if (filePath.indexOf('/d/') === 0) { - filePath = filePath.replace('/d/', '/') - } - // 这个不需要处理 - if (filePath.indexOf('/p/') === 0) { - filePath = filePath.replace('/p/', '/') - } - const { passwdInfo } = pathFindPasswd(webdavConfig.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) - ctx.req.url = path.dirname(ctx.req.url) + '/' + realName - ctx.req.urlAddr = path.dirname(ctx.req.urlAddr) + '/' + realName - logger.debug('@@@@download-fileName', ctx.req.url, fileName, realName) - await next() - return - } - await next() -} - -encNameRouter.get(/^\/d\/*/, bodyparserMw, handleDownload) -encNameRouter.get(/\/p\/*/, bodyparserMw, handleDownload) - -// restRouter.all(/\/enc-api\/*/, router.routes(), restRouter.allowedMethods()) -export default encNameRouter diff --git a/node-proxy/src/middleware/common.ts b/node-proxy/src/middleware/common.ts new file mode 100644 index 0000000..f802e0a --- /dev/null +++ b/node-proxy/src/middleware/common.ts @@ -0,0 +1,212 @@ +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) { + ctx.throw(404, '请求资源未找到!') + } + } catch (err) { + logger.error('@@err') + console.trace(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, req: request, res: response } = ctx + + const { headers } = request + // 要定位请求文件的位置 bytes=98304- + const range = headers.range + const start = range ? Number(range.replace('bytes=', '').split('-')[0]) : 0 + // 检查路径是否满足加密要求,要拦截的路径可能有中文 + const { passwdInfo } = pathFindPasswd(state.serverConfig.passwdList, decodeURIComponent(request.url)) + + logger.info('匹配密码信息', passwdInfo === null ? '无密码' : passwdInfo.password) + + let encryptTransform: Transform, decryptTransform: Transform + // fix webdav move file + if (request.method.toLocaleUpperCase() === 'MOVE' && headers.destination) { + let destination = flat(headers.destination) + destination = state.serverAddr + destination.substring(destination.indexOf(path.dirname(request.url)), destination.length) + request.headers.destination = destination + } + + // 如果是上传文件,那么进行流加密,目前只支持webdav上传,如果alist页面有上传功能,那么也可以兼容进来 + if (request.method.toLocaleUpperCase() === 'PUT' && passwdInfo) { + // 兼容macos的webdav客户端x-expected-entity-length + ctx.state.fileSize = Number(headers['content-length'] || flat(headers['x-expected-entity-length']) || 0) + // 需要知道文件长度,等于0 说明不用加密,这个来自webdav奇怪的请求 + if (ctx.state.fileSize !== 0) { + encryptTransform = new FlowEnc(passwdInfo.password, passwdInfo.encType, ctx.state.fileSize).encryptTransform() + } + } + + // 如果是下载文件,那么就进行判断是否解密 + if ('GET,HEAD,POST'.includes(request.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 (request.headers.authorization) { + // 这里要判断是否webdav进行请求, 这里默认就是webdav请求了 + const authorization = request.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 + } + } + + state.passwdInfo = passwdInfo + + // 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: state.passwdInfo, + fileSize: state.fileSize, + request, + response, + encryptTransform, + decryptTransform, + }) +} diff --git a/node-proxy/src/middleware/globalHandle.js b/node-proxy/src/middleware/globalHandle.js deleted file mode 100644 index c70c3c4..0000000 --- a/node-proxy/src/middleware/globalHandle.js +++ /dev/null @@ -1,32 +0,0 @@ -'use strict' - -export default async function (ctx, next) { - const env = 'dev' - try { - await next() - // 兼容webdav中401的时候,body = '' - if (!ctx.body) { - return - } - // 参数转换, 转换成自己的数据格式 - } catch (err) { - // 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志 - // app.emit('error', err, this); - const status = err.status || 500 - // 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息 - const error = status === 500 && env === 'prod' ? 'Internal Server Error' : err.message - console.error('@@err', err) - // 从 error 对象上读出各个属性,设置到响应中 - ctx.body = { - success: false, - message: error, - code: status, // 服务端自身的处理逻辑错误(包含框架错误500 及 自定义业务逻辑错误533开始 ) 客户端请求参数导致的错误(4xx开始),设置不同的状态码 - data: null, - } - // 406 是能让用户看到的错误,参数校验失败也不能让用户看到(一般不存在参数校验失败) - if (status === '403' || status === '406') { - ctx.body.message = error - } - ctx.status = 200 - } -} diff --git a/node-proxy/src/middleware/responseHandle.js b/node-proxy/src/middleware/responseHandle.js deleted file mode 100644 index 033a9b8..0000000 --- a/node-proxy/src/middleware/responseHandle.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict' - -export default async function (ctx, next) { - const env = 'dev' - try { - await next() - // 参数转换, 转换成自己的数据格式 - if (typeof ctx.body === 'object') { - const { flag, msg, code, data } = ctx.body - const body = { - flag: flag || true, - msg, - code: code || 200, - data, - } - ctx.body = body - } - if (ctx.status === 404) { - ctx.throw(404, '请求资源未找到!') - } - } catch (err) { - // 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志 - console.log(err) - const status = err.status || 500 - // 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息 - const error = status === 500 && env === 'prod' ? 'Internal Server Error' : err.message - // 从 error 对象上读出各个属性,设置到响应中 - ctx.body = { - flag: false, - msg: error, - code: status, // 服务端自身的处理逻辑错误(包含框架错误500 及 自定义业务逻辑错误533开始 ) 客户端请求参数导致的错误(4xx开始),设置不同的状态码 - data: null, - } - - // 406 是能让用户看到的错误,参数校验失败也不能让用户看到(一般不存在参数校验失败) - if (status === '403' || status === '406') { - ctx.body.message = error - } - ctx.status = 200 - } -} diff --git a/node-proxy/src/router.js b/node-proxy/src/router.js deleted file mode 100644 index cb3a5f8..0000000 --- a/node-proxy/src/router.js +++ /dev/null @@ -1,203 +0,0 @@ -'use strict' - -import Router from 'koa-router' -import bodyparser from 'koa-bodyparser' -import crypto from 'crypto' -import fs from 'fs' -import { alistServer, webdavServer, port, initAlistConfig, version } from './config' -import { getUserInfo, cacheUserToken, getUserByToken, updateUserInfo } from './dao/userDao' -import responseHandle from './middleware/responseHandle' -import { encodeFolderName, decodeFolderName } from './utils/commonUtil' -import { encryptFile, searchFile } from './utils/convertFile' - -// bodyparser解析body -const bodyparserMw = bodyparser({ enableTypes: ['json', 'form', 'text'] }) - -// 总路径,添加所有的子路由 -const allRouter = new Router() -// 拦截全部 -allRouter.all(/^\/enc-api\/*/, bodyparserMw, responseHandle, async (ctx, next) => { - console.log('@@log request-url: ', ctx.req.url) - await next() -}) - -// 白名单路由 -allRouter.all('/enc-api/login', async (ctx, next) => { - const { username, password } = ctx.request.body - console.log(username, password) - const userInfo = await getUserInfo(username) - console.log(userInfo) - if (userInfo && password === userInfo.password) { - // 创建token - const token = crypto.randomUUID() - // 异步执行 - cacheUserToken(token, userInfo) - userInfo.password = null - ctx.body = { data: { userInfo, jwtToken: token } } - return - } - ctx.body = { msg: 'passwword error', code: 500 } -}) - -// 拦截登录 -allRouter.all(/^\/enc-api\/*/, async (ctx, next) => { - // nginx不支持下划线headers - const { authorizetoken: authorizeToken } = ctx.request.headers - // 查询数据库是否有密码 - const userInfo = await getUserByToken(authorizeToken) - if (userInfo == null) { - ctx.body = { code: 401, msg: 'user unlogin' } - return - } - ctx.userInfo = userInfo - await next() -}) - -// 设置前缀 -const router = new Router({ prefix: '/enc-api' }) - -// 用户信息 -router.all('/getUserInfo', async (ctx, next) => { - const userInfo = ctx.userInfo - console.log('@@getUserInfo', userInfo) - userInfo.password = null - const data = { - codes: [16, 9, 10, 11, 12, 13, 15], - userInfo, - menuList: [], - roles: ['admin'], - version, - } - ctx.body = { data } -}) - -// 更新用户信息 -router.all('/updatePasswd', async (ctx, next) => { - const { password, newpassword, username } = ctx.request.body - if (newpassword.length < 7) { - ctx.body = { msg: 'password too short, at less 8 digits', code: 500 } - return - } - const userInfo = await getUserInfo(username) - if (password !== userInfo.password) { - ctx.body = { msg: 'password error', code: 500 } - return - } - userInfo.password = newpassword - updateUserInfo(userInfo) - ctx.body = { msg: 'update success' } -}) - -router.all('/getAlistConfig', async (ctx, next) => { - ctx.body = { data: alistServer._snapshot } -}) - -router.all('/saveAlistConfig', async (ctx, next) => { - let alistConfig = ctx.request.body - for (const index in alistConfig.passwdList) { - const passwdInfo = alistConfig.passwdList[index] - if (typeof passwdInfo.encPath === 'string') { - passwdInfo.encPath = passwdInfo.encPath.split(',') - } - } - const _snapshot = JSON.parse(JSON.stringify(alistConfig)) - // 写入到文件中,这里并不是真正的同步,, - fs.writeFileSync(process.cwd() + '/conf/config.json', JSON.stringify({ alistServer: _snapshot, webdavServer, port }, '', '\t')) - alistConfig = initAlistConfig(alistConfig) - Object.assign(alistServer, alistConfig) - alistServer._snapshot = _snapshot - ctx.body = { msg: 'save ok' } -}) - -router.all('/getWebdavonfig', async (ctx, next) => { - ctx.body = { data: webdavServer } -}) - -router.all('/saveWebdavConfig', async (ctx, next) => { - const config = ctx.request.body - for (const index in config.passwdList) { - const passwdInfo = config.passwdList[index] - if (typeof passwdInfo.encPath === 'string') { - passwdInfo.encPath = passwdInfo.encPath.split(',') - } - } - config.id = crypto.randomUUID() - webdavServer.push(config) - fs.writeFileSync(process.cwd() + '/conf/config.json', JSON.stringify({ alistServer: alistServer._snapshot, webdavServer, port }, '', '\t')) - ctx.body = { data: webdavServer } -}) - -router.all('/updateWebdavConfig', async (ctx, next) => { - const config = ctx.request.body - for (const index in config.passwdList) { - const passwdInfo = config.passwdList[index] - if (typeof passwdInfo.encPath === 'string') { - passwdInfo.encPath = passwdInfo.encPath.split(',') - } - } - - for (const index in webdavServer) { - if (webdavServer[index].id === config.id) { - webdavServer[index] = config - } - } - fs.writeFileSync(process.cwd() + '/conf/config.json', JSON.stringify({ alistServer: alistServer._snapshot, webdavServer, port }, '', '\t')) - ctx.body = { data: webdavServer } -}) - -router.all('/delWebdavConfig', async (ctx, next) => { - const { id } = ctx.request.body - for (const index in webdavServer) { - if (webdavServer[index].id === id) { - webdavServer.splice(index, 1) - } - } - fs.writeFileSync(process.cwd() + '/conf/config.json', JSON.stringify({ alistServer: alistServer._snapshot, webdavServer, port }, '', '\t')) - ctx.body = { data: webdavServer } -}) - -// get folder passwd encode -router.all('/encodeFoldName', async (ctx, next) => { - const { password, encType, folderPasswd, folderEncType } = ctx.request.body - const folderNameEnc = encodeFolderName(password, encType, folderPasswd, folderEncType) - ctx.body = { data: { folderNameEnc } } - console.log('@@encodeFoldName', password, folderNameEnc) -}) - -router.all('/decodeFoldName', async (ctx, next) => { - const { password, folderNameEnc, encType } = ctx.request.body - const arr = folderNameEnc.split('_') - if (arr.length < 2) { - ctx.body = { msg: 'folderName not encdoe', code: 500 } - return - } - const data = decodeFolderName(password, encType, folderNameEnc) - if (!data) { - ctx.body = { msg: 'folderName is error', code: 500 } - return - } - const { folderEncType, folderPasswd } = data - ctx.body = { data: { folderEncType, folderPasswd } } -}) - -// encrypt or decrypt file -router.all('/encryptFile', async (ctx, next) => { - const { folderPath, outPath, encType, password, operation, encName } = ctx.request.body - if (!fs.existsSync(folderPath)) { - ctx.body = { msg: 'encrypt file path not exists', code: 500 } - return - } - const files = searchFile(folderPath) - if (files.length > 10000) { - ctx.body = { msg: 'too maney file, exceeding 10000', code: 500 } - return - } - encryptFile(password, encType, operation, folderPath, outPath, encName) - ctx.body = { msg: 'waiting operation' } -}) - -// 用这种方式代替前缀的功能,{ prefix: } 不能和正则联合使用 -allRouter.use(router.routes(), router.allowedMethods()) - -// restRouter.all(/\/enc-api\/*/, router.routes(), restRouter.allowedMethods()) -export default allRouter diff --git a/node-proxy/src/router/alist/index.ts b/node-proxy/src/router/alist/index.ts new file mode 100644 index 0000000..1c46efe --- /dev/null +++ b/node-proxy/src/router/alist/index.ts @@ -0,0 +1,262 @@ +import path from 'path' +import crypto from 'crypto' +import type { Transform } from 'stream' + +import Router from 'koa-router' + +import nedb from '@/dao/levelDB' +import FlowEnc from '@/utils/flowEnc' +import { logger } from '@/common/logger' +import { preProxy } from '@/utils/common' +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 { convertRealName, convertShowName, encodeName, pathFindPasswd } from '@/utils/cryptoUtil' +import type { alist } from '@/@types/alist' + +const alistRouter = new Router({ prefix: '/api' }) +// 其他的代理request预处理,添加到ctx.state中 +alistRouter.use(preProxy(alistServer, false)) + +alistRouter.all, ParsedContext>('/fs/list', bodyParserMiddleware, async (ctx, next) => { + logger.info('从alist获取文件列表 ', ctx.req.url) + + await next() + const { path } = ctx.request.body + + const response = JSON.parse(ctx.response.body) as alist.FsListResponseBody + + logger.info(`已从alist获取文件列表,路径:${path} 文件数量:${response.data.content?.length || 0}`) + logger.trace(`原始文件信息: `, response.data) + + if (response.code !== 200) return + + const files = response.data.content + if (!files) return + + const state = ctx.state + let encrypted = false + + for (let i = 0; i < files.length; i++) { + const file = files[i] + file.path = path + '/' + file.name + await cacheFileInfo(file) + + if (file.is_dir) continue + + const { passwdInfo } = pathFindPasswd(state.serverConfig.passwdList, decodeURI(file.path)) + + if (passwdInfo && passwdInfo.encName) { + encrypted = true + file.name = convertShowName(passwdInfo.password, passwdInfo.encType, file.name) + } + } + + logger.info(encrypted ? '解密完成' : '无需解密内容,跳过解密') + + const coverNameMap = {} //根据不含后缀的视频文件名找到对应的含后缀的封面文件名 + const omitNames = [] //用于隐藏封面文件 + + files + .filter((fileInfo) => fileInfo.type === 5) + .forEach((fileInfo) => { + coverNameMap[fileInfo.name.split('.')[0]] = fileInfo.name + }) + + files + .filter((fileInfo) => fileInfo.type === 2) + .forEach((fileInfo) => { + const coverName = coverNameMap[fileInfo.name.split('.')[0]] + + if (coverName) { + omitNames.push(coverName) + fileInfo.thumb = `/d${path}/${coverName}` + } + }) + + //不展示封面文件,也许可以添加个配置让用户选择是否展示封面源文件 + response.data.content = files.filter((fileInfo) => !omitNames.includes(fileInfo.name)) + ctx.body = response + + logger.info(`返回文件列表信息`) + logger.trace(`处理后文件信息: `, ctx.body.data) +}) + +alistRouter.all, ParsedContext>('/fs/get', bodyParserMiddleware, async (ctx, next) => { + const { path: filePath } = ctx.request.body + const state = ctx.state + const { passwdInfo } = pathFindPasswd(state.serverConfig.passwdList, filePath) + + const encrypted = passwdInfo && passwdInfo.encName + + logger.info(`文件路径: ${filePath} 是否被加密 ${encrypted}`) + + if (encrypted) { + // reset content-length length + delete ctx.req.headers['content-length'] + // check fileName is not enc + const fileName = path.basename(filePath) + const fileInfo = await getFileInfo(encodeURIComponent(filePath)) + // Check if it is a directory + if (fileInfo && fileInfo.is_dir) { + await next() + return + } + + const realName = convertRealName(passwdInfo.password, passwdInfo.encType, fileName) + const fileUri = path.dirname(filePath) + '/' + realName + logger.info(`构造原始路径: ${filePath} -> ${fileUri}`) + ctx.request.body.path = fileUri + } + + logger.info('获取文件内容...') + + await next() + + ctx.body = JSON.parse(ctx.body) + + logger.info(encrypted ? '开始解密文件信息' : '文件未加密,跳过解密') + + if (encrypted) { + // return showName + const respBody = ctx.body + respBody.data.name = convertShowName(passwdInfo.password, passwdInfo.encType, respBody.data.name) + + const { headers } = ctx.request + const key = crypto.randomUUID() + + await nedb.setExpire( + key, + { + redirectUrl: respBody.data.raw_url, + passwdInfo, + fileSize: respBody.data.size, + }, + 60 * 60 * 72 + ) // 缓存起来,默认3天,足够下载和观看了 + + respBody.data.raw_url = `${ + headers.origin || (headers['x-forwarded-proto'] || ctx.protocol) + '://' + ctx.state.selfHost + }/redirect/${key}?decode=1&lastUrl=${encodeURIComponent(ctx.request.body.path)}` + + if (respBody.data.provider === 'AliyundriveOpen') respBody.data.provider = 'Local' + + ctx.body = respBody + } + + logger.info(`返回文件信息`) +}) + +// 处理网页上传文件 +alistRouter.put, EmptyObj>('/fs/put', emptyMiddleware, async (ctx) => { + const { headers } = ctx.request + const state = ctx.state + const uploadPath = headers['file-path'] ? decodeURIComponent(headers['file-path'] as string) : '/-' + const { passwdInfo } = pathFindPasswd(state.serverConfig.passwdList, uploadPath) + state.fileSize = Number(headers['content-length'] || 0) + + const encrypted = passwdInfo && passwdInfo.encName + logger.info(`上传文件: ${uploadPath} 加密${Boolean(encrypted)}`) + + let encryptTransform: Transform + if (encrypted) { + const fileName = path.basename(uploadPath) + const ext = passwdInfo.encSuffix || path.extname(fileName) + const encName = encodeName(passwdInfo.password, passwdInfo.encType, fileName) + const filePath = path.dirname(uploadPath) + '/' + encName + ext + logger.info('加密后路径: ', filePath) + headers['file-path'] = encodeURIComponent(filePath) + const flowEnc = new FlowEnc(passwdInfo.password, passwdInfo.encType, ctx.state.fileSize) + encryptTransform = flowEnc.encryptTransform() + } + + return await httpFlowClient({ + urlAddr: state.urlAddr, + passwdInfo, + fileSize: state.fileSize, + request: ctx.req, + response: ctx.res, + encryptTransform, + }) +}) + +alistRouter.all, ParsedContext>('/fs/rename', bodyParserMiddleware, async (ctx) => { + const { path: filePath, name } = ctx.request.body + const { serverConfig: config, urlAddr } = ctx.state + const { passwdInfo } = pathFindPasswd(config.passwdList, filePath) + + const reqBody: alist.FsRenameRequestBody = { path: filePath, name } + + logger.info(`重命名文件 ${filePath} -> ${name}`) + + // reset content-length length + delete ctx.req.headers['content-length'] + + let fileInfo = await getFileInfo(encodeURIComponent(filePath)) + + if (fileInfo == null && passwdInfo && passwdInfo.encName) { + // maybe a file + const realName = convertRealName(passwdInfo.password, passwdInfo.encType, filePath) + const realFilePath = path.dirname(filePath) + '/' + realName + logger.info(`转化为原始文件路径: ${filePath} ${realFilePath}`) + fileInfo = await getFileInfo(encodeURIComponent(realFilePath)) + } + + if (passwdInfo && passwdInfo.encName && fileInfo && !fileInfo.is_dir) { + // reset content-length length + const ext = passwdInfo.encSuffix || path.extname(name) + const realName = convertRealName(passwdInfo.password, passwdInfo.encType, filePath) + const fileUri = path.dirname(filePath) + '/' + realName + const newName = encodeName(passwdInfo.password, passwdInfo.encType, name) + reqBody.path = fileUri + reqBody.name = newName + ext + } + + logger.info(`重命名: ${reqBody.path} -> ${reqBody.name}`) + ctx.body = await httpClient({ + urlAddr, + reqBody: JSON.stringify(reqBody), + request: ctx.req, + }) +}) + +// remove +alistRouter.all, ParsedContext>('/fs/remove', bodyParserMiddleware, async (ctx) => { + const { dir, names } = ctx.request.body + const state = ctx.state + + logger.info(`删除文件: 路径${dir} 文件名${JSON.stringify(names)}`) + + const { passwdInfo } = pathFindPasswd(state.serverConfig.passwdList, dir) + + // maybe a folder,remove anyway the name + const fileNames = Object.assign([], names) //TODO 并没有判断是否加密,而是尝试同时删除加密前与加密后的文件,应该修改为判断是否加密再删除 + if (passwdInfo && passwdInfo.encName) { + for (const name of names) { + // is not enc name + const realName = convertRealName(passwdInfo.password, passwdInfo.encType, name) + fileNames.push(realName) + } + + logger.info('转化为原始文件名: ', JSON.stringify(fileNames)) + } + + const reqBody = { dir, names: fileNames } + + // reset content-length length + delete ctx.req.headers['content-length'] + + ctx.body = await httpClient({ + urlAddr: state.urlAddr, + reqBody: JSON.stringify(reqBody), + request: ctx.req, + }) +}) + +alistRouter.all, ParsedContext>('/fs/move', bodyParserMiddleware, copyOrMoveFileMiddleware) + +alistRouter.all, ParsedContext>('/fs/copy', bodyParserMiddleware, copyOrMoveFileMiddleware) + +export default alistRouter diff --git a/node-proxy/src/router/alist/utils.ts b/node-proxy/src/router/alist/utils.ts new file mode 100644 index 0000000..82de339 --- /dev/null +++ b/node-proxy/src/router/alist/utils.ts @@ -0,0 +1,50 @@ +import path from 'path' + +import type { Middleware } from 'koa' + +import { logger } from '@/common/logger' +import { httpClient } from '@/utils/httpClient' +import { encodeName, pathFindPasswd } from '@/utils/cryptoUtil' +import { alist } from '@/@types/alist' + +const origPrefix = 'orig_' + +export const copyOrMoveFileMiddleware: Middleware< + ProxiedState, + ParsedContext +> = async (ctx) => { + const state = ctx.state + const { dst_dir: dstDir, src_dir: srcDir, names } = ctx.request.body + const { passwdInfo } = pathFindPasswd(state.serverConfig.passwdList, srcDir) + + logger.info(`复制/移动文件: ${JSON.stringify(names)}`) + logger.info(`原文件夹:${srcDir} -> 目标文件夹:${dstDir}`) + + let fileNames = [] + if (passwdInfo && passwdInfo.encName) { + for (const name of names) { + // is not enc name + if (name.indexOf(origPrefix) === 0) { + const origName = name.replace(origPrefix, '') + fileNames.push(origName) + break + } + const fileName = path.basename(name) + // you can custom Suffix + const ext = passwdInfo.encSuffix || path.extname(fileName) + const encName = encodeName(passwdInfo.password, passwdInfo.encType, fileName) + const newFileName = encName + ext + fileNames.push(newFileName) + } + + logger.info('转化为原始文件名: ', JSON.stringify(fileNames)) + } else { + fileNames = Object.assign([], names) + } + + const reqBody = { dst_dir: dstDir, src_dir: srcDir, names: fileNames } + // reset content-length length + delete ctx.req.headers['content-length'] + + ctx.body = await httpClient({ urlAddr: state.urlAddr, reqBody: JSON.stringify(reqBody), request: ctx.req }) +} diff --git a/node-proxy/src/router/other/index.ts b/node-proxy/src/router/other/index.ts new file mode 100644 index 0000000..2f13334 --- /dev/null +++ b/node-proxy/src/router/other/index.ts @@ -0,0 +1,119 @@ +import Router from 'koa-router' +import { flat, preProxy } from '@/utils/common' +import { alistServer, version } from '@/config' +import levelDB from '@/dao/levelDB' +import FlowEnc from '@/utils/flowEnc' +import { pathExec } from '@/utils/cryptoUtil' +import { httpClient, httpFlowClient } from '@/utils/httpClient' +import { logger } from '@/common/logger' +import { bodyParserMiddleware, compose, proxyHandler } from '@/middleware/common' +import { downloadMiddleware } from '@/router/other/utils' + +const otherRouter = new Router() + +otherRouter.get, EmptyObj>( + /^\/d\/*/, + compose(bodyParserMiddleware, preProxy(alistServer, false), downloadMiddleware), + proxyHandler +) + +otherRouter.get, EmptyObj>( + /^\/p\/*/, + compose(bodyParserMiddleware, preProxy(alistServer, false), downloadMiddleware), + proxyHandler +) + +// 修复alist 图标不显示的问题 +otherRouter.all(/^\/images\/*/, compose(bodyParserMiddleware, preProxy(alistServer, false)), async (ctx) => { + const state = ctx.state + + delete ctx.req.headers.host + + return await httpFlowClient({ + urlAddr: state.urlAddr, + passwdInfo: state.passwdInfo, + fileSize: state.fileSize, + request: ctx.req, + response: ctx.res, + }) +}) + +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) + + if (data === null) { + ctx.body = 'no found' + return + } + + const { passwdInfo, redirectUrl, fileSize } = data + // 要定位请求文件的位置 bytes=98304- + const range = request.headers.range + const start = range ? Number(range.replace('bytes=', '').split('-')[0]) : 0 + const decode = ctx.query.decode + + logger.info(`重定向: ${ctx.path} -> ${redirectUrl}, 解密${decode !== '0'}`) + + const flowEnc = new FlowEnc(passwdInfo.password, passwdInfo.encType, fileSize) + + if (start) { + await flowEnc.setPosition(start) + } + + // 设置请求地址和是否要解密 + // 修改百度头 + if (~redirectUrl.indexOf('baidupcs.com')) { + request.headers['User-Agent'] = 'pan.baidu.com' + } + request.url = decodeURIComponent(flat(ctx.query.lastUrl)) + delete request.headers.host + delete request.headers.referer + // 123网盘和天翼网盘多次302 + // authorization 是alist网页版的token,不是webdav的,这里修复天翼云无法获取资源的问题 + delete request.headers.authorization + + // 默认判断路径来识别是否要解密,如果有decode参数,那么则按decode来处理,这样可以让用户手动处理是否解密?(那还不如直接在alist下载) + let decryptTransform = passwdInfo.enable && pathExec(passwdInfo.encPath, request.url) ? flowEnc.decryptTransform() : null + if (decode) { + decryptTransform = decode !== '0' ? flowEnc.decryptTransform() : null + } + // 请求实际服务资源 + await httpFlowClient({ + urlAddr: redirectUrl, + passwdInfo, + fileSize, + request, + response, + decryptTransform, + }) +}) + +otherRouter.all, EmptyObj>(new RegExp(alistServer.path), preProxy(alistServer, false), async (ctx) => { + let respBody = await httpClient({ + urlAddr: ctx.state.urlAddr, + reqBody: JSON.stringify(ctx.request.body), + request: ctx.req, + response: ctx.res, + }) + + respBody = respBody.replace( + '', + ` + ` + ) + ctx.body = respBody +}) + +export default otherRouter diff --git a/node-proxy/src/router/other/utils.ts b/node-proxy/src/router/other/utils.ts new file mode 100644 index 0000000..4812eaa --- /dev/null +++ b/node-proxy/src/router/other/utils.ts @@ -0,0 +1,42 @@ +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 new file mode 100644 index 0000000..2f52954 --- /dev/null +++ b/node-proxy/src/router/static/index.ts @@ -0,0 +1,7 @@ +import Router from 'koa-router' + +const staticRouter = new Router({ prefix: '/index' }) + +staticRouter.redirect('/', '/public/index.html', 302) + +export default staticRouter diff --git a/node-proxy/src/router/webdav/index.ts b/node-proxy/src/router/webdav/index.ts new file mode 100644 index 0000000..f7e4cf9 --- /dev/null +++ b/node-proxy/src/router/webdav/index.ts @@ -0,0 +1,20 @@ +import Router from 'koa-router' + +import { preProxy } from '@/utils/common' +import { alistServer, webdavServer } from '@/config' +import { encDavMiddleware } from '@/router/webdav/middlewares' +import { compose, proxyHandler } from '@/middleware/common' + +const webdavRouter = new Router() + +// 初始化webdav路由 +webdavServer.forEach((webdavConfig) => { + if (webdavConfig.enable) { + webdavRouter.all(new RegExp(webdavConfig.path), compose(preProxy(webdavConfig, true), encDavMiddleware), proxyHandler) + } +}) + +// 单独处理alist的所有/dav +webdavRouter.all(/^\/dav\/*/, compose(preProxy(alistServer, false), encDavMiddleware), proxyHandler) + +export default webdavRouter diff --git a/node-proxy/src/encDavHandle.js b/node-proxy/src/router/webdav/middlewares.ts similarity index 62% rename from node-proxy/src/encDavHandle.js rename to node-proxy/src/router/webdav/middlewares.ts index 0099888..53f90f4 100644 --- a/node-proxy/src/encDavHandle.js +++ b/node-proxy/src/router/webdav/middlewares.ts @@ -1,70 +1,23 @@ -'use strict' - -import { pathFindPasswd, convertRealName, convertShowName } from './utils/commonUtil' -import { cacheFileInfo, getFileInfo } from './dao/fileDao' -import { logger } from './common/logger' import path from 'path' -import { httpClient } from './utils/httpClient' + import { XMLParser } from 'fast-xml-parser' -// import { escape } from 'querystring' +import type { Middleware } from 'koa' -async function sleep(time) { - return new Promise((resolve) => { - setTimeout(() => { - resolve() - }, time || 3000) - }) -} +import { flat, sleep } from '@/utils/common' +import { logger } from '@/common/logger' +import { httpClient } from '@/utils/httpClient' +import { cacheFileInfo, getFileInfo } from '@/dao/fileDao' +import { convertRealName, convertShowName, pathFindPasswd } from '@/utils/cryptoUtil' +import { cacheWebdavFileInfo, getFileNameForShow } from '@/router/webdav/utils' -// bodyparser解析body const parser = new XMLParser({ removeNSPrefix: true }) -function getFileNameForShow(fileInfo, passwdInfo) { - let getcontentlength = -1 - const href = fileInfo.href - const fileName = path.basename(href) - if (fileInfo.propstat instanceof Array) { - getcontentlength = fileInfo.propstat[0].prop.getcontentlength - } else if (fileInfo.propstat.prop) { - getcontentlength = fileInfo.propstat.prop.getcontentlength - } - // logger.debug('@@fileInfo_show', JSON.stringify(fileInfo)) - // is not dir - if (getcontentlength !== undefined && getcontentlength > -1) { - const showName = convertShowName(passwdInfo.password, passwdInfo.encType, href) - return { fileName, showName } - } - // cache this folder info - return {} -} +export const encDavMiddleware: Middleware> = async (ctx, next) => { + const request = ctx.req + const state = ctx.state -function cacheWebdavFileInfo(fileInfo) { - let getcontentlength = -1 - const href = fileInfo.href - const fileName = path.basename(href) - if (fileInfo.propstat instanceof Array) { - getcontentlength = fileInfo.propstat[0].prop.getcontentlength - } else if (fileInfo.propstat.prop) { - getcontentlength = fileInfo.propstat.prop.getcontentlength - } - // logger.debug('@@@cacheWebdavFileInfo', href, fileName) - // it is a file - if (getcontentlength !== undefined && getcontentlength > -1) { - const fileDetail = { path: href, name: fileName, is_dir: false, size: getcontentlength } - cacheFileInfo(fileDetail) - return fileDetail - } - // cache this folder info - const fileDetail = { path: href, name: fileName, is_dir: true, size: 0 } - cacheFileInfo(fileDetail) - return fileDetail -} + const { passwdInfo } = pathFindPasswd(state.serverConfig.passwdList, decodeURIComponent(request.url)) -// 拦截全部 -const handle = async (ctx, next) => { - const request = ctx.req - const { passwdList } = request.webdavConfig - const { passwdInfo } = pathFindPasswd(passwdList, decodeURIComponent(request.url)) if (ctx.method.toLocaleUpperCase() === 'PROPFIND' && passwdInfo && passwdInfo.encName) { // check dir, convert url const url = request.url @@ -73,29 +26,32 @@ const handle = async (ctx, next) => { const reqFileName = path.basename(url) // cache source file info, realName has execute encodeUrl(),this '(' '+' can't encodeUrl. const realName = convertRealName(passwdInfo.password, passwdInfo.encType, url) - // when the name contain the + , ! , + // when the name contain the '+,!' , const sourceUrl = path.dirname(url) + '/' + realName const sourceFileInfo = await getFileInfo(sourceUrl) logger.debug('@@@sourceFileInfo', sourceFileInfo, reqFileName, realName, url, sourceUrl) // it is file, convert file name if (sourceFileInfo && !sourceFileInfo.is_dir) { request.url = path.dirname(request.url) + '/' + realName - request.urlAddr = path.dirname(request.urlAddr) + '/' + realName + ctx.state.urlAddr = path.dirname(ctx.state.urlAddr) + '/' + realName } } // decrypt file name - let respBody = await httpClient(ctx.req, ctx.res) + let respBody = await httpClient({ + urlAddr: state.urlAddr, + request: ctx.req, + response: ctx.res, + }) const respData = parser.parse(respBody) // convert file name for show if (respData.multistatus) { const respJson = respData.multistatus.response if (respJson instanceof Array) { // console.log('@@respJsonArray', respJson) - respJson.forEach((fileInfo) => { - // cache real file info,include forder name - cacheWebdavFileInfo(fileInfo) + for (const fileInfo of respJson) { + await cacheWebdavFileInfo(fileInfo) if (passwdInfo && passwdInfo.encName) { - const { fileName, showName } = getFileNameForShow(fileInfo, passwdInfo) + const { fileName, showName } = await getFileNameForShow(fileInfo, passwdInfo) // logger.debug('@@getFileNameForShow1 list', passwdInfo.password, fileName, decodeURI(fileName), showName) if (fileName) { const showXmlName = showName.replace(/&/g, '&').replace(/ { respBody = respBody.replace(`${decodeURI(fileName)}`, `${decodeURI(showXmlName)}`) } } - }) + } // waiting cacheWebdavFileInfo a moment await sleep(50) } else if (passwdInfo && passwdInfo.encName) { - const fileInfo = respJson - const { fileName, showName } = getFileNameForShow(fileInfo, passwdInfo) + const { fileName, showName } = await getFileNameForShow(respJson, passwdInfo) // logger.debug('@@getFileNameForShow2 file', fileName, showName, url, respJson.propstat) if (fileName) { const showXmlName = showName.replace(/&/g, '&').replace(/ { ctx.body = respBody return } - // copy or move file + if ('COPY,MOVE'.includes(request.method.toLocaleUpperCase()) && passwdInfo && passwdInfo.encName) { const url = request.url const realName = convertRealName(passwdInfo.password, passwdInfo.encType, url) - request.headers.destination = path.dirname(request.headers.destination) + '/' + encodeURI(realName) + request.headers.destination = path.dirname(flat(request.headers.destination)) + '/' + encodeURI(realName) request.url = path.dirname(request.url) + '/' + encodeURI(realName) - request.urlAddr = path.dirname(request.urlAddr) + '/' + encodeURI(realName) + state.urlAddr = path.dirname(state.urlAddr) + '/' + encodeURI(realName) } // upload file @@ -149,8 +104,12 @@ const handle = async (ctx, next) => { const realName = convertRealName(passwdInfo.password, passwdInfo.encType, url) // maybe from aliyundrive, check this req url while get file list from enc folder if (url.endsWith('/') && 'GET,DELETE'.includes(request.method.toLocaleUpperCase())) { - let respBody = await httpClient(ctx.req, ctx.res) - if(request.method.toLocaleUpperCase() === 'GET'){ + let respBody = await httpClient({ + urlAddr: state.urlAddr, + request: ctx.req, + response: ctx.res, + }) + if (request.method.toLocaleUpperCase() === 'GET') { const aurlArr = respBody.match(/href="[^"]*"/g) // logger.debug('@@aurlArr', aurlArr) if (aurlArr && aurlArr.length) { @@ -173,9 +132,9 @@ const handle = async (ctx, next) => { // console.log('@@convert file name', fileName, realName) request.url = path.dirname(request.url) + '/' + realName - request.urlAddr = path.dirname(request.urlAddr) + '/' + realName + state.urlAddr = path.dirname(state.urlAddr) + '/' + realName // cache file before upload in next(), rclone cmd 'copy' will PROPFIND this file when the file upload success right now - const contentLength = request.headers['content-length'] || request.headers['x-expected-entity-length'] || 0 + const contentLength = Number(flat(request.headers['content-length'] || request.headers['x-expected-entity-length']) || 0) const fileDetail = { path: url, name: fileName, is_dir: false, size: contentLength } logger.info('@@@put url', url) // 在页面上传文件,rclone会重复上传,所以要进行缓存文件信息,也不能在next() 因为rclone copy命令会出异常 @@ -183,5 +142,3 @@ const handle = async (ctx, next) => { } await next() } - -export default handle diff --git a/node-proxy/src/router/webdav/utils.ts b/node-proxy/src/router/webdav/utils.ts new file mode 100644 index 0000000..afe0377 --- /dev/null +++ b/node-proxy/src/router/webdav/utils.ts @@ -0,0 +1,44 @@ +import path from 'path' +import { cacheFileInfo } from '@/dao/fileDao' +import { convertShowName } from '@/utils/cryptoUtil' + +export const cacheWebdavFileInfo = async (fileInfo) => { + let contentLength = -1 + const href = fileInfo.href + const fileName = path.basename(href) + if (fileInfo.propstat instanceof Array) { + contentLength = fileInfo.propstat[0].prop.getcontentlength + } else if (fileInfo.propstat.prop) { + contentLength = fileInfo.propstat.prop.getcontentlength + } + // logger.debug('@@@cacheWebdavFileInfo', href, fileName) + // it is a file + if (contentLength !== undefined && contentLength > -1) { + const fileDetail = { path: href, name: fileName, is_dir: false, size: contentLength } + await cacheFileInfo(fileDetail) + return fileDetail + } + // cache this folder info + const fileDetail = { path: href, name: fileName, is_dir: true, size: 0 } + await cacheFileInfo(fileDetail) + return fileDetail +} + +export const getFileNameForShow = async (fileInfo, passwdInfo: PasswdInfo) => { + let contentLength = -1 + const href = fileInfo.href + const fileName = path.basename(href) + if (fileInfo.propstat instanceof Array) { + contentLength = fileInfo.propstat[0].prop.getcontentlength + } else if (fileInfo.propstat.prop) { + contentLength = fileInfo.propstat.prop.getcontentlength + } + // logger.debug('@@fileInfo_show', JSON.stringify(fileInfo)) + // is not dir + if (contentLength !== undefined && contentLength > -1) { + const showName = convertShowName(passwdInfo.password, passwdInfo.encType, href) + return { fileName, showName } + } + // cache this folder info + return {} +} diff --git a/node-proxy/src/router/webui/index.ts b/node-proxy/src/router/webui/index.ts new file mode 100644 index 0000000..84f18fd --- /dev/null +++ b/node-proxy/src/router/webui/index.ts @@ -0,0 +1,323 @@ +import fs from 'fs' +import crypto from 'crypto' + +import Router from 'koa-router' + +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 { RawPasswdInfo, response, splitEncPath } from '@/router/webui/utils' +import { getUserInfo, cacheUserToken, updateUserInfo } from '@/dao/userDao' +import { alistServer, webdavServer, port, initAlistConfig, version } from '@/config' +import type { webui } from '@/@types/webui' + +const webuiRouter = new Router({ prefix: '/enc-api' }) +webuiRouter.use(bodyParserMiddleware) //目前webui不涉及二进制数据的响应,因此全都使用bodyParser解析request.body为对象 + +//登录路由 +webuiRouter.all< + EmptyObj, + ParsedContext<{ + username: string + password: string + }> +>('/login', emptyMiddleware, async (ctx) => { + const { username, password } = ctx.request.body + const userInfo = await getUserInfo(username) + logger.info('用户信息', JSON.stringify(userInfo)) + + if (!userInfo || password !== userInfo.password) { + ctx.body = response({ msg: 'password error', code: 500 }) + return + } + + const token = crypto.randomUUID() + await cacheUserToken(token, userInfo) + userInfo.password = null + ctx.body = response({ data: { userInfo, jwtToken: token } }) +}) + +// 用户信息 +webuiRouter.all('/getUserInfo', userInfoMiddleware, async (ctx) => { + const userInfo = ctx.state.userInfo + logger.info('用户信息', JSON.stringify(userInfo)) + + userInfo.password = null + const data = { + codes: [16, 9, 10, 11, 12, 13, 15], + userInfo, + menuList: [], + roles: ['admin'], + version, + } + ctx.body = response({ data }) +}) + +// 更新用户信息 +webuiRouter.all< + webui.State, + ParsedContext<{ + username: string + password: string + newpassword: string + }> +>('/updatePasswd', userInfoMiddleware, async (ctx) => { + const { password, newpassword, username } = ctx.request.body + logger.info(`用户名: ${username} 原密码:${password} 新密码:${newpassword}`) + + if (newpassword.length < 7) { + ctx.body = response({ msg: 'password too short, at less 8 digits', code: 500 }) + return + } + + const userInfo = await getUserInfo(username) + if (password !== userInfo.password) { + ctx.body = response({ msg: 'password error', code: 500 }) + return + } + + userInfo.password = newpassword + await updateUserInfo(userInfo) + ctx.body = response({ msg: 'update success' }) + + logger.info('已更新用户信息', JSON.stringify(userInfo)) +}) + +webuiRouter.all('/getAlistConfig', userInfoMiddleware, async (ctx) => { + ctx.body = response({ data: alistServer._snapshot }) +}) + +webuiRouter.all>>( + '/saveAlistConfig', + userInfoMiddleware, + async (ctx) => { + let alistConfig: AlistServer = { + ...ctx.request.body, + passwdList: ctx.request.body.passwdList.map((p) => splitEncPath(p)), + } + + logger.info('保存alist配置信息', JSON.stringify(alistConfig)) + const _snapshot = JSON.parse(JSON.stringify(alistConfig)) + // 写入到文件中,这里并不是真正的同步 + fs.writeFileSync( + process.cwd() + '/conf/config.json', + JSON.stringify( + { + alistServer: _snapshot, + webdavServer, + port, + }, + undefined, + '\t' + ) + ) + alistConfig = initAlistConfig(alistConfig) + Object.assign(alistServer, alistConfig) + alistServer._snapshot = _snapshot + logger.info('alist配置信息已更新') + ctx.body = response({ msg: 'save ok' }) + } +) + +//TODO 纠正拼写错误的路由 getWebdavonfig->getWebdavConfig +webuiRouter.all('/getWebdavonfig', userInfoMiddleware, async (ctx) => { + logger.info('获取webdav配置信息: ', JSON.stringify(webdavServer)) + ctx.body = response({ data: webdavServer }) +}) + +webuiRouter.all>>( + '/saveWebdavConfig', + userInfoMiddleware, + async (ctx) => { + const newWebdavServer: WebdavServer = { + ...ctx.request.body, + passwdList: ctx.request.body.passwdList.map((p) => splitEncPath(p)), + } + + newWebdavServer.id = crypto.randomUUID() + logger.info('新增webdav配置信息', JSON.stringify(newWebdavServer)) + + webdavServer.push(newWebdavServer) + fs.writeFileSync( + process.cwd() + '/conf/config.json', + JSON.stringify( + { + alistServer: alistServer._snapshot, + webdavServer, + port, + }, + undefined, + '\t' + ) + ) + + logger.info('webdav配置信息已更新') + ctx.body = response({ data: webdavServer }) + } +) + +webuiRouter.all>>( + '/updateWebdavConfig', + userInfoMiddleware, + async (ctx) => { + const config = ctx.request.body + logger.info('更新webdav配置信息', config.id, config) + + for (const index in webdavServer) { + if (webdavServer[index].id === config.id) { + webdavServer[index] = { + ...config, + passwdList: ctx.request.body.passwdList.map((p) => splitEncPath(p)), + } + } + } + + fs.writeFileSync( + process.cwd() + '/conf/config.json', + JSON.stringify( + { + alistServer: alistServer._snapshot, + webdavServer, + port, + }, + undefined, + '\t' + ) + ) + + logger.info('webdav配置信息已更新') + ctx.body = response({ data: webdavServer }) + } +) + +webuiRouter.all< + webui.State, + ParsedContext<{ + id: string + passwdList: PasswdInfo[] + }> +>('/delWebdavConfig', userInfoMiddleware, async (ctx) => { + const { id } = ctx.request.body + + logger.info('删除webdav配置信息', id) + + let indexToDelete = -1 // 初始化索引为-1,表示未找到 + for (const server of webdavServer) { + if (server.id === id) { + indexToDelete = webdavServer.indexOf(server) // 找到匹配的元素后,获取其索引 + break // 找到第一个匹配项后退出循环 + } + } + + if (indexToDelete !== -1) { + webdavServer.splice(indexToDelete, 1) // 如果找到了匹配的索引,则删除该元素 + } + + fs.writeFileSync( + process.cwd() + '/conf/config.json', + JSON.stringify( + { + alistServer: alistServer._snapshot, + webdavServer, + port, + }, + undefined, + '\t' + ) + ) + + logger.info('webdav配置信息已删除', id) + ctx.body = response({ data: webdavServer }) +}) + +// get folder passwd encode +webuiRouter.all< + webui.State, + ParsedContext<{ + password: string + encType: EncryptType + folderEncType: string + folderPasswd: string + }> +>('/encodeFoldName', userInfoMiddleware, async (ctx) => { + const { password, encType, folderPasswd, folderEncType } = ctx.request.body + + logger.info('加密文件夹', password, encType, folderPasswd) + + const result = encodeFolderName(password, encType, folderPasswd, folderEncType) + ctx.body = response({ + data: { + folderNameEnc: result, + }, + }) + + logger.info('加密结果: ', result) +}) + +webuiRouter.all< + webui.State, + ParsedContext<{ + password: string + encType: EncryptType + folderNameEnc: string + }> +>('/decodeFoldName', userInfoMiddleware, async (ctx) => { + const { password, folderNameEnc, encType } = ctx.request.body + const arr = folderNameEnc.split('_') + logger.info('解密文件夹', password, encType, folderNameEnc) + + if (arr.length < 2) { + ctx.body = response({ msg: 'folderName not encoded', code: 500 }) + return + } + + const data = decodeFolderName(password, encType, folderNameEnc) + if (!data) { + ctx.body = response({ msg: 'folderName is error', code: 500 }) + return + } + + const { folderEncType, folderPasswd } = data + ctx.body = response({ data: { folderEncType, folderPasswd } }) + + logger.info('解密结果: ', data) +}) + +// encrypt or decrypt file +webuiRouter.all< + webui.State, + ParsedContext<{ + folderPath: string + outPath: string + encType: EncryptType + encName: string + password: string + operation: 'enc' | 'dec' + }> +>('/encryptFile', userInfoMiddleware, async (ctx) => { + const { folderPath, outPath, encType, password, operation, encName } = ctx.request.body + + logger.info(`加密本地文件: 源文件夹${folderPath} 目标文件夹:${outPath}`) + + if (!fs.existsSync(folderPath)) { + ctx.body = response({ msg: 'encrypt file path not exists', code: 500 }) + return + } + + const files = searchFile(folderPath) + + if (files.length > 10000) { + ctx.body = response({ msg: 'too many file, exceeding 10000', code: 500 }) + return + } + + encryptFile(password, encType, operation, folderPath, outPath, encName).then() + + logger.info('加密中,请稍后...') + + ctx.body = response({ msg: 'waiting operation' }) +}) + +export default webuiRouter diff --git a/node-proxy/src/router/webui/middlewares.ts b/node-proxy/src/router/webui/middlewares.ts new file mode 100644 index 0000000..e8b6c7a --- /dev/null +++ b/node-proxy/src/router/webui/middlewares.ts @@ -0,0 +1,23 @@ +import type { Middleware } from 'koa' + +import { flat } from '@/utils/common' +import { response } from '@/router/webui/utils' +import { getUserByToken } from '@/dao/userDao' + +//获取用户信息的中间件 +export const userInfoMiddleware: Middleware = async (ctx, next) => { + // nginx不支持下划线headers + const { authorizetoken: authorizeToken } = ctx.request.headers + + // 查询数据库是否有密码 + const userInfo = await getUserByToken(flat(authorizeToken)) + + if (userInfo == null) { + ctx.body = response({ code: 401, msg: 'user not login' }) + return + } + + ctx.state.userInfo = userInfo //放入ctx.state,之后可以用ctx.state,userInfo获取登录信息 + + await next() +} diff --git a/node-proxy/src/router/webui/utils.ts b/node-proxy/src/router/webui/utils.ts new file mode 100644 index 0000000..669bea0 --- /dev/null +++ b/node-proxy/src/router/webui/utils.ts @@ -0,0 +1,17 @@ +import type { webui } from '@/@types/webui' + +export type RawPasswdInfo = ModifyPropType + +//将文件目录字符串转化为列表 +export const splitEncPath = (raw: RawPasswdInfo): PasswdInfo => { + return { + ...raw, + encPath: Array.isArray(raw.encPath) ? raw.encPath : raw.encPath.split(','), + } +} + +//构造webui返回响应的body +export const response = (raw: Partial): webui.ResponseBody => { + const { flag, msg, code, data } = raw + return { flag: flag || true, msg, code: code || 200, data } +} diff --git a/node-proxy/src/utils/chaCha20.js b/node-proxy/src/utils/chaCha20.js deleted file mode 100644 index bfeb541..0000000 --- a/node-proxy/src/utils/chaCha20.js +++ /dev/null @@ -1,206 +0,0 @@ -/** - * - * @param {Uint8Array} key - * @param {Uint8Array} nonce - * @param {number} counter - * @throws {Error} - * - * @constructor - */ -const JSChaCha20 = function (key, nonce, counter) { - if (typeof counter === 'undefined') { - counter = 0 - } - - if (!(key instanceof Uint8Array) || key.length !== 32) { - throw new Error('Key should be 32 byte array!') - } - - if (!(nonce instanceof Uint8Array) || nonce.length !== 12) { - throw new Error('Nonce should be 12 byte array!') - } - - this._rounds = 20 - // Constants - this._sigma = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574] - - // param construction - this._param = [ - this._sigma[0], - this._sigma[1], - this._sigma[2], - this._sigma[3], - // key - this._get32(key, 0), - this._get32(key, 4), - this._get32(key, 8), - this._get32(key, 12), - this._get32(key, 16), - this._get32(key, 20), - this._get32(key, 24), - this._get32(key, 28), - // counter - counter, - // nonce - this._get32(nonce, 0), - this._get32(nonce, 4), - this._get32(nonce, 8), - ] - - // init 64 byte keystream block // - this._keystream = [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ] - - // internal byte counter // - this._byteCounter = 0 -} - -JSChaCha20.prototype._chacha = function () { - var mix = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - var i = 0 - var b = 0 - - // copy param array to mix // - for (i = 0; i < 16; i++) { - mix[i] = this._param[i] - } - - // mix rounds // - for (i = 0; i < this._rounds; i += 2) { - this._quarterround(mix, 0, 4, 8, 12) - this._quarterround(mix, 1, 5, 9, 13) - this._quarterround(mix, 2, 6, 10, 14) - this._quarterround(mix, 3, 7, 11, 15) - - this._quarterround(mix, 0, 5, 10, 15) - this._quarterround(mix, 1, 6, 11, 12) - this._quarterround(mix, 2, 7, 8, 13) - this._quarterround(mix, 3, 4, 9, 14) - } - - for (i = 0; i < 16; i++) { - // add - mix[i] += this._param[i] - - // store keystream - this._keystream[b++] = mix[i] & 0xff - this._keystream[b++] = (mix[i] >>> 8) & 0xff - this._keystream[b++] = (mix[i] >>> 16) & 0xff - this._keystream[b++] = (mix[i] >>> 24) & 0xff - } -} - -/** - * The basic operation of the ChaCha algorithm is the quarter round. - * It operates on four 32-bit unsigned integers, denoted a, b, c, and d. - * - * @param {Array} output - * @param {number} a - * @param {number} b - * @param {number} c - * @param {number} d - * @private - */ -JSChaCha20.prototype._quarterround = function (output, a, b, c, d) { - output[d] = this._rotl(output[d] ^ (output[a] += output[b]), 16) - output[b] = this._rotl(output[b] ^ (output[c] += output[d]), 12) - output[d] = this._rotl(output[d] ^ (output[a] += output[b]), 8) - output[b] = this._rotl(output[b] ^ (output[c] += output[d]), 7) - - // JavaScript hack to make UINT32 :) // - output[a] >>>= 0 - output[b] >>>= 0 - output[c] >>>= 0 - output[d] >>>= 0 -} - -/** - * Little-endian to uint 32 bytes - * - * @param {Uint8Array|[number]} data - * @param {number} index - * @return {number} - * @private - */ -JSChaCha20.prototype._get32 = function (data, index) { - return data[index++] ^ (data[index++] << 8) ^ (data[index++] << 16) ^ (data[index] << 24) -} - -/** - * Cyclic left rotation - * - * @param {number} data - * @param {number} shift - * @return {number} - * @private - */ -JSChaCha20.prototype._rotl = function (data, shift) { - return (data << shift) | (data >>> (32 - shift)) -} - -/** - * Encrypt data with key and nonce - * - * @param {Uint8Array} data - * @return {Uint8Array} - */ -JSChaCha20.prototype.encrypt = function (data) { - return this._update(data) -} - -/** - * Decrypt data with key and nonce - * - * @param {Uint8Array} data - * @return {Uint8Array} - */ -JSChaCha20.prototype.decrypt = function (data) { - return this._update(data) -} - -/** - * Encrypt or Decrypt data with key and nonce - * - * @param {Uint8Array} data - * @return {Uint8Array} - * @private - */ -JSChaCha20.prototype._update = function (data) { - if (!(data instanceof Uint8Array) || data.length === 0) { - throw new Error('Data should be type of bytes (Uint8Array) and not empty!') - } - - // core function, build block and xor with input data // - for (let i = data.length; i--; ) { - if (this._byteCounter === 0 || this._byteCounter === 64) { - // generate new block // - this._chacha() - // counter increment // - this._param[12]++ - // reset internal counter // - this._byteCounter = 0 - } - data[i] ^= this._keystream[this._byteCounter++] - } - return data -} - -JSChaCha20.prototype.setPosition = function (length) { - // core function, build block and xor with input data // - for (let i = length; i--; ) { - if (this._byteCounter === 0 || this._byteCounter === 64) { - // generate new block // - this._chacha() - // counter increment // - this._param[12]++ - // reset internal counter // - this._byteCounter = 0 - } - this._byteCounter++ - } -} - -// EXPORT // -export default JSChaCha20 diff --git a/node-proxy/src/utils/chaCha20Poly.js b/node-proxy/src/utils/chaCha20Poly.js deleted file mode 100644 index 1225b04..0000000 --- a/node-proxy/src/utils/chaCha20Poly.js +++ /dev/null @@ -1,106 +0,0 @@ -import crypto from 'crypto' -import { Transform } from 'stream' - -class ChaCha20Poly { - constructor(password, sizeSalt) { - this.password = password - this.sizeSalt = sizeSalt - // share you folder passwdOutward safety - this.passwdOutward = password - if (password.length !== 32) { - // add 'RC4' as salt - const sha256 = crypto.createHash('sha256') - const key = sha256.update(password + 'CHA20').digest('hex') - this.passwdOutward = crypto.createHash('md5').update(key).digest('hex') - } - // add salt - const passwdSalt = this.passwdOutward + sizeSalt - // fileHexKey: file passwd,could be share - const fileHexKey = crypto.createHash('sha256').update(passwdSalt).digest() - const iv = crypto.pbkdf2Sync(this.passwdOutward, sizeSalt + '', 10000, 12, 'sha256') - this.cipher = crypto.createCipheriv('chacha20-poly1305', fileHexKey, iv, { - authTagLength: 16, - }) - this.decipher = crypto.createDecipheriv('chacha20-poly1305', fileHexKey, iv, { - authTagLength: 16, - }) - } - - async setPositionAsync(_position) { - const buf = Buffer.alloc(1024) - const position = parseInt(_position / 1024) - const mod = _position % 1024 - for (let i = 0; i < position; i++) { - this.decChaPoly(buf) - } - const modBuf = Buffer.alloc(mod) - for (let i = 0; i < mod; i++) { - this.decChaPoly(modBuf) - } - } - - encryptTransform() { - return new Transform({ - transform: (chunk, encoding, next) => { - next(null, this.encChaPoly(chunk)) - }, - }) - } - - decryptTransform() { - return new Transform({ - transform: (chunk, encoding, next) => { - next(null, this.decChaPoly(chunk, false)) - }, - }) - } - - encChaPoly(data) { - if (typeof data === 'string') { - data = Buffer.from(data, 'utf8') - } - try { - const encrypted = this.cipher.update(data) - return encrypted - } catch (err) { - console.log(err) - } - } - - encChaPolyFinal() { - return this.cipher.final() - } - - getAuthTag() { - return this.cipher.getAuthTag() - } - - decChaPoly(bufferData, authTag) { - try { - if (authTag) { - this.decipher.setAuthTag(authTag) - } - if (authTag === true) { - this.decipher.setAuthTag(this.cipher.getAuthTag()) - } - if (typeof authTag === 'string') { - this.decipher.setAuthTag(Buffer.from(authTag)) - } - - return this.decipher.update(bufferData) - // const decryptData = Buffer.concat([this.decipher.update(bufferData), this.decipher.final()]).toString('utf8') - } catch (err) { - console.log(err) - } - } - - decChaPolyFinal() { - try { - this.decipher.final() - } catch (err) { - console.log(err) - } - } -} - -export default ChaCha20Poly diff --git a/node-proxy/src/utils/common.ts b/node-proxy/src/utils/common.ts new file mode 100644 index 0000000..5ec3148 --- /dev/null +++ b/node-proxy/src/utils/common.ts @@ -0,0 +1,41 @@ +import type { Middleware } from 'koa' + +//平整化 +export const flat = (value: T | T[]): T => { + return Array.isArray(value) ? value[0] : value +} + +//存储请求的部分原始信息,供代理使用 +export function preProxy(serverConfig: WebdavServer, isWebdav: true): Middleware +export function preProxy(serverConfig: AlistServer, isWebdav: false): Middleware +export function preProxy(serverConfig: WebdavServer | AlistServer, isWebdav: boolean): Middleware { + return async (ctx, next) => { + const { serverHost, serverPort, https } = serverConfig + + if (isWebdav) { + // 不能把authorization缓存起来,单线程 + ctx.state.isWebdav = isWebdav + // request.headers.authorization = request.headers.authorization ? (authorization = request.headers.authorization) : authorization + } + + const request = ctx.request + const protocol = https ? 'https' : 'http' + + ctx.state.selfHost = request.headers.host // 原来的host保留,以后可能会用到 + ctx.state.origin = request.headers.origin + request.headers.host = serverHost + ':' + serverPort + ctx.state.urlAddr = `${protocol}://${request.headers.host}${request.url}` + ctx.state.serverAddr = `${protocol}://${request.headers.host}` + ctx.state.serverConfig = serverConfig + + await next() + } +} + +export const sleep = (time: number) => { + return new Promise((resolve) => { + setTimeout(() => { + resolve() + }, time || 3000) + }) +} diff --git a/node-proxy/src/utils/convertFile.ts b/node-proxy/src/utils/convertFile.ts index f701a9e..8604b23 100644 --- a/node-proxy/src/utils/convertFile.ts +++ b/node-proxy/src/utils/convertFile.ts @@ -1,11 +1,12 @@ -'use strict' import fs from 'fs' import path from 'path' import mkdirp from 'mkdirp' import FlowEnc from './flowEnc' -import { encodeName, decodeName } from './commonUtil' +import { logger } from '@/common/logger' +import { encodeName, decodeName } from './cryptoUtil' +//找出文件夹下的文件 export function searchFile(filePath: string) { const fileArray: { size: number; filePath: string }[] = [] const files = fs.readdirSync(filePath) @@ -23,10 +24,10 @@ export function searchFile(filePath: string) { return fileArray } -// encrypt +// 加密文件夹下的全部文件 export async function encryptFile( password: string, - encType: string, + encType: EncryptType, enc: 'enc' | 'dec', encPath: string, outPath?: string, @@ -34,18 +35,18 @@ export async function encryptFile( ) { const start = Date.now() const interval = setInterval(() => { - console.log(new Date(), 'waiting finish!!!') + logger.warn(new Date(), 'waiting finish!!!') }, 2000) if (!path.isAbsolute(encPath)) { encPath = path.join(process.cwd(), encPath) } outPath = outPath || path.join(process.cwd(), 'outFile', Date.now().toString()) - console.log('you input:', password, encType, enc, encPath) + logger.info('文件加密配置: ', password, encType, enc, encPath) if (!fs.existsSync(encPath)) { - console.log('you input filePath is not exists ') + logger.warn('文件夹不存在!') return } - // init outpath dir + // init outPath dir if (!fs.existsSync(outPath)) { mkdirp.sync(outPath) } @@ -84,10 +85,10 @@ export async function encryptFile( // console.log('@@outFilePath', outFilePath, encType, size) const writeStream = fs.createWriteStream(outFilePathTemp) const readStream = fs.createReadStream(filePath) - const promise = new Promise((resolve, reject) => { + const promise = new Promise((resolve) => { readStream.pipe(enc === 'enc' ? flowEnc.encryptTransform() : flowEnc.decryptTransform()).pipe(writeStream) readStream.on('end', () => { - console.log('@@finish filePath', filePath, outFilePathTemp) + logger.info(`加密完成: ${filePath} -> ${outFilePathTemp}`) fs.renameSync(outFilePathTemp, outFilePath) resolve() }) @@ -100,19 +101,21 @@ export async function encryptFile( } await Promise.all(promiseArr) fs.rmSync(tempDir, { recursive: true }) - console.log('@@all finish', ((Date.now() - start) / 1000).toFixed(2) + 's') + logger.info('全部文件加密完成', ((Date.now() - start) / 1000).toFixed(2) + 's') clearInterval(interval) } -export function convertFile(...args: [password: string, encType: string, enc: 'enc' | 'dec', encPath: string, outPath?: string, encName?: string]) { +export function convertFile( + ...args: [password: string, encType: EncryptType, enc: 'enc' | 'dec', encPath: string, outPath?: string, encName?: string] +) { const statTime = Date.now() if (args.length > 3) { encryptFile(...args).then(() => { - console.log('all file finish enc!!! time:', Date.now() - statTime) + logger.info('all file finish enc!!! time:', Date.now() - statTime) process.exit(0) }) } else { - console.error('input error, example param:nodejs-linux passwd12345 rc4 enc ./myfolder /tmp/outPath encname ') + console.error('input error, example param:nodejs-linux passwd12345 rc4 enc ./myFolder /tmp/outPath encName ') process.exit(1) } } diff --git a/node-proxy/src/utils/crc6-8.js b/node-proxy/src/utils/crc6-8.js deleted file mode 100644 index 7e712ba..0000000 --- a/node-proxy/src/utils/crc6-8.js +++ /dev/null @@ -1,104 +0,0 @@ -// "Class" for calculating CRC8 checksums... -function CRCn(num, polynomial, initialValue = 0) { - // constructor takes an optional polynomial type from CRC8.POLY - polynomial = polynomial || CRCn.POLY8.CRC8_DALLAS_MAXIM - if (num === 6) { - this.table = CRCn.generateTable6() - } - if (num === 8) { - this.table = CRCn.generateTable8MAXIM(polynomial) - } - this.initialValue = initialValue -} - -// Returns the 8-bit checksum given an array of byte-sized numbers -CRCn.prototype.checksum = function (byteArray) { - let c = this.initialValue - for (let i = 0; i < byteArray.length; i++) { - c = this.table[(c ^ byteArray[i]) % 256] - } - return c -} - -// returns a lookup table byte array given one of the values from CRC8.POLY -CRCn.generateTable8 = function (polynomial) { - const csTable = [] // 256 max len byte array - for (let i = 0; i < 256; ++i) { - let curr = i - for (let j = 0; j < 8; ++j) { - if ((curr & 0x80) !== 0) { - curr = ((curr << 1) ^ polynomial) % 256 - } else { - curr = (curr << 1) % 256 - } - } - csTable[i] = curr - } - return csTable -} - -// CRC8_DALLAS_MAXIM,跟上面的比较的话,不需要输入和输出的反转 -CRCn.generateTable8MAXIM = function (polynomial) { - const csTable = [] // 256 max len byte array - for (let i = 0; i < 256; ++i) { - let curr = i - for (let j = 0; j < 8; ++j) { - if ((curr & 0x01) !== 0) { - // 0x31=>00110001 翻转 10001100=>0x8C - curr = ((curr >> 1) ^ 0x8c) % 256 - } else { - curr = (curr >> 1) % 256 - } - } - csTable[i] = curr - } - return csTable -} - -// 已经完成了输入和输出的反转,所以输入和输出就不需要单独反转 -CRCn.generateTable6 = function () { - const csTable = [] // 256 max len byte array - for (let i = 0; i < 256; i++) { - let curr = i - for (let j = 0; j < 8; ++j) { - if ((curr & 0x01) !== 0) { - // 0x03(多项式:x6+x+1,00100011),最高位不需要异或,直接去掉 0000 0011 - // 0x30 = (reverse 0x03)>>(8-6) = 00110000 - curr = ((curr >> 1) ^ 0x30) % 256 - } else { - curr = (curr >> 1) % 256 - } - } - csTable[i] = curr - } - return csTable -} - -CRCn.generateTable6test = function () { - const csTable = [] // 256 max len byte array - for (let i = 0; i < 256; i++) { - let curr = i - for (let j = 0; j < 8; ++j) { - if ((curr & 0x80) !== 0) { - // 0x03(多项式:x6+x+1,00100011),最高位不需要异或,直接去掉 - // 0x30 = (reverse 0x03) >> (8-6) - curr = ((curr << 1) ^ 0x03) % 256 - } else { - curr = (curr << 1) % 256 - } - } - csTable[i] = curr >> 2 - } - return csTable -} - -// This "enum" can be used to indicate what kind of CRC8 checksum you will be calculating -CRCn.POLY8 = { - CRC8: 0xd5, - CRC8_CCITT: 0x07, - CRC8_DALLAS_MAXIM: 0x31, - CRC8_SAE_J1850: 0x1d, - CRC_8_WCDMA: 0x9b, -} - -export default CRCn diff --git a/node-proxy/src/utils/PRGAExcute.js b/node-proxy/src/utils/crypto/RPGA/PRGAExecute.js similarity index 84% rename from node-proxy/src/utils/PRGAExcute.js rename to node-proxy/src/utils/crypto/RPGA/PRGAExecute.js index 32632a5..ccb15e3 100644 --- a/node-proxy/src/utils/PRGAExcute.js +++ b/node-proxy/src/utils/crypto/RPGA/PRGAExecute.js @@ -1,4 +1,4 @@ -const PRGAExcute = function (data) { +const PRGAExecute = function (data) { let { sbox: S, i, j, position } = data for (let k = 0; k < position * 1; k++) { i = (i + 1) % 256 @@ -14,6 +14,6 @@ const PRGAExcute = function (data) { process.on('message', function ({ msgId, workerData }) { console.log('来自父进程的消息: ' + JSON.stringify(workerData)) - const resData = PRGAExcute(workerData) + const resData = PRGAExecute(workerData) process.send({ msgId, resData }) }) diff --git a/node-proxy/src/utils/PRGAFork.js b/node-proxy/src/utils/crypto/RPGA/PRGAFork.js similarity index 75% rename from node-proxy/src/utils/PRGAFork.js rename to node-proxy/src/utils/crypto/RPGA/PRGAFork.js index 4fa122f..8069589 100644 --- a/node-proxy/src/utils/PRGAFork.js +++ b/node-proxy/src/utils/crypto/RPGA/PRGAFork.js @@ -1,18 +1,19 @@ +import os from 'os' import { fork } from 'child_process' import { randomUUID } from 'crypto' -import os from 'os' -let index = parseInt(os.cpus().length / 2) + +let index = os.cpus().length / 2 const childList = [] for (let i = index; i--; ) { - const child = fork('./utils/PRGAExcute.js') + const child = fork('./utils/PRGAExecute.js') childList[i] = child child.once('message', ({ msgId, resData }) => { child.emit(msgId, resData) }) } -const PRGAExcuteThread = function (workerData) { +const PRGAExecuteThread = function (workerData) { return new Promise((resolve, reject) => { const child = childList[index++ % childList.length] // 只监听一次,这样就可以重复监听 @@ -24,4 +25,4 @@ const PRGAExcuteThread = function (workerData) { }) } -export default PRGAExcuteThread +export default PRGAExecuteThread diff --git a/node-proxy/src/utils/PRGAThread.js b/node-proxy/src/utils/crypto/RPGA/PRGAThread.js similarity index 88% rename from node-proxy/src/utils/PRGAThread.js rename to node-proxy/src/utils/crypto/RPGA/PRGAThread.js index 8a12006..bb47488 100644 --- a/node-proxy/src/utils/PRGAThread.js +++ b/node-proxy/src/utils/crypto/RPGA/PRGAThread.js @@ -11,7 +11,7 @@ const pkgThreadPath = pkgDirPath + '/PRGAThreadCom.js' // dev threadPath const threadPath = require.resolve('./PRGAThread.js') let index = 0 -let PRGAExcuteThread = null +let PRGAExecuteThread = null // 一定要加上这个,不然会产生递归,创建无数线程 if (isMainThread) { // 避免消耗光资源,加一用于后续的预加载RC4的位置 @@ -36,7 +36,7 @@ if (isMainThread) { }) } - PRGAExcuteThread = function (data) { + PRGAExecuteThread = function (data) { return new Promise((resolve, reject) => { const worker = workerList[index++ % workerNum] const msgId = randomUUID() @@ -52,7 +52,7 @@ if (isMainThread) { // 如果是线程执行了这个文件,就开始处理 if (!isMainThread) { // 异步线程去计算这个位置 - const PRGAExcute = function (data) { + const PRGAExecute = function (data) { let { sbox: S, i, j, position } = data for (let k = 0; k < position; k++) { i = (i + 1) % 256 @@ -68,10 +68,10 @@ if (!isMainThread) { // workerData 由主线程发送过来的信息 parentPort.on('message', ({ msgId, data }) => { const startTime = Date.now() - const resData = PRGAExcute(data) + const resData = PRGAExecute(data) parentPort.postMessage({ msgId, resData }) const time = Date.now() - startTime - console.log('@@@PRGAExcute-end', data.position, Date.now(), '@time:' + time, workerData) + console.log('@@@PRGAExecute-end', data.position, Date.now(), '@time:' + time, workerData) }) } -module.exports = PRGAExcuteThread +module.exports = PRGAExecuteThread diff --git a/node-proxy/src/utils/PRGAThreadCom.js b/node-proxy/src/utils/crypto/RPGA/PRGAThreadCom.js similarity index 79% rename from node-proxy/src/utils/PRGAThreadCom.js rename to node-proxy/src/utils/crypto/RPGA/PRGAThreadCom.js index e9887ad..20d6550 100644 --- a/node-proxy/src/utils/PRGAThreadCom.js +++ b/node-proxy/src/utils/crypto/RPGA/PRGAThreadCom.js @@ -3,7 +3,7 @@ const { isMainThread, parentPort, workerData } = require('worker_threads') // is not MainThread if (!isMainThread) { // Excute the posistion info - const PRGAExcute = function (data) { + const PRGAExecute = function (data) { let { sbox: S, i, j, position } = data for (let k = 0; k < position; k++) { i = (i + 1) % 256 @@ -19,9 +19,9 @@ if (!isMainThread) { // workerData 由主线程发送过来的信息 parentPort.on('message', ({ msgId, data }) => { const startTime = Date.now() - const resData = PRGAExcute(data) + const resData = PRGAExecute(data) parentPort.postMessage({ msgId, resData }) const time = Date.now() - startTime - console.log('@@@PRGAExcute-end', data.position, Date.now(), '@time:' + time, workerData) + console.log('@@@PRGAExecute-end', data.position, Date.now(), '@time:' + time, workerData) }) } diff --git a/node-proxy/src/utils/aesCTR.js b/node-proxy/src/utils/crypto/aesCTR.ts similarity index 60% rename from node-proxy/src/utils/aesCTR.js rename to node-proxy/src/utils/crypto/aesCTR.ts index 91da4a2..133304e 100644 --- a/node-proxy/src/utils/aesCTR.js +++ b/node-proxy/src/utils/crypto/aesCTR.ts @@ -1,45 +1,78 @@ import crypto from 'crypto' import { Transform } from 'stream' -class AesCTR { - constructor(password, sizeSalt) { +class AesCTR implements EncryptFlow { + public password: string + public passwdOutward: string + + private iv: Buffer + private readonly sourceIv: Buffer + private cipher: crypto.Cipher + private readonly key: Buffer + + constructor(password: string, sizeSalt: string) { + if (!sizeSalt) { + throw new Error('salt is null') + } + this.password = password - this.sizeSalt = sizeSalt + '' // check base64 if (password.length !== 32) { this.passwdOutward = crypto.pbkdf2Sync(this.password, 'AES-CTR', 1000, 16, 'sha256').toString('hex') } + // create file aes-ctr key - const passwdSalt = this.passwdOutward + sizeSalt - this.key = crypto.createHash('md5').update(passwdSalt).digest() - this.iv = crypto.createHash('md5').update(this.sizeSalt).digest() - // copy to soureIv - const ivBuffer = Buffer.alloc(this.iv.length) - this.iv.copy(ivBuffer) - this.soureIv = ivBuffer - this.cipher = crypto.createCipheriv('aes-128-ctr', this.key, this.iv) - } + this.key = crypto + .createHash('md5') + .update(this.passwdOutward + sizeSalt) + .digest() + this.iv = crypto.createHash('md5').update(sizeSalt).digest() - encrypt(messageBytes) { - return this.cipher.update(messageBytes) + // copy to sourceIv + this.sourceIv = Buffer.alloc(this.iv.length) + this.iv.copy(this.sourceIv) + this.cipher = crypto.createCipheriv('aes-128-ctr', this.key, this.iv) } - decrypt(messageBytes) { - return this.cipher.update(messageBytes) + incrementIV(increment: number) { + const MAX_UINT32 = 0xffffffff + const incrementBig = ~~(increment / MAX_UINT32) + const incrementLittle = (increment % MAX_UINT32) - incrementBig + // split the 128bits IV in 4 numbers, 32bits each + let overflow = 0 + for (let idx = 0; idx < 4; ++idx) { + let num = this.iv.readUInt32BE(12 - idx * 4) + let inc = overflow + if (idx === 0) inc += incrementLittle + if (idx === 1) inc += incrementBig + num += inc + const numBig = ~~(num / MAX_UINT32) + const numLittle = (num % MAX_UINT32) - numBig + overflow = numBig + this.iv.writeUInt32BE(numLittle, 12 - idx * 4) + } } // reset position - async setPositionAsync(position) { - const ivBuffer = Buffer.alloc(this.soureIv.length) - this.soureIv.copy(ivBuffer) + async setPositionAsync(position: number) { + const ivBuffer = Buffer.alloc(this.sourceIv.length) + this.sourceIv.copy(ivBuffer) this.iv = ivBuffer - const increment = parseInt(position / 16) - this.incrementIV(increment) + this.incrementIV(position / 16) // create new Cipheriv this.cipher = crypto.createCipheriv('aes-128-ctr', this.key, this.iv) - const offset = position % 16 - const buffer = Buffer.alloc(offset) + const buffer = Buffer.alloc(position % 16) this.encrypt(buffer) + + return this + } + + encrypt(plainBuffer: Buffer) { + return this.cipher.update(plainBuffer) + } + + decrypt(encryptedBuffer: Buffer) { + return this.cipher.update(encryptedBuffer) } encryptTransform() { @@ -58,25 +91,6 @@ class AesCTR { }, }) } - - incrementIV(increment) { - const MAX_UINT32 = 0xffffffff - const incrementBig = ~~(increment / MAX_UINT32) - const incrementLittle = (increment % MAX_UINT32) - incrementBig - // split the 128bits IV in 4 numbers, 32bits each - let overflow = 0 - for (let idx = 0; idx < 4; ++idx) { - let num = this.iv.readUInt32BE(12 - idx * 4) - let inc = overflow - if (idx === 0) inc += incrementLittle - if (idx === 1) inc += incrementBig - num += inc - const numBig = ~~(num / MAX_UINT32) - const numLittle = (num % MAX_UINT32) - numBig - overflow = numBig - this.iv.writeUInt32BE(numLittle, 12 - idx * 4) - } - } } export default AesCTR diff --git a/node-proxy/src/utils/crypto/crc6-8.ts b/node-proxy/src/utils/crypto/crc6-8.ts new file mode 100644 index 0000000..8b5cf8b --- /dev/null +++ b/node-proxy/src/utils/crypto/crc6-8.ts @@ -0,0 +1,64 @@ +// "Class" for calculating CRC8 checksums... +class CRCn { + private readonly table: number[] + private readonly initialValue: number + + constructor(num: number, initialValue = 0) { + // constructor takes an optional polynomial type from CRC8.POLY + if (num === 6) { + this.table = CRCn.generateTable6() + } + if (num === 8) { + this.table = CRCn.generateTable8MAXIM() + } + this.initialValue = initialValue + } + + // CRC8_DALLAS_MAXIM,跟上面的比较的话,不需要输入和输出的反转 + static generateTable8MAXIM(): number[] { + const csTable: number[] = [] // 256 max len byte array + for (let i = 0; i < 256; ++i) { + let curr = i + for (let j = 0; j < 8; ++j) { + if ((curr & 0x01) !== 0) { + // 0x31=>00110001 翻转 10001100=>0x8C + curr = ((curr >> 1) ^ 0x8c) % 256 + } else { + curr = (curr >> 1) % 256 + } + } + csTable[i] = curr + } + return csTable + } + + // 已经完成了输入和输出的反转,所以输入和输出就不需要单独反转 + static generateTable6(): number[] { + const csTable: number[] = [] // 256 max len byte array + for (let i = 0; i < 256; i++) { + let curr = i + for (let j = 0; j < 8; ++j) { + if ((curr & 0x01) !== 0) { + // 0x03(多项式:x6+x+1,00100011),最高位不需要异或,直接去掉 0000 0011 + // 0x30 = (reverse 0x03)>>(8-6) = 00110000 + curr = ((curr >> 1) ^ 0x30) % 256 + } else { + curr = (curr >> 1) % 256 + } + } + csTable[i] = curr + } + return csTable + } + + // Returns the 8-bit checksum given an array of byte-sized numbers + checksum(byteArray: Buffer): number { + let c = this.initialValue + for (let i = 0; i < byteArray.length; i++) { + c = this.table[(c ^ byteArray[i]) % 256] + } + return c + } +} + +export default CRCn diff --git a/node-proxy/src/utils/crypto/mixBase64.ts b/node-proxy/src/utils/crypto/mixBase64.ts new file mode 100644 index 0000000..0abd14b --- /dev/null +++ b/node-proxy/src/utils/crypto/mixBase64.ts @@ -0,0 +1,126 @@ +import crypto from 'crypto' + +const source = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-~+' + +// use sha1 str to init + +// 自定义的base64加密 +class MixBase64 implements EncryptMethod { + password: string + passwdOutward: string + + private mapChars: Record = {} + + private readonly chars: string[] + + constructor(password: string, salt = 'mix64') { + const secret = password.length === 64 ? password : this.initKSA(password + salt) + this.chars = secret.split('') + this.chars.forEach((e, index) => { + this.mapChars[e] = index + }) + // 编码加密 + } + + initKSA(password: string) { + const key = crypto.createHash('sha256').update(password).digest() + const K = [] + // 对S表进行初始赋值 + const sbox = [] + const sourceKey = source.split('') + // 对S表进行初始赋值 + for (let i = 0; i < source.length; i++) { + sbox[i] = i + } + // 用种子密钥对K表进行填充 + for (let i = 0; i < source.length; i++) { + K[i] = key[i % key.length] + } + // 对S表进行置换 + for (let i = 0, j = 0; i < source.length; i++) { + j = (j + sbox[i] + K[i]) % source.length + const temp = sbox[i] + sbox[i] = sbox[j] + sbox[j] = temp + } + let secret = '' + for (const i of sbox) { + secret += sourceKey[i] + } + return secret + } + + static getSourceChar(index: number) { + // 不能使用 = 号,url穿参数不支持 + return source.split('')[index] + } + + encrypt(plainBuffer: Buffer) { + let result = '' + let arr: Buffer + let bt: Buffer + let char: string + + for (let i = 0; i < plainBuffer.length; i += 3) { + if (i + 3 > plainBuffer.length) { + arr = plainBuffer.subarray(i, plainBuffer.length) + break + } + + bt = plainBuffer.subarray(i, i + 3) + char = + this.chars[bt[0] >> 2] + + this.chars[((bt[0] & 3) << 4) | (bt[1] >> 4)] + + this.chars[((bt[1] & 15) << 2) | (bt[2] >> 6)] + + this.chars[bt[2] & 63] + result += char + } + + if (plainBuffer.length % 3 === 1) { + char = this.chars[arr[0] >> 2] + this.chars[(arr[0] & 3) << 4] + this.chars[64] + this.chars[64] + result += char + } else if (plainBuffer.length % 3 === 2) { + char = this.chars[arr[0] >> 2] + this.chars[((arr[0] & 3) << 4) | (arr[1] >> 4)] + this.chars[(arr[1] & 15) << 2] + this.chars[64] + result += char + } + + return Buffer.from(result) + } + + // 编码解密 + decrypt(encryptedBuffer: Buffer) { + const base64Str = encryptedBuffer.toString() + + let size = (base64Str.length / 4) * 3 + let j = 0 + + if (~base64Str.indexOf(this.chars[64] + '' + this.chars[64])) { + size -= 2 + } else if (~base64Str.indexOf(this.chars[64])) { + size -= 1 + } + + const buffer = Buffer.alloc(size) + let enc1: number + let enc2: number + let enc3: number + let enc4: number + let i = 0 + while (i < base64Str.length) { + enc1 = this.mapChars[base64Str.charAt(i++)] + enc2 = this.mapChars[base64Str.charAt(i++)] + enc3 = this.mapChars[base64Str.charAt(i++)] + enc4 = this.mapChars[base64Str.charAt(i++)] + buffer.writeUInt8((enc1 << 2) | (enc2 >> 4), j++) + if (enc3 !== 64) { + buffer.writeUInt8(((enc2 & 15) << 4) | (enc3 >> 2), j++) + } + if (enc4 !== 64) { + buffer.writeUInt8(((enc3 & 3) << 6) | enc4, j++) + } + } + return buffer + } +} + +export default MixBase64 diff --git a/node-proxy/src/utils/mixEnc.js b/node-proxy/src/utils/crypto/mixEnc.ts similarity index 53% rename from node-proxy/src/utils/mixEnc.js rename to node-proxy/src/utils/crypto/mixEnc.ts index a66850a..3583ccc 100644 --- a/node-proxy/src/utils/mixEnc.js +++ b/node-proxy/src/utils/crypto/mixEnc.ts @@ -1,13 +1,18 @@ -'use strict' - import crypto from 'crypto' import { Transform } from 'stream' +import { logger } from '@/common/logger' /** * 混淆算法,加密强度低,容易因为文件特征被破解。可以提升encode长度来对抗 */ -class MixEnc { - constructor(password) { +class MixEnc implements EncryptFlow { + public password: string + public passwdOutward: string + + private readonly encode: Buffer + private readonly decode: Buffer + + constructor(password: string) { this.password = password // 说明是输入encode的秘钥,用于找回文件加解密 this.passwdOutward = password @@ -15,11 +20,12 @@ class MixEnc { if (password.length !== 32) { this.passwdOutward = crypto.pbkdf2Sync(this.password, 'MIX', 1000, 16, 'sha256').toString('hex') } - console.log('MixEnc.passwdOutward', this.passwdOutward) + const encode = crypto.createHash('sha256').update(this.passwdOutward).digest() const decode = [] const length = encode.length const decodeCheck = {} + for (let i = 0; i < length; i++) { const enc = encode[i] ^ i // 这里会产生冲突 @@ -37,20 +43,30 @@ class MixEnc { } } } + this.encode = encode this.decode = Buffer.from(decode) - // console.log('@encode:', this.encode.toString('hex')) - // console.log('@decode:', this.decode.toString('hex')) } - // MD5 - md5(content) { - const md5 = crypto.createHash('md5') - return md5.update(this.passwdOutward + content).digest('hex') + async setPositionAsync(position: number) { + logger.info('in the mix ', position) + return this } - async setPositionAsync() { - console.log('in the mix ') + // 加密方法 + encrypt(plainBuffer: Buffer) { + for (let i = plainBuffer.length; i--; ) { + plainBuffer[i] ^= this.encode[plainBuffer[i] % 32] + } + return plainBuffer + } + + // 解密方法 + decrypt(encryptedBuffer: Buffer) { + for (let i = encryptedBuffer.length; i--; ) { + encryptedBuffer[i] ^= this.decode[encryptedBuffer[i] % 32] + } + return encryptedBuffer } // 加密流转换 @@ -58,7 +74,7 @@ class MixEnc { return new Transform({ // 匿名函数确保this是指向 FlowEnc transform: (chunk, encoding, next) => { - next(null, this.encodeData(chunk)) + next(null, this.encrypt(chunk)) }, }) } @@ -68,50 +84,10 @@ class MixEnc { return new Transform({ transform: (chunk, encoding, next) => { // this.push() 用push也可以 - next(null, this.decodeData(chunk)) + next(null, this.decrypt(chunk)) }, }) } - - // 加密方法 - encodeData(data) { - data = Buffer.from(data) - for (let i = data.length; i--; ) { - data[i] ^= this.encode[data[i] % 32] - } - return data - } - - // 解密方法 - decodeData(data) { - for (let i = data.length; i--; ) { - data[i] ^= this.decode[data[i] % 32] - } - return data - } -} - -// 检查 encode 是否正确使用的 -MixEnc.checkEncode = function (_encode) { - const encode = Buffer.from(_encode, 'hex') - const length = encode.length - const decodeCheck = {} - for (let i = 0; i < encode.length; i++) { - const enc = encode[i] ^ i - // 这里会产生冲突 - if (!decodeCheck[enc % length]) { - decodeCheck[enc % length] = encode[i] - } else { - return null - } - } - return encode } -// const flowEnc = new MixEnc('abc1234', 1234) -// const encode = flowEnc.encodeData('测试的明文加密1234¥%#') -// const nwe = new MixEnc('5fc8482ac3a7b3fd9325566dfdd31673', 1234) -// const decode = nwe.decodeData(encode) -// console.log('@@@decode', encode, decode.toString()) - export default MixEnc diff --git a/node-proxy/src/utils/rc4Md5.js b/node-proxy/src/utils/crypto/rc4Md5.ts similarity index 64% rename from node-proxy/src/utils/rc4Md5.js rename to node-proxy/src/utils/crypto/rc4Md5.ts index 677a7c6..029cfc9 100644 --- a/node-proxy/src/utils/rc4Md5.js +++ b/node-proxy/src/utils/crypto/rc4Md5.ts @@ -1,8 +1,6 @@ -'use strict' - import crypto from 'crypto' import { Transform } from 'stream' -import PRGAExcuteThread from './PRGAThread' +import PRGAExecuteThread from '@/utils/crypto/RPGA/PRGAThread' /** * RC4算法,安全性相对好很多 @@ -11,18 +9,23 @@ import PRGAExcuteThread from './PRGAThread' // Reset sbox every 100W bytes const segmentPosition = 100 * 10000 -class Rc4Md5 { +class Rc4Md5 implements EncryptFlow { + public password: string + public passwdOutward: string + + private i = 0 + private j = 0 + private position = 0 + private sbox: number[] = [] + + private readonly fileHexKey: string + // password,salt: ensure that each file has a different password - constructor(password, sizeSalt) { + constructor(password: string, sizeSalt: string) { if (!sizeSalt) { throw new Error('salt is null') } - this.position = 0 - this.i = 0 - this.j = 0 - this.sbox = [] this.password = password - this.sizeSalt = sizeSalt // share you folder passwdOutward safety this.passwdOutward = password // check base64,create passwdOutward @@ -30,14 +33,16 @@ class Rc4Md5 { this.passwdOutward = crypto.pbkdf2Sync(this.password, 'RC4', 1000, 16, 'sha256').toString('hex') } // add salt - const passwdSalt = this.passwdOutward + sizeSalt - // fileHexKey: file passwd,could be share - this.fileHexKey = crypto.createHash('md5').update(passwdSalt).digest('hex') + this.fileHexKey = crypto + .createHash('md5') + .update(this.passwdOutward + sizeSalt) + .digest('hex') + this.resetKSA() } resetKSA() { - const offset = parseInt(this.position / segmentPosition) * segmentPosition + const offset = (this.position / segmentPosition) * segmentPosition const buf = Buffer.alloc(4) buf.writeInt32BE(offset) const rc4Key = Buffer.from(this.fileHexKey, 'hex') @@ -48,38 +53,72 @@ class Rc4Md5 { this.initKSA(rc4Key) } - // reset sbox,i,j - setPosition(newPosition = 0) { - newPosition *= 1 - this.position = newPosition - this.resetKSA() - // use PRGAExecPostion no change potision - this.PRGAExecPostion(newPosition % segmentPosition) - return this + initKSA(key: Buffer) { + const K = [] + // init sbox + for (let i = 0; i < 256; i++) { + this.sbox[i] = i + } + // 用种子密钥对K表进行填充 + for (let i = 0; i < 256; i++) { + K[i] = key[i % key.length] + } + // 对S表进行置换 + for (let i = 0, j = 0; i < 256; i++) { + j = (j + this.sbox[i] + K[i]) % 256 + const temp = this.sbox[i] + this.sbox[i] = this.sbox[j] + this.sbox[j] = temp + } + this.i = 0 + this.j = 0 + } + + // 初始化长度,因为有一些文件下载 Range: bytes=3600-5000 + PRGAExecute(plainBuffer: Buffer) { + let { sbox: S, i, j } = this + for (let k = 0; k < plainBuffer.length; k++) { + i = (i + 1) % 256 + j = (j + S[i]) % 256 + // swap + const temp = S[i] + S[i] = S[j] + S[j] = temp + plainBuffer[k] ^= S[(S[i] + S[j]) % 256] + if (++this.position % segmentPosition === 0) { + // reset sbox initKSA + this.resetKSA() + i = this.i + j = this.j + S = this.sbox + } + } + // save the i,j + this.i = i + this.j = j + return plainBuffer } // reset sbox,i,j, in other thread async setPositionAsync(newPosition = 0) { - // return this.setPosition(newPosition) - newPosition *= 1 this.position = newPosition this.resetKSA() const { sbox, i, j } = this - const data = await PRGAExcuteThread({ sbox, i, j, position: newPosition % segmentPosition }) + const data = await PRGAExecuteThread({ sbox, i, j, position: newPosition % segmentPosition }) this.sbox = data.sbox this.i = data.i this.j = data.j - return data - } - encryptText(plainTextLen) { - const plainBuffer = Buffer.from(plainTextLen) - return this.encrypt(plainBuffer) + return this } // 加解密都是同一个方法 - encrypt(plainBuffer) { - return this.PRGAExcute(plainBuffer) + encrypt(plainBuffer: Buffer) { + return this.PRGAExecute(plainBuffer) + } + + decrypt(encryptedBuffer: Buffer) { + return this.PRGAExecute(encryptedBuffer) } // 加密流转换 @@ -101,72 +140,6 @@ class Rc4Md5 { }, }) } - - // 初始化长度,因为有一些文件下载 Range: bytes=3600-5000 - PRGAExcute(plainBuffer) { - let { sbox: S, i, j } = this - for (let k = 0; k < plainBuffer.length; k++) { - i = (i + 1) % 256 - j = (j + S[i]) % 256 - // swap - const temp = S[i] - S[i] = S[j] - S[j] = temp - plainBuffer[k] ^= S[(S[i] + S[j]) % 256] - if (++this.position % segmentPosition === 0) { - // reset sbox initKSA - this.resetKSA() - i = this.i - j = this.j - S = this.sbox - } - } - // save the i,j - this.i = i - this.j = j - return plainBuffer - } - - PRGAExecPostion(plainLen) { - let { sbox: S, i, j } = this - // k-- is inefficient - for (let k = 0; k < plainLen; k++) { - i = (i + 1) % 256 - j = (j + S[i]) % 256 - const temp = S[i] - S[i] = S[j] - S[j] = temp - } - this.i = i - this.j = j - } - - initKSA(key) { - const K = [] - // init sbox - for (let i = 0; i < 256; i++) { - this.sbox[i] = i - } - // 用种子密钥对K表进行填充 - for (let i = 0; i < 256; i++) { - K[i] = key[i % key.length] - } - // 对S表进行置换 - for (let i = 0, j = 0; i < 256; i++) { - j = (j + this.sbox[i] + K[i]) % 256 - const temp = this.sbox[i] - this.sbox[i] = this.sbox[j] - this.sbox[j] = temp - } - this.i = 0 - this.j = 0 - } } -// const rc4 = new Rc4('123456') -// const buffer = rc4.encryptText('abc') -// // 要记得重置 -// const plainBy = rc4.resetSbox().encrypt(buffer) -// console.log('@@@', buffer, Buffer.from(plainBy).toString('utf-8')) - export default Rc4Md5 diff --git a/node-proxy/src/utils/commonUtil.ts b/node-proxy/src/utils/cryptoUtil.ts similarity index 70% rename from node-proxy/src/utils/commonUtil.ts rename to node-proxy/src/utils/cryptoUtil.ts index 37af043..381ab29 100644 --- a/node-proxy/src/utils/commonUtil.ts +++ b/node-proxy/src/utils/cryptoUtil.ts @@ -1,28 +1,30 @@ -import { pathToRegexp } from 'path-to-regexp' -import FlowEnc from './flowEnc' import path from 'path' -import MixBase64 from './mixBase64' -import Crcn from './crc6-8' +import { pathToRegexp } from 'path-to-regexp' + +import Crcn from './crypto/crc6-8' +import MixBase64 from './crypto/mixBase64' +import { getPassWdOutward } from './flowEnc' +import { logger } from '@/common/logger' const crc6 = new Crcn(6) const origPrefix = 'orig_' // check file name, return real name -export function convertRealName(password: string, encType: string, pathText: string) { +export function convertRealName(password: string, encType: EncryptType, pathText: string) { const fileName = path.basename(pathText) if (fileName.indexOf(origPrefix) === 0) { return fileName.replace(origPrefix, '') } // try encode name, fileName don't need decodeURI,encodeUrl func can't encode that like '(' '!' in nodejs const ext = path.extname(fileName) + logger.info(`获取原文件名: ${decodeURIComponent(fileName)} ${encType} ${password}`) const encName = encodeName(password, encType, decodeURIComponent(fileName)) - console.log('@@decodeURI(fileName)', decodeURIComponent(fileName)) return encName + ext } -// if file name has encrypt, return show name -export function convertShowName(password: string, encType: string, pathText: string) { +// if file name has encrypted, return show name +export function convertShowName(password: string, encType: EncryptType, pathText: string) { const fileName = path.basename(decodeURIComponent(pathText)) const ext = path.extname(fileName) const encName = fileName.replace(ext, '') @@ -45,20 +47,20 @@ export function pathExec(encPath: string[], url: string) { return null } -export function encodeName(password: string, encType: string, plainName: string) { - const passwdOutward = FlowEnc.getPassWdOutward(password, encType) +export function encodeName(password: string, encType: EncryptType, plainName: string) { + const passwdOutward = getPassWdOutward(password, encType) // randomStr const mix64 = new MixBase64(passwdOutward) - let encodeName = mix64.encode(plainName) + let encodeName = mix64.encrypt(Buffer.from(plainName)).toString() const crc6Bit = crc6.checksum(Buffer.from(encodeName + passwdOutward)) const crc6Check = MixBase64.getSourceChar(crc6Bit) encodeName += crc6Check return encodeName } -export function decodeName(password: string, encType: string, encodeName: string) { +export function decodeName(password: string, encType: EncryptType, encodeName: string) { const crc6Check = encodeName.substring(encodeName.length - 1) - const passwdOutward = FlowEnc.getPassWdOutward(password, encType) + const passwdOutward = getPassWdOutward(password, encType) const mix64 = new MixBase64(passwdOutward) // start dec const subEncName = encodeName.substring(0, encodeName.length - 1) @@ -70,19 +72,19 @@ export function decodeName(password: string, encType: string, encodeName: string // event pass crc6,it maybe decode error, like this name '68758PICxAd_1024-666 - 副本33.png' let decodeStr = null try { - decodeStr = mix64.decode(subEncName).toString('utf8') + decodeStr = mix64.decrypt(Buffer.from(subEncName)).toString() } catch (e) { - console.log('@@mix64 decode error', subEncName) + logger.error('@@mix64 decode error', subEncName) } return decodeStr } -export function encodeFolderName(password: string, encType: string, folderPasswd: string, folderEncType: string) { +export function encodeFolderName(password: string, encType: EncryptType, folderPasswd: string, folderEncType: string) { const passwdInfo = folderEncType + '_' + folderPasswd return encodeName(password, encType, passwdInfo) } -export function decodeFolderName(password: string, encType: string, encodeName: string) { +export function decodeFolderName(password: string, encType: EncryptType, encodeName: string) { const arr = encodeName.split('_') if (arr.length < 2) { return false @@ -98,7 +100,13 @@ export function decodeFolderName(password: string, encType: string, encodeName: } // 检查 -export function pathFindPasswd(passwdList: PasswdInfo[], url: string) { +export function pathFindPasswd( + passwdList: PasswdInfo[], + url: string +): { + passwdInfo: PasswdInfo | null + pathInfo: RegExpExecArray | null +} { for (const passwdInfo of passwdList) { for (const filePath of passwdInfo.encPath) { const result = passwdInfo.enable ? pathToRegexp(new RegExp(filePath)).exec(url) : null @@ -120,5 +128,5 @@ export function pathFindPasswd(passwdList: PasswdInfo[], url: string) { } } } - return {} + return { passwdInfo: null, pathInfo: null } } diff --git a/node-proxy/src/utils/flowEnc.js b/node-proxy/src/utils/flowEnc.js deleted file mode 100644 index a2375e8..0000000 --- a/node-proxy/src/utils/flowEnc.js +++ /dev/null @@ -1,65 +0,0 @@ -'use strict' - -import MixEnc from './mixEnc' -import Rc4Md5 from './rc4Md5' -import AesCTR from './aesCTR' - -const cachePasswdOutward = {} - -class FlowEnc { - constructor(password, encryptType = 'mix', fileSize = 0) { - fileSize *= 1 - let encryptFlow = null - if (encryptType === 'mix') { - console.log('@@mix', encryptType) - encryptFlow = new MixEnc(password, fileSize) - this.passwdOutward = encryptFlow.passwdOutward - } - if (encryptType === 'rc4') { - console.log('@@rc4', encryptType, fileSize) - encryptFlow = new Rc4Md5(password, fileSize) - this.passwdOutward = encryptFlow.passwdOutward - } - if (encryptType === 'aesctr') { - console.log('@@AesCTR', encryptType, fileSize) - encryptFlow = new AesCTR(password, fileSize) - this.passwdOutward = encryptFlow.passwdOutward - } - if (encryptType === null) { - throw new Error('FlowEnc error') - } - cachePasswdOutward[password + encryptType] = this.passwdOutward - this.encryptFlow = encryptFlow - this.encryptType = encryptType - } - - async setPosition(position) { - await this.encryptFlow.setPositionAsync(position) - } - - // 加密流转换 - encryptTransform() { - return this.encryptFlow.encryptTransform() - } - - decryptTransform() { - return this.encryptFlow.decryptTransform() - } -} - -FlowEnc.getPassWdOutward = function (password, encryptType) { - const passwdOutward = cachePasswdOutward[password + encryptType] - if (passwdOutward) { - return passwdOutward - } - const flowEnc = new FlowEnc(password, encryptType, 1) - return flowEnc.passwdOutward -} - -// const flowEnc = new FlowEnc('abc1234') -// const encode = flowEnc.encodeData('测试的明文加密1234¥%#') -// const decode = flowEnc.decodeData(encode) -// console.log('@@@decode', encode, decode.toString()) -// console.log(new FlowEnc('e10adc3949ba56abbe5be95ff90a8636')) - -export default FlowEnc diff --git a/node-proxy/src/utils/flowEnc.ts b/node-proxy/src/utils/flowEnc.ts new file mode 100644 index 0000000..733830b --- /dev/null +++ b/node-proxy/src/utils/flowEnc.ts @@ -0,0 +1,64 @@ +import { Transform } from 'stream' + +import MixEnc from '@/utils/crypto/mixEnc' +import Rc4Md5 from '@/utils/crypto/rc4Md5' +import AesCTR from '@/utils/crypto/aesCTR' +import { logger } from '@/common/logger' + +const cachePasswdOutward = {} + +class FlowEnc { + public passwdOutward: string + public encryptType: EncryptType + public encryptFlow: EncryptFlow + + constructor(password: string, encryptType: EncryptType = 'mix', fileSize = 0) { + switch (encryptType) { + case 'mix': + logger.info('加解密算法: ', encryptType) + this.encryptFlow = new MixEnc(password) + break + case 'rc4': + logger.info('加解密算法: ', encryptType, fileSize) + this.encryptFlow = new Rc4Md5(password, fileSize.toString()) + break + case 'aesctr': + logger.info('加解密算法: ', encryptType, fileSize) + this.encryptFlow = new AesCTR(password, fileSize.toString()) + break + } + + if (!this.encryptFlow) { + throw new Error('FlowEnc error') + } + + this.encryptType = encryptType + this.passwdOutward = this.encryptFlow.passwdOutward + cachePasswdOutward[password + encryptType] = this.passwdOutward + } + + async setPosition(position: number) { + await this.encryptFlow.setPositionAsync(position) + } + + // 加密流转换 + encryptTransform(): Transform { + return this.encryptFlow.encryptTransform() + } + + decryptTransform(): Transform { + return this.encryptFlow.decryptTransform() + } +} + +export const getPassWdOutward = function (password: string, encryptType: EncryptType) { + let passwdOutward = cachePasswdOutward[password + encryptType] + if (!passwdOutward) { + const flowEnc = new FlowEnc(password, encryptType, 1) + passwdOutward = flowEnc.passwdOutward + } + + return passwdOutward +} + +export default FlowEnc diff --git a/node-proxy/src/utils/httpClient.js b/node-proxy/src/utils/httpClient.ts similarity index 62% rename from node-proxy/src/utils/httpClient.js rename to node-proxy/src/utils/httpClient.ts index e62b0fd..04c0655 100644 --- a/node-proxy/src/utils/httpClient.js +++ b/node-proxy/src/utils/httpClient.ts @@ -1,21 +1,101 @@ +import path from 'path' import http from 'http' import https from 'node:https' +import { Transform } from 'stream' import crypto, { randomUUID } from 'crypto' -import levelDB from './levelDB' -import path from 'path' -import { decodeName } from './commonUtil' -// import { pathExec } from './commonUtil' -const Agent = http.Agent -const Agents = https.Agent + +import { flat } from '@/utils/common' +import levelDB from '@/dao/levelDB' +import { decodeName } from '@/utils/cryptoUtil' +import { logger } from '@/common/logger' // 默认maxFreeSockets=256 -const httpsAgent = new Agents({ keepAlive: true }) -const httpAgent = new Agent({ keepAlive: true }) +const httpsAgent = new https.Agent({ keepAlive: true }) +const httpAgent = new http.Agent({ keepAlive: true }) + +export async function httpClient({ + urlAddr, + reqBody, + request, + response, +}: { + urlAddr: string + reqBody?: string + request: http.IncomingMessage + response?: http.ServerResponse +}): Promise { + const { method, headers, url } = request + logger.info('代理请求发起: ', method, urlAddr) + logger.trace('http请求头: ', headers) + // 创建请求 + const httpRequest = ~urlAddr.indexOf('https') ? https : http + const options = { + method, + headers, + agent: ~urlAddr.indexOf('https') ? httpsAgent : httpAgent, + rejectUnauthorized: false, + } + + return new Promise((resolve) => { + // 处理重定向的请求,让下载的流量经过代理服务器 + const httpReq = httpRequest.request(urlAddr, options, async (httpResp) => { + logger.info('http响应接收: 状态码', httpResp.statusCode) + logger.trace('http响应头:', httpResp.headers) + if (response) { + response.statusCode = httpResp.statusCode + for (const key in httpResp.headers) { + response.setHeader(key, httpResp.headers[key]) + } + } + + let result = '' + httpResp + .on('data', (chunk) => { + result += chunk + }) + .on('end', () => { + resolve(result) + logger.info('代理请求结束结束: ', url) + }) + }) + + httpReq.on('error', (err) => { + logger.error('http请求出错: ', err) + }) + + // check request type + if (!reqBody) { + url ? request.pipe(httpReq) : httpReq.end() + return + } + + httpReq.write(reqBody) + httpReq.end() + }) +} -export async function httpProxy(request, response, encryptTransform, decryptTransform) { - const { method, headers, urlAddr, passwdInfo, url, fileSize } = request +export async function httpFlowClient({ + urlAddr, + passwdInfo, + fileSize, + request, + response, + encryptTransform, + decryptTransform, +}: { + urlAddr: string + passwdInfo: PasswdInfo + fileSize: number + request: http.IncomingMessage + response?: http.ServerResponse + encryptTransform?: Transform + decryptTransform?: Transform +}) { + const { method, headers, url } = request const reqId = randomUUID() - console.log('@@request_info: ', reqId, method, urlAddr, headers, !!encryptTransform, !!decryptTransform) + logger.info(`代理请求(${reqId})发起: `, method, urlAddr) + logger.info(`httpFlow(${reqId})加解密: `, `流加密${!!encryptTransform}`, `流解密${!!decryptTransform}`) + logger.trace(`httpFlow(${reqId})请求头: `, headers) // 创建请求 const options = { method, @@ -24,10 +104,11 @@ export async function httpProxy(request, response, encryptTransform, decryptTran rejectUnauthorized: false, } const httpRequest = ~urlAddr.indexOf('https') ? https : http - return new Promise((resolve, reject) => { + return new Promise((resolve) => { // 处理重定向的请求,让下载的流量经过代理服务器 const httpReq = httpRequest.request(urlAddr, options, async (httpResp) => { - console.log('@@statusCode', reqId, httpResp.statusCode, httpResp.headers) + logger.info(`httpFlow(${reqId})响应接收: 状态码`, httpResp.statusCode) + logger.trace(`httpFlow(${reqId})响应头: `, httpResp.headers) response.statusCode = httpResp.statusCode if (response.statusCode % 300 < 5) { // 可能出现304,redirectUrl = undefined @@ -35,12 +116,12 @@ export async function httpProxy(request, response, encryptTransform, decryptTran // 百度云盘不是https,坑爹,因为天翼云会多次302,所以这里要保持,跳转后的路径保持跟上次一致,经过本服务器代理就可以解密 if (decryptTransform && passwdInfo.enable) { const key = crypto.randomUUID() - console.log() + await levelDB.setExpire(key, { redirectUrl, passwdInfo, fileSize }, 60 * 60 * 72) // 缓存起来,默认3天,足够下载和观看了 // 、Referer httpResp.headers.location = `/redirect/${key}?decode=1&lastUrl=${encodeURIComponent(url)}` } - console.log('302 redirectUrl:', redirectUrl) + logger.info(`httpFlow(${reqId})重定向到:`, redirectUrl) } else if (httpResp.headers['content-range'] && httpResp.statusCode === 200) { response.statusCode = 206 } @@ -52,10 +133,12 @@ export async function httpProxy(request, response, encryptTransform, decryptTran if (method === 'GET' && response.statusCode === 200 && passwdInfo && passwdInfo.enable && passwdInfo.encName) { let fileName = decodeURIComponent(path.basename(url)) fileName = decodeName(passwdInfo.password, passwdInfo.encType, fileName.replace(path.extname(fileName), '')) + if (fileName) { let cd = response.getHeader('content-disposition') - cd = cd ? cd.replace(/filename\*?=[^=;]*;?/g, '') : '' - console.log('解密文件名...', reqId, fileName) + cd = flat(cd) + cd = cd ? cd.toString().replace(/filename\*?=[^=;]*;?/g, '') : '' + logger.info(`httpFlow(${reqId})解密后文件名:`, fileName) response.setHeader('content-disposition', cd + `filename*=UTF-8''${encodeURIComponent(fileName)};`) } } @@ -65,7 +148,7 @@ export async function httpProxy(request, response, encryptTransform, decryptTran resolve() }) .on('close', () => { - console.log('响应关闭...', reqId, urlAddr) + logger.info(`代理请求(${reqId})结束:`, urlAddr) // response.destroy() if (decryptTransform) decryptTransform.destroy() }) @@ -73,59 +156,14 @@ export async function httpProxy(request, response, encryptTransform, decryptTran decryptTransform ? httpResp.pipe(decryptTransform).pipe(response) : httpResp.pipe(response) }) httpReq.on('error', (err) => { - console.log('@@httpProxy request error ', reqId, err, urlAddr, headers) + logger.error(`httpFlow(${reqId})请求出错:`, urlAddr, err) }) // 是否需要加密 encryptTransform ? request.pipe(encryptTransform).pipe(httpReq) : request.pipe(httpReq) // 重定向的请求 关闭时 关闭被重定向的请求 response.on('close', () => { - console.log('响应关闭...', reqId, url) + logger.trace(`响应(${reqId})关闭: `, url) httpReq.destroy() }) }) } - -export async function httpClient(request, response) { - const { method, headers, urlAddr, reqBody, url } = request - console.log('@@request_client: ', method, urlAddr, headers) - // 创建请求 - const options = { - method, - headers, - agent: ~urlAddr.indexOf('https') ? httpsAgent : httpAgent, - rejectUnauthorized: false, - } - const httpRequest = ~urlAddr.indexOf('https') ? https : http - return new Promise((resolve, reject) => { - // 处理重定向的请求,让下载的流量经过代理服务器 - const httpReq = httpRequest.request(urlAddr, options, async (httpResp) => { - console.log('@@statusCode', httpResp.statusCode, httpResp.headers) - if (response) { - response.statusCode = httpResp.statusCode - for (const key in httpResp.headers) { - response.setHeader(key, httpResp.headers[key]) - } - } - let result = '' - httpResp - .on('data', (chunk) => { - result += chunk - }) - .on('end', () => { - resolve(result) - console.log('httpResp响应结束...', url) - }) - }) - httpReq.on('error', (err) => { - console.log('@@httpClient request error ', err) - }) - // check request type - if (!reqBody) { - url ? request.pipe(httpReq) : httpReq.end() - return - } - // 发送请求 - typeof reqBody === 'string' ? httpReq.write(reqBody) : httpReq.write(JSON.stringify(reqBody)) - httpReq.end() - }) -} diff --git a/node-proxy/src/utils/levelDB.js b/node-proxy/src/utils/levelDB.js deleted file mode 100644 index baf8db0..0000000 --- a/node-proxy/src/utils/levelDB.js +++ /dev/null @@ -1,62 +0,0 @@ -import Datastore from 'nedb-promises' - -// let datastore = Datastore.create('/path/to/db.db') -/** - * 封装新方法 - */ -class Nedb { - constructor(dbFile) { - this.datastore = Datastore.create(dbFile) - } - - async load() { - await this.datastore.load() - } - - // 新增过期设置 - async setValue(key, value) { - await this.datastore.removeMany({ key }) - console.log('@@setValue', key, value ) - await this.datastore.insert({ key, expire: -1, value }) - } - - async setExpire(key, value, second = 6 * 10) { - await this.datastore.removeMany({ key }) - const expire = Date.now() + second * 1000 - await this.datastore.insert({ key, expire, value }) - } - - async getValue(key) { - try { - const { expire, value } = await this.datastore.findOne({ key }) - // 没有限制时间 - if (expire < 0) { - return value - } - if (expire && expire > Date.now()) { - return value - } - } catch (e) { - return null - } - // 删除key - this.datastore.remove(key) - return null - } -} - -const nedb = new Nedb(process.cwd() + '/conf/nedb/datafile') - -// 定时清除过期的数据 -setInterval(async () => { - const allData = await nedb.datastore.find({}) - for (const data of allData) { - const { key, expire } = data - if (expire && expire > 0 && expire < Date.now()) { - console.log('@@expire:', key, expire, Date.now()) - nedb.datastore.remove({ key }) - } - } -}, 30 * 1000) - -export default nedb diff --git a/node-proxy/src/utils/mixBase64.js b/node-proxy/src/utils/mixBase64.js deleted file mode 100644 index 5a5a035..0000000 --- a/node-proxy/src/utils/mixBase64.js +++ /dev/null @@ -1,162 +0,0 @@ -'use strict' -import crypto from 'crypto' - -const source = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-~+' - -// use sha1 str to init -function initKSA(passwd) { - let key = passwd - if (typeof passwd === 'string') { - key = crypto.createHash('sha256').update(passwd).digest() - } - const K = [] - // 对S表进行初始赋值 - const sbox = [] - const sourceKey = source.split('') - // 对S表进行初始赋值 - for (let i = 0; i < source.length; i++) { - sbox[i] = i - } - // 用种子密钥对K表进行填充 - for (let i = 0; i < source.length; i++) { - K[i] = key[i % key.length] - } - // 对S表进行置换 - for (let i = 0, j = 0; i < source.length; i++) { - j = (j + sbox[i] + K[i]) % source.length - const temp = sbox[i] - sbox[i] = sbox[j] - sbox[j] = temp - } - let secret = '' - for (const i of sbox) { - secret += sourceKey[i] - } - return secret -} - -// 自定义的base64加密 -function MixBase64(passwd, salt = 'mix64') { - // 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' - const secret = passwd.length === 64 ? passwd : initKSA(passwd + salt) - const chars = secret.split('') - const mapChars = {} - chars.forEach((e, index) => { - mapChars[e] = index - }) - this.chars = chars - // 编码加密 - this.encode = function (bufferOrStr, encoding = 'utf-8') { - const buffer = bufferOrStr instanceof Buffer ? bufferOrStr : Buffer.from(bufferOrStr, encoding) - let result = '' - let arr = [] - let bt = [] - let char - for (let i = 0; i < buffer.length; i += 3) { - if (i + 3 > buffer.length) { - arr = buffer.subarray(i, buffer.length) - break - } - bt = buffer.subarray(i, i + 3) - char = chars[bt[0] >> 2] + chars[((bt[0] & 3) << 4) | (bt[1] >> 4)] + chars[((bt[1] & 15) << 2) | (bt[2] >> 6)] + chars[bt[2] & 63] - result += char - } - if (buffer.length % 3 === 1) { - char = chars[arr[0] >> 2] + chars[(arr[0] & 3) << 4] + chars[64] + chars[64] - result += char - } else if (buffer.length % 3 === 2) { - char = chars[arr[0] >> 2] + chars[((arr[0] & 3) << 4) | (arr[1] >> 4)] + chars[(arr[1] & 15) << 2] + chars[64] - result += char - } - return result - } - // 编码解密 - this.decode = function (base64Str) { - let size = (base64Str.length / 4) * 3 - let j = 0 - if (~base64Str.indexOf(chars[64] + '' + chars[64])) { - size -= 2 - } else if (~base64Str.indexOf(chars[64])) { - size -= 1 - } - const buffer = Buffer.alloc(size) - let enc1 - let enc2 - let enc3 - let enc4 - let i = 0 - while (i < base64Str.length) { - enc1 = mapChars[base64Str.charAt(i++)] - enc2 = mapChars[base64Str.charAt(i++)] - enc3 = mapChars[base64Str.charAt(i++)] - enc4 = mapChars[base64Str.charAt(i++)] - buffer.writeUInt8((enc1 << 2) | (enc2 >> 4), j++) - if (enc3 !== 64) { - buffer.writeUInt8(((enc2 & 15) << 4) | (enc3 >> 2), j++) - } - if (enc4 !== 64) { - buffer.writeUInt8(((enc3 & 3) << 6) | enc4, j++) - } - } - return buffer - } -} - -MixBase64.sourceChars = source.split('') - -// plaintext check bit -MixBase64.getCheckBit = function (text) { - const bufferArr = Buffer.from(text) - let count = 0 - for (const num of bufferArr) { - count += num - } - count %= 64 - return MixBase64.sourceChars[count] -} - -MixBase64.randomSecret = function () { - // 不能使用 = 号,url穿参数不支持 - const source = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-~+' - const chars = source.split('') - const newChars = [] - while (chars.length > 0) { - const index = Math.floor(Math.random() * chars.length) - newChars.push(chars[index]) - chars.splice(index, 1) - } - return newChars.join('') -} - -MixBase64.randomStr = function (length) { - // 不能使用 = 号,url穿参数不支持 - const chars = source.split('') - const newChars = [] - while (length-- > 0) { - const index = Math.floor(Math.random() * chars.length) - newChars.push(chars[index]) - chars.splice(index, 1) - } - return newChars.join('') -} - -MixBase64.getSourceChar = function (index) { - // 不能使用 = 号,url穿参数不支持 - return source.split('')[index] -} - -MixBase64.initKSA = initKSA - -export default MixBase64 -// Buffer.from(str, 'base64').toString('base64') === str - -// function test() { -// const secret = '123456' -// const mybase64 = new MixBase64(secret) -// const test = 'test123456' -// const encodeStr = mybase64.encode(test) -// const bufferData = mybase64.decode(encodeStr) -// const encodeFromBuffer = mybase64.encode(bufferData) -// console.log(encodeStr, encodeFromBuffer, bufferData.toString()) -// } -// test() diff --git a/node-proxy/src/utils/webdavClient.js b/node-proxy/src/utils/webdavClient.js deleted file mode 100644 index 4413c7b..0000000 --- a/node-proxy/src/utils/webdavClient.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict' - -import { httpClient } from './httpClient' -import { XMLParser } from 'fast-xml-parser' - -// get file info from webdav -export async function getWebdavFileInfo(urlAddr, authorization) { - const request = { - method: 'PROPFIND', - headers: { - depth: 1, - authorization, - }, - urlAddr, - } - const parser = new XMLParser({ removeNSPrefix: true }) - try { - const XMLdata = await httpClient(request) - const respBody = parser.parse(XMLdata) - const res = respBody.multistatus.response - const filePath = res.href - const size = res.propstat.prop.getcontentlength || 0 - const name = res.propstat.prop.displayname - const isDir = size === 0 - return { size, name, is_dir: isDir, path: filePath } - } catch (e) { - console.log('@@webdavFileInfo_error:', urlAddr) - } - return null -} diff --git a/node-proxy/src/utils/webdavClient.ts b/node-proxy/src/utils/webdavClient.ts new file mode 100644 index 0000000..e34a34f --- /dev/null +++ b/node-proxy/src/utils/webdavClient.ts @@ -0,0 +1,38 @@ +import http from 'http' + +import { XMLParser } from 'fast-xml-parser' + +import { httpClient } from '@/utils/httpClient' +import { logger } from '@/common/logger' + +// get file info from webdav +export async function getWebdavFileInfo(urlAddr: string, authorization: string): Promise { + //eslint-disable-next-line + //@ts-ignore + const request: http.IncomingMessage = { + method: 'PROPFIND', + headers: { + depth: '1', + authorization, + }, + } + + const parser = new XMLParser({ removeNSPrefix: true }) + + try { + const XMLData = await httpClient({ + urlAddr, + request, + }) + const respBody = parser.parse(XMLData) + const res = respBody.multistatus.response + + const size = Number(res.propstat.prop.getcontentlength || 0) + const name = res.propstat.prop.displayname + + return { size, name, is_dir: size === 0, path: res.href } + } catch (e) { + logger.error('@@webdavFileInfo_error:', urlAddr) + } + return null +} diff --git a/node-proxy/test/chachaTest.js b/node-proxy/test/chachaTest.js deleted file mode 100644 index 95a6d76..0000000 --- a/node-proxy/test/chachaTest.js +++ /dev/null @@ -1,63 +0,0 @@ -import crypto from 'crypto' -import ChaCha20 from '.@/utils/chaCha20' -import Rc4 from '.@/utils/rc4Md5' -import ChaCha20Poly from '.@/utils/chaCha20Poly' -import AesCTR from '.@/utils/aesCTR' - -let decrypted = null -const rc4System = crypto.createCipheriv('rc4', 'MY SECRET KEY', '') -const decipher = crypto.createDecipheriv('rc4', 'MY SECRET KEY', '') -decrypted = rc4System.update(Buffer.from('test rc4 encrypt'), 'binary', 'hex') -// decrypted += encipher.final('hex') -console.log('@@@@', decrypted, decipher.update(decrypted, 'hex', 'utf8')) - -const iv = crypto.randomBytes(12) -const keyenc = crypto.randomBytes(32) - -const keyaes = crypto.randomBytes(16) -const ivaes = crypto.randomBytes(16) -const chaCha20System = new ChaCha20Poly(keyenc, iv) -const chaCha20Local = new ChaCha20(keyenc, iv) -const rc4Local = new Rc4('1234', 33) -const aseCTRSystem = new AesCTR(keyaes, ivaes) - -const textPlain = `test测试性能速度-测试性能速度-测试性能速度-测试性能速度-测试性能速度-测试性能速度-测试性能速度 - 测试性能速度-测试性能速度-测试性能速度-测试性能速度-测试性能速度-测试性能速度-测试性能速度 - 测试性能速度-测试性能速度-测试性能速度-测试性能速度-测试性能速度-测试性能速度-测试性能速度 - 测试性能速度-测试性能速度-测试性能速度-测试性能速度-测试性能速度-测试性能速度-测试性能速度 - 测试性能速度-测试性能速度-测试性能速度-测试性能速度-测试性能速度-测试性能速度-测试性能速度 - 测试性能速度-测试性能速度-测试性能速度-测试性能速度-测试性能速度-测试性能速度-测试性能速度 - 测试性能速度-测试性能速度-测试性能速度-测试性能速度-测试性能速度-测试-` - -const textBuf = Buffer.from(textPlain) -console.log('@textBuf.length', textBuf.length) - -const startDate = Date.now() -for (let i = 0; i < 1; i++) { - // chaCha20Local.encrypt(textBuf) - // rc4Local.encrypt(textBuf) - // chaCha20System.encChaPoly(textBuf) - // aseCTRSystem.encrypt(textBuf) -} -const rc4BUf = Buffer.from(textPlain) -rc4Local.setPosition(0) -const encdata = rc4Local.encrypt(rc4BUf) -rc4Local.setPosition(0) -console.log('encd--------ata rc4BUf', rc4BUf.length) -const position = 124 -rc4Local.setPosition(position) -// rc4Local.encrypt(encdata.subarray(0, position)) -// - -const startPosition = Date.now() -// chaCha20Local.setPosition(12345678) -// rc4Local.setPosition(123456788) -// console.log('startPosition time: ' + (Date.now() - startPosition)) - -rc4Local.setPositionAsync(position).then((res) => { - console.log('@@@encdata 33: ', rc4Local.encrypt(encdata.subarray(position, rc4BUf.length)).toString('utf-8')) - - // console.log('rc4 startPosition async time: ' + (Date.now() - startPosition)) -}) - -console.log('test time: ' + (Date.now() - startDate)) diff --git a/node-proxy/tsconfig.json b/node-proxy/tsconfig.json index 18c5377..248ce3b 100644 --- a/node-proxy/tsconfig.json +++ b/node-proxy/tsconfig.json @@ -1,16 +1,24 @@ { "compilerOptions": { "module": "NodeNext", - "target": "ES5", - "types": ["./src/@types"], + "target": "ESNext", + "types": [ + "./src/@types" + ], "baseUrl": ".", - "paths": { "@/*": ["src/*"] }, - "allowSyntheticDefaultImports": true, + "paths": { + "@/*": [ + "src/*" + ] + }, // "allowImportingTsExtensions": true, - "allowJs": true, + "allowJs": false, "outDir": "./dist" }, - "exclude": ["node_modules", "**/node_modules/*", "dist"], + "include": [ + "src/**/*.ts", + "src/**/*.d.ts" + ], "ts-node": { "compilerOptions": { "module": "NodeNext" diff --git a/node-proxy/webdavTest.js b/node-proxy/webdavTest.js deleted file mode 100644 index f1ade68..0000000 --- a/node-proxy/webdavTest.js +++ /dev/null @@ -1,6 +0,0 @@ -const url = 'http://192.168.8.21:5344/dav/aliyun' -const client = { - username: 'admin', - password: 'YiuNH7ly', -} -console.log(url, client) diff --git a/node-proxy/webpack.config.ts b/node-proxy/webpack.config.ts index 88192a2..5b5932b 100644 --- a/node-proxy/webpack.config.ts +++ b/node-proxy/webpack.config.ts @@ -10,7 +10,7 @@ const output = { } export default () => { return { - entry: { index: path.resolve('./app.js'), PRGAThreadCom: path.resolve('./src/utils/PRGAThreadCom.js') }, + entry: { index: path.resolve('./app.ts'), PRGAThreadCom: path.resolve('./src/utils/PRGAThreadCom.js') }, output, module: { rules: [