This commit is contained in:
2026-01-17 01:13:55 +08:00
parent 327f115ef2
commit cc043bfd7e
17 changed files with 768 additions and 790 deletions

View File

@@ -155,15 +155,6 @@ export type AssistantConfigData = {
* share 是对外共享 pages 目录下的页面
*/
auth?: AuthPermission;
/**
* HTTPS 证书配置, 启用后,助手服务会启用 HTTPS 服务, 默认 HTTP
* 理论上也不需要https因为可以通过反向代理实现https
*/
https?: {
type?: 'https' | 'http';
keyPath?: string; // 证书私钥路径
certPath?: string; // 证书路径
};
};
let assistantConfig: AssistantConfigData;
type AssistantConfigOptions = {

View File

@@ -1,180 +0,0 @@
import { createCert } from '@kevisual/router/sign';
import path from 'node:path';
import fs from 'node:fs';
import { AssistantConfig } from '../config/index.ts';
import { checkFileExists } from '../file/index.ts';
import { chalk } from '@/module/chalk.ts';
import dayjs from 'dayjs';
type Attributes = {
name: string;
value: string;
};
type AltNames = {
type: number;
value?: string;
ip?: string;
};
export class HttpsPem {
assistantConfig: AssistantConfig;
key: string;
cert: string;
isHttps = false;
constructor(assistantConfig: AssistantConfig) {
this.assistantConfig = assistantConfig;
this.#initKeyCert();
}
#initKeyCert() {
this.assistantConfig.checkMounted();
const config = this.assistantConfig.getConfig();
if (config.https) {
const httpsType = config.https?.type || 'https';
if (httpsType !== 'https') {
// console.log(chalk.yellow('当前配置文件 https.type 不是 https, 不使用证书'));
return;
}
this.isHttps = true;
if (config.https.keyPath) {
const keyPath = config.https.keyPath;
const certPath = config.https.certPath;
if (checkFileExists(keyPath) && checkFileExists(certPath)) {
this.key = fs.readFileSync(keyPath, 'utf-8');
this.cert = fs.readFileSync(certPath, 'utf-8');
console.log(chalk.green('使用配置文件 https.keyPath 和 https.certPath 的证书'), keyPath, certPath);
return;
} else {
console.log(chalk.red('证书路径不存在,请检查配置文件 https.keyPath 和 https.certPath 是否正确'));
}
}
}
if(!this.isHttps) return;
const { key, cert } = this.getCert();
this.key = key;
this.cert = cert;
}
getPemDir() {
const configDir = this.assistantConfig.configPath?.configDir || process.cwd();
const pemDir = path.join(configDir, 'pem');
if (!checkFileExists(pemDir)) {
fs.mkdirSync(pemDir, { recursive: true });
}
return pemDir;
}
getCert() {
if (!this.assistantConfig.init) return;
const pemDir = this.getPemDir();
const pemPath = {
key: path.join(pemDir, 'https-private-key.pem'),
cert: path.join(pemDir, 'https-cert.pem'),
config: path.join(pemDir, 'https-config.json'),
};
const writeCreate = (opts: { key: string; cert: string; data: { createTime: number; expireTime: number } }) => {
fs.writeFileSync(pemPath.key, opts.key);
fs.writeFileSync(pemPath.cert, opts.cert);
fs.writeFileSync(pemPath.config, JSON.stringify(opts.data, null, 2));
};
if (!checkFileExists(pemPath.key) || !checkFileExists(pemPath.cert)) {
const { key, cert, data } = this.createCert();
writeCreate({ key, cert, data });
console.log(chalk.green('证书创建成功,浏览器需要导入当前证书'));
return {
key,
cert,
};
}
if (!checkFileExists(pemPath.config)) {
const data = this.createExpireData();
fs.writeFileSync(pemPath.config, JSON.stringify(data, null, 2));
}
const key = fs.readFileSync(pemPath.key, 'utf-8');
const cert = fs.readFileSync(pemPath.cert, 'utf-8');
const config = fs.readFileSync(pemPath.config, 'utf-8');
let expireTime = 0;
try {
const data = JSON.parse(config);
expireTime = data.expireTime;
if (typeof expireTime !== 'number') {
throw new Error('expireTime is not a number');
}
} catch (error) {
console.log(chalk.red('证书配置文件损坏,重新生成证书'));
}
const now = new Date().getTime();
if (now > expireTime) {
this.removeCert();
const { key, cert, data } = this.createCert();
writeCreate({ key, cert, data });
console.log(chalk.green('证书更新成功, 浏览器需要重新导入当前证书'));
return {
key,
cert,
};
}
return {
key,
cert,
};
}
createExpireData() {
const expireTime = new Date().getTime() + 365 * 24 * 60 * 60 * 1000;
const expireDate = dayjs(expireTime).format('YYYY-MM-DD HH:mm:ss');
return {
description: '手动导入证书到浏览器, https-cert.pem文件, 具体使用教程访问 https://kevisual.cn/root/pem-docs/',
createTime: new Date().getTime(),
expireDate,
expireTime,
};
}
/*
* 重新生成证书
*/
removeCert() {
const pemDir = this.getPemDir();
const pemPath = {
key: path.join(pemDir, 'https-private-key.pem'),
cert: path.join(pemDir, 'https-cert.pem'),
};
const oldPath = {
key: path.join(pemDir, 'https-private-key.pem.bak'),
cert: path.join(pemDir, 'https-cert.pem.bak'),
};
if (checkFileExists(pemPath.key)) {
fs.renameSync(pemPath.key, oldPath.key);
}
if (checkFileExists(pemPath.cert)) {
fs.renameSync(pemPath.cert, oldPath.cert);
}
}
/*
* 创建证书
* @param attrs 证书属性
* @param altNames 证书备用名称
*/
createCert(attrs?: Attributes[], altNames?: AltNames[]) {
const attributes = attrs || [];
const altNamesList = altNames || [];
const { key, cert } = createCert(
[
{
name: 'commonName',
value: 'localhost',
},
{
name: 'organizationName',
value: 'kevisual',
},
...attributes,
],
altNamesList,
);
const data = this.createExpireData();
return {
key,
cert,
data: {
...data,
},
};
}
}

View File

@@ -2,7 +2,6 @@ import * as http from 'http';
import * as fs from 'fs';
import { isBun } from './utils.ts';
import { Readable } from 'stream';
/**
* 文件流管道传输函数
* 将指定文件的内容通过流的方式传输给客户端响应
@@ -112,4 +111,17 @@ export const pipeProxyReq = async (req: http.IncomingMessage, proxyReq: http.Cli
// Node.js标准环境下直接使用pipe进行流传输
req.pipe(proxyReq, { end: true });
}
}
export const pipeBusboy = async (req: http.IncomingMessage, res: http.ServerResponse, busboy: any) => {
if (isBun) {
// @ts-ignore
const bunRequest = req.bun.request;
const arrayBuffer = await bunRequest.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
busboy.end(buffer);
} else {
req.pipe(busboy);
}
}