feat(api-server): Add HTTPS support and custom certificate configuration (#3874)

This commit is contained in:
MohanadAhmed
2025-12-19 09:10:12 +02:00
committed by GitHub
parent b1d2112bfc
commit 58a19cdaa2
5 changed files with 103 additions and 11 deletions

View File

@ -323,6 +323,22 @@
},
"port": {
"label": "Port"
},
"https": {
"label": "HTTPS & Certificates",
"submenu": {
"enable-https": {
"label": "Enable HTTPS"
},
"cert": {
"label": "Certificate file (.crt/.pem)",
"dialogTitle": "Select HTTPS certificate file"
},
"key": {
"label": "Private key file (.key/.pem)",
"dialogTitle": "Select HTTPS private key file"
}
}
}
},
"name": "API Server [Beta]",

View File

@ -1,3 +1,7 @@
import { createServer as createHttpServer } from 'node:http';
import { createServer as createHttpsServer } from 'node:https';
import { readFileSync } from 'node:fs';
import { jwt } from 'hono/jwt';
import { OpenAPIHono as Hono } from '@hono/zod-openapi';
import { cors } from 'hono/cors';
@ -48,22 +52,26 @@ export const backend = createBackend<BackendType, APIServerConfig>({
(newVolumeState: VolumeState) => (this.volumeState = newVolumeState),
);
this.run(config.hostname, config.port);
this.run(config);
},
stop() {
this.end();
},
onConfigChange(config) {
const old = this.oldConfig;
if (
this.oldConfig?.hostname === config.hostname &&
this.oldConfig?.port === config.port
old?.hostname === config.hostname &&
old?.port === config.port &&
old?.useHttps === config.useHttps &&
old?.certPath === config.certPath &&
old?.keyPath === config.keyPath
) {
this.oldConfig = config;
return;
}
this.end();
this.run(config.hostname, config.port);
this.run(config);
this.oldConfig = config;
},
@ -153,15 +161,30 @@ export const backend = createBackend<BackendType, APIServerConfig>({
this.injectWebSocket = ws.injectWebSocket.bind(this);
},
run(hostname, port) {
run(config) {
if (!this.app) return;
try {
this.server = serve({
fetch: this.app.fetch.bind(this.app),
port,
hostname,
});
const serveOptions =
config.useHttps && config.certPath && config.keyPath
? {
fetch: this.app.fetch.bind(this.app),
port: config.port,
hostname: config.hostname,
createServer: createHttpsServer,
serverOptions: {
key: readFileSync(config.keyPath),
cert: readFileSync(config.certPath),
},
}
: {
fetch: this.app.fetch.bind(this.app),
port: config.port,
hostname: config.hostname,
createServer: createHttpServer,
};
this.server = serve(serveOptions);
if (this.injectWebSocket && this.server) {
this.injectWebSocket(this.server);

View File

@ -17,6 +17,6 @@ export type BackendType = {
injectWebSocket?: (server: ReturnType<typeof serve>) => void;
init: (ctx: BackendContext<APIServerConfig>) => void;
run: (hostname: string, port: number) => void;
run: (config: APIServerConfig) => void;
end: () => void;
};

View File

@ -11,6 +11,9 @@ export interface APIServerConfig {
secret: string;
authorizedClients: string[];
useHttps: boolean;
certPath: string;
keyPath: string;
}
export const defaultAPIServerConfig: APIServerConfig = {
@ -21,4 +24,7 @@ export const defaultAPIServerConfig: APIServerConfig = {
secret: Date.now().toString(36),
authorizedClients: [],
useHttps: false,
certPath: '',
keyPath: '',
};

View File

@ -1,3 +1,4 @@
import { dialog } from 'electron';
import prompt from 'custom-electron-prompt';
import { t } from '@/i18n';
@ -93,5 +94,51 @@ export const onMenu = async ({
},
],
},
{
label: t('plugins.api-server.menu.https.label'),
type: 'submenu',
submenu: [
{
label: t('plugins.api-server.menu.https.submenu.enable-https.label'),
type: 'checkbox',
checked: config.useHttps,
click(menuItem) {
setConfig({ ...config, useHttps: menuItem.checked });
},
},
{
label: t('plugins.api-server.menu.https.submenu.cert.label'),
type: 'normal',
async click() {
const config = await getConfig();
const result = await dialog.showOpenDialog(window, {
title: t(
'plugins.api-server.menu.https.submenu.cert.dialogTitle',
),
filters: [{ name: 'Certificate', extensions: ['crt', 'pem'] }],
properties: ['openFile'],
});
if (!result.canceled && result.filePaths.length > 0) {
setConfig({ ...config, certPath: result.filePaths[0] });
}
},
},
{
label: t('plugins.api-server.menu.https.submenu.key.label'),
type: 'normal',
async click() {
const config = await getConfig();
const result = await dialog.showOpenDialog(window, {
title: t('plugins.api-server.menu.https.submenu.key.dialogTitle'),
filters: [{ name: 'Private Key', extensions: ['key', 'pem'] }],
properties: ['openFile'],
});
if (!result.canceled && result.filePaths.length > 0) {
setConfig({ ...config, keyPath: result.filePaths[0] });
}
},
},
],
},
];
};