moved self-signed certificate creation to server-side

Massimo Melina committed Nov 24, 2024 at 23:42 UTC 3711589063ea50611922a0146d4537932b3e5e67
5 files changed +51 -46
admin/index.html
-1
@@ -3,7 +3,6 @@
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 - <script async src='https://unpkg.com/node-forge@1.3.1/dist/forge.min.js'></script>
6 <title>HFS Admin-panel</title>
7 <script type="module" src="/src/index.ts"></script>
8 <link rel="icon" type="image/svg+xml" href="/hfs-logo-icon.svg" />
admin/src/OptionsPage.ts
+3 -26
@@ -8,7 +8,7 @@ import { Link as RouterLink } from 'react-router-dom'
8 import { CardMembership, EditNote, Refresh, Warning } from '@mui/icons-material'
9 import { adminApis } from '../../src/adminApis'
10 import {
11 - MAX_TILE_SIZE, REPO_URL, SORT_BY_OPTIONS, THEME_OPTIONS, PORT_DISABLED, CFG, IMAGE_FILEMASK,
11 + MAX_TILE_SIZE, REPO_URL, SORT_BY_OPTIONS, THEME_OPTIONS, CFG, IMAGE_FILEMASK,
12 Dict, md, wait, with_, try_, ipForUrl,
13 } from './misc'
14 import { iconTooltip, InLink, LinkBtn, propsForModifiedValues, wikiLink, useBreakpoint, NetmaskField, WildcardsSupported } from './mui'
@@ -401,9 +401,8 @@ export async function suggestMakingCert() {
401 const stop = waitDialog()
402 try {
403 await wait(50) // give time to start animation before cpu intensive task
404 - const saved = await apiCall('save_pem', await makeCert({}))
404 + const saved = await apiCall('make_self_signed_cert', { fileName: 'self' })
405 stop()
406 - await apiCall('set_config', { values: saved })
406 if (loaded) // when undefined we are not in this page
407 Object.assign(loaded, saved)
408 setTimeout(exposedReloadStatus!, 1000) // give some time for backend to apply
@@ -414,26 +413,4 @@ export async function suggestMakingCert() {
413 finally { stop() }
414 }
415 })
417 -}
418 -
419 -async function makeCert(attributes: Record<string, string>) {
420 - // this relies on having loaded node-forge/dist/forge.min.js
421 - const { pki } = (window as any).forge
422 - const keys = pki.rsa.generateKeyPair(2048);
423 - const cert = pki.createCertificate();
424 - cert.publicKey = keys.publicKey
425 - cert.serialNumber = '01'
426 - cert.validity.notBefore = new Date()
427 - cert.validity.notAfter = new Date()
428 - cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1)
429 -
430 - const attrs = Object.entries(attributes).map(x => ({ name: x[0], value: x[1] }))
431 - cert.setSubject(attrs)
432 - cert.setIssuer(attrs)
433 - cert.sign(keys.privateKey)
434 -
435 - return {
436 - cert: pki.certificateToPem(cert),
437 - private_key: pki.privateKeyToPem(keys.privateKey),
438 - }
439 -}
416 +}
\ No newline at end of file
package.json
+4 -2
@@ -91,9 +91,10 @@
91 "lodash": "^4.17.21",
92 "minimist": "^1.2.6",
93 "nat-upnp-rejetto": "^2.1.0",
94 + "node-forge": "^1.3.1",
95 "open": "^8.4.0",
95 - "qr-creator": "^1.0.0",
96 "picomatch": "^3.0.1",
97 + "qr-creator": "^1.0.0",
98 "tssrp6a": "^3.0.0",
99 "unzip-stream": "^0.3.4",
100 "valtio": "^1.10.3",
@@ -113,15 +114,16 @@
114 "@types/minimist": "^1.2.2",
115 "@types/mocha": "^9.0.0",
116 "@types/node": "^18.17.14",
117 + "@types/node-forge": "^1.3.11",
118 "@types/picomatch": "^2.3.3",
119 "@types/tough-cookie": "^4.0.2",
120 "@types/unzipper": "^0.10.5",
121 + "@yao-pkg/pkg": "^5.15.0",
122 "cross-env": "^7.0.3",
123 "koa-better-http-proxy": "^0.2.9",
124 "mocha": "^9.1.3",
125 "nm-prune": "^5.0.0",
126 "nodemon": "^2.0.15",
124 - "@yao-pkg/pkg": "^5.15.0",
127 "tough-cookie": "^4.0.0",
128 "ts-node": "^10.4.0"
129 }
src/adminApis.ts
+4 -17
@@ -4,13 +4,8 @@ import { ApiError, ApiHandler, ApiHandlers } from './apiMiddleware'
4 import { configFile, defineConfig, getWholeConfig, setConfig } from './config'
5 import { getBaseUrlOrDefault, getIps, getServerStatus, getUrls } from './listen'
6 import {
7 - API_VERSION,
8 - BUILD_TIMESTAMP,
9 - COMPATIBLE_API_VERSION,
10 - HFS_STARTED,
11 - IS_WINDOWS,
12 - VERSION,
13 - HTTP_UNAUTHORIZED, HTTP_BAD_REQUEST, HTTP_SERVER_ERROR, HTTP_FORBIDDEN
7 + API_VERSION, BUILD_TIMESTAMP, COMPATIBLE_API_VERSION, HFS_STARTED, IS_WINDOWS, VERSION,
8 + HTTP_UNAUTHORIZED, HTTP_SERVER_ERROR, HTTP_FORBIDDEN
9 } from './const'
10 import vfsApis from './api.vfs'
11 import accountsApis from './api.accounts'
@@ -19,12 +14,12 @@ import monitorApis from './api.monitor'
14 import langApis from './api.lang'
15 import netApis from './api.net'
16 import logApis from './api.log'
17 +import certApis from './api.cert'
18 import { getConnections } from './connections'
19 import { apiAssertTypes, debounceAsync, isLocalHost, makeNetMatcher, typedEntries, waitFor } from './misc'
20 import { accountCanLoginAdmin, accountsConfig } from './perm'
21 import Koa from 'koa'
22 import { cloudflareDetected, getProxyDetected } from './middlewares'
27 -import { writeFile } from 'fs/promises'
23 import { execFile } from 'child_process'
24 import { promisify } from 'util'
25 import { customHtmlSections, customHtml, saveCustomHtml } from './customHtml'
@@ -49,6 +44,7 @@ export const adminApis = {
44 ...langApis,
45 ...netApis,
46 ...logApis,
47 + ...certApis,
48 get_dynamic_dns_error,
49
50 async set_config({ values }) {
@@ -140,15 +136,6 @@ export const adminApis = {
136 }
137 },
138
143 - async save_pem({ cert, private_key, name='self' }) {
144 - if (!cert || !private_key)
145 - return new ApiError(HTTP_BAD_REQUEST)
146 - const files = { cert: name + '.cer', private_key: name + '.key' }
147 - await writeFile(files.private_key, private_key)
148 - await writeFile(files.cert, cert)
149 - return files
150 - },
151 -
139 async add_block({ merge, ip, expire, comment }: BlockingRule & { merge?: Partial<BlockingRule> }) {
140 apiAssertTypes({
141 string: { ip },
src/api.cert.ts new
+40
@@ -0,0 +1,40 @@
1 +import { apiAssertTypes } from './misc'
2 +import { ApiHandlers } from './apiMiddleware'
3 +import { writeFile } from 'fs/promises'
4 +import { pki } from 'node-forge'
5 +import { setConfig } from './config'
6 +
7 +export default {
8 +
9 + async make_self_signed_cert({ attributes, fileName }: { fileName?: string, attributes?: Record<string, string> }) {
10 + apiAssertTypes({
11 + object_undefined: { attributes },
12 + string_undefined: { fileName },
13 + })
14 +
15 + const keys = pki.rsa.generateKeyPair(2048)
16 + const cert = pki.createCertificate()
17 + cert.publicKey = keys.publicKey
18 + cert.serialNumber = '01'
19 + cert.validity.notBefore = new Date()
20 + cert.validity.notAfter = new Date()
21 + cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1)
22 +
23 + const attrs = Object.entries(attributes || {}).map(x => ({ name: x[0], value: x[1] }))
24 + cert.setSubject(attrs)
25 + cert.setIssuer(attrs)
26 + cert.sign(keys.privateKey)
27 + const ret = {
28 + cert: pki.certificateToPem(cert),
29 + private_key: pki.privateKeyToPem(keys.privateKey),
30 + }
31 + if (!fileName)
32 + return ret
33 + const configs = { cert: fileName + '.cer', private_key: fileName + '.key' }
34 + await writeFile(configs.private_key, ret.private_key)
35 + await writeFile(configs.cert, ret.cert)
36 + setConfig(configs)
37 + return configs
38 + }
39 +
40 +} satisfies ApiHandlers
\ No newline at end of file