config.admin_port and admin_network
Massimo Melina committed
Feb 5, 2022 at 16:15 UTC
58eaa394dd93dc37052c82ed1260c1a2847e561b
15 files changed
+142
-75
README.md
+2
@@ -64,6 +64,8 @@ When not specified, default values will be used.
64
Supported entries are:
65
- `port` where to accept http connections. Default is 80.
66
- `vfs` the files and folders you want to expose. For details see the dedicated following section.
67
+- `admin_port` the port where to reach admin interface. Default is 63636.
68
+- `admin_network` the network address where to reach admin interface. Default is 127.0.0.1 .
69
- `log` path of the log file. Default is `access.log`.
70
- `error_log` path of the log file for errors. Default is `error.log`.
71
- `errors_in_main_log` if you want to use a single file for both kind of entries. Default is false.
admin/src/ConfigPage.ts
+8
-1
@@ -5,7 +5,7 @@ import { state, useSnapState } from './state'
5
import { Refresh } from '@mui/icons-material'
6
import { Dict } from './misc'
7
import { subscribeKey } from 'valtio/utils'
8
-import { Form, ServerPort, BoolField, NumberField, StringField } from './Form';
8
+import { Form, ServerPort, BoolField, NumberField, StringField, SelectField } from './Form';
9
import StringStringField from './StringStringField'
10
11
let loaded: Dict | undefined
@@ -50,6 +50,13 @@ export default function ConfigPage() {
50
return { md: shortField ? 3 : 6 }
51
},
52
fields: [
53
+ { k: 'admin_port', comp: ServerPort, label: 'Admin port' },
54
+ { k: 'admin_network', comp: SelectField, label: 'Admin access',
55
+ options:[
56
+ { value: '127.0.0.1', label: 'localhost only' },
57
+ { value: '0.0.0.0', label: 'any network' }
58
+ ]
59
+ },
60
{ k: 'port', comp: ServerPort, label:'HTTP port' },
61
{ k: 'https_port', comp: ServerPort, label: 'HTTPS port' },
62
{ k: 'cert', comp: StringField, label: 'HTTPS certificate file' },
admin/src/MonitorPage.ts
+1
-1
@@ -53,7 +53,7 @@ function MoreInfo() {
53
}
54
55
function Connections() {
56
- const { list, error } = useApiList ('get_connections')
56
+ const { list, error } = useApiList('get_connections')
57
const rows = useMemo(()=> list?.map((x:any,id:number) => ({ id, ...x })), [list])
58
if (error)
59
return h(Alert, { severity: 'error' }, error)
admin/src/index.ts
+2
-1
@@ -1,6 +1,7 @@
1
-import { createElement as h, StrictMode } from 'react';
1
+import { createElement as h, StrictMode } from 'react'
2
import ReactDOM from 'react-dom';
3
import './index.css';
4
+import './min-crypto-polyfill'
5
import App from './App';
6
//import reportWebVitals from './reportWebVitals';
7
admin/src/min-crypto-polyfill.js
new
+19
@@ -0,0 +1,19 @@
1
+export {}
2
+// this is the minimum required for lib tssrp6a to work
3
+if (!window.crypto?.subtle) {
4
+ console.debug('poly subtle')
5
+
6
+ const subtle = {
7
+ async digest(algo, buff) {
8
+ if (algo !== 'SHA-512')
9
+ return alert(algo + ' required but not supported')
10
+ const lib = await import('js-sha512')
11
+ const sha = lib.default.arrayBuffer
12
+ return sha(buff)
13
+ }
14
+ }
15
+ if (!window.crypto)
16
+ window.crypto = { subtle }
17
+ if (!crypto.subtle)
18
+ crypto.subtle = subtle
19
+}
admin/tsconfig.json
+1
-1
@@ -1,6 +1,6 @@
1
{
2
"compilerOptions": {
3
- "target": "es5",
3
+ "target": "es2017",
4
"lib": [
5
"dom",
6
"dom.iterable",
frontend/tsconfig.json
+1
-1
@@ -1,6 +1,6 @@
1
{
2
"compilerOptions": {
3
- "target": "es5",
3
+ "target": "es2017",
4
"lib": [
5
"dom",
6
"dom.iterable",
package.json
+4
-2
@@ -14,8 +14,10 @@
14
"start-frontend": "cd frontend && npm run start",
15
"start-admin": "cd admin && npm run start",
16
"start": "node dist",
17
- "build": "npm install && tsc --target es2018 && cp -v -r package*.json READ* plugins tests dist && cp config.yaml dist/config-example.yaml && cp accounts.yaml dist/accounts-example.yaml && cd dist && npm ci --production && cd .. && node afterbuild",
18
- "build-all": "rm -rf dist && npm run build && cd frontend && npm install && npm run build && npm audit --production && echo COMPLETED",
17
+ "build": "npm install && rm -rf dist/src && tsc --target es2018 && cp -v -r package*.json READ* plugins tests dist && cp config.yaml dist/config-example.yaml && cp accounts.yaml dist/accounts-example.yaml && cd dist && npm ci --production && cd .. && node afterbuild",
18
+ "build-all": "rm -rf dist && npm run build && npm run build-frontend && npm run build-admin && npm audit --production && echo COMPLETED",
19
+ "build-frontend": "cd frontend && npm install && npm run build",
20
+ "build-admin": "cd admin && npm install && npm run build",
21
"test": "mocha -r ts-node/register 'tests/**/*.ts'",
22
"zip-dist": "cd dist && zip hfs.zip -r * -x *.zip *.exe",
23
"exe-dist": "pkg . -C brotli && cd dist && rm -rf src node_modules && zip hfs.exe.zip -r * -x *.zip run.bat",
src/config.ts
+11
-10
@@ -20,20 +20,21 @@ watchLoad(path, data => {
20
setConfig(data)
21
}, { failOnFirstAttempt:()=> setConfig({}) })
22
23
-const configProps:Record<string, ConfigProps> = {}
23
+const configProps:Record<string, ConfigProps<any>> = {}
24
25
-interface ConfigProps {
26
- defaultValue?:any,
27
- caster?:(argV:string)=>any
25
+interface ConfigProps<T> {
26
+ defaultValue?: T,
27
+ caster?:(argV:string)=> T
28
}
29
-export function defineConfig(k:string, definition:ConfigProps) {
29
+export function defineConfig<T>(k: string, definition: ConfigProps<T>) {
30
configProps[k] = definition
31
if (!definition.caster)
32
if (typeof definition.defaultValue === 'number')
33
+ // @ts-ignore
34
definition.caster = Number
35
}
36
36
-export function subscribeConfig({ k, ...definition }:{ k:string } & ConfigProps, cb:(v:any, was?:any)=>void) {
37
+export function subscribeConfig<T>({ k, ...definition }:{ k:string } & ConfigProps<T>, cb:(v:T, was?:T)=>void) {
38
if (definition)
39
defineConfig(k, definition)
40
const { caster, defaultValue } = configProps[k] ?? {}
@@ -91,12 +92,12 @@ export const saveConfigAsap = _.debounce(async () => {
92
.catch(err => console.error('Failed at saving config file, please ensure it is writable.', String(err)))
93
})
94
94
-// async version of getConfig, allowing you to wait for onfig to be ready
95
-export async function getConfigReady(k: string, definition?: object) {
96
- return new Promise(resolve => {
95
+// async version of getConfig, allowing you to wait for config to be ready
96
+export async function getConfigReady<T>(k: string, definition?: object) {
97
+ return new Promise<T>(resolve => {
98
const off = subscribeConfig({ k, ...definition }, v => {
99
off?.()
99
- resolve(v)
100
+ resolve(v as T)
101
})
102
})
103
}
src/connections.ts
+1
-1
@@ -12,7 +12,7 @@ export interface Connection {
12
13
const all: Connection[] = []
14
15
-export function newConnection(socket: Socket, secure:boolean) {
15
+export function newConnection(socket: Socket, secure:boolean=false) {
16
const conn: Connection = { socket, secure, got: 0, sent: 0, started: new Date() }
17
all.push(conn)
18
app.emit('connection', conn) // we'll use these events for SSE
src/index.ts
+4
-14
@@ -11,8 +11,6 @@ import { headRequests, gzipper, sessions, frontendAndSharedFiles } from './middl
11
import './listen'
12
import { serveAdminFiles } from './serveFrontend'
13
import { adminApis } from './adminApis'
14
-import { getConfigReady } from './config'
15
-import open from 'open'
14
15
export const BUILD_TIMESTAMP = "-"
16
export const SESSION_DURATION = 30*60_000
@@ -21,18 +19,10 @@ export const HFS_STARTED = new Date()
19
console.log('started', HFS_STARTED.toLocaleString(), 'build', BUILD_TIMESTAMP, DEV)
20
console.debug('cwd', process.cwd())
21
24
-getConfigReady('open_browser_at_start', { defaultValue: true }).then(config => {
25
- const ADMIN_PORT = 63636
26
- new Koa()
27
- .use(mount(API_URI, apiMiddleware(adminApis)))
28
- .use(serveAdminFiles)
29
- .on('error', errorHandler)
30
- .listen(ADMIN_PORT, '127.0.0.1', () => {
31
- if (config !== false)
32
- open('http://localhost:' + ADMIN_PORT).then()
33
- console.log('admin interface on http://localhost:' + ADMIN_PORT)
34
- })
35
-})
22
+export const adminApp = new Koa()
23
+ .use(mount(API_URI, apiMiddleware(adminApis)))
24
+ .use(serveAdminFiles)
25
+ .on('error', errorHandler)
26
27
export const app = new Koa({ keys: ['hfs-keys-test'] })
28
app.use(sessions(app))
src/listen.ts
+69
-37
@@ -1,21 +1,48 @@
1
import * as http from 'http'
2
import { getConfig, subscribeConfig } from './config'
3
-import { app } from './index'
3
+import { adminApp, app } from './index'
4
import * as https from 'https'
5
import { watchLoad } from './watchLoad'
6
import { networkInterfaces } from 'os';
7
import { newConnection } from './connections'
8
+import open from 'open'
9
+import { debounceAsync } from './misc'
10
11
let httpSrv: http.Server
12
let httpsSrv: http.Server
13
+let adminSrv: http.Server
14
let cert:string, key: string
15
13
-subscribeConfig({ k:'port', defaultValue: 80 }, async (port: number) => {
16
+subscribeConfig<number>({ k:'port', defaultValue: 80 }, async port => {
17
await stopServer(httpSrv)
15
- httpSrv = http.createServer(app.callback());
16
- await startServer(httpSrv, port ?? 80)
18
+ httpSrv = http.createServer(app.callback())
19
+ port = await startServer(httpSrv, { port, name:'http' })
20
+ httpSrv.on('connection', newConnection)
21
+ printUrls(port, 'http')
22
})
23
24
+const considerAdmin = debounceAsync(async () => {
25
+ const port = getConfig('admin_port')
26
+ const net = getConfig('admin_network')
27
+ const ad = adminSrv?.address()
28
+ if (ad && typeof ad !== 'string'
29
+ && ad.port === port && ad.address === net) return
30
+ await stopServer(adminSrv)
31
+ adminSrv = http.createServer(adminApp.callback())
32
+ const resultPort = await startServer(adminSrv, {
33
+ port ,
34
+ name: 'admin',
35
+ net,
36
+ })
37
+ if (!resultPort)
38
+ return
39
+ if (getConfig('open_browser_at_start') !== false)
40
+ open('http://localhost:' + resultPort).then()
41
+ console.log('admin interface on http://localhost:' + resultPort)
42
+})
43
+
44
+subscribeConfig<string>({ k:'admin_network', defaultValue: '127.0.0.1' }, considerAdmin)
45
+subscribeConfig<number>({ k:'admin_port', defaultValue: 63636 }, considerAdmin)
46
47
subscribeConfig({ k:'cert' }, async (v: string) => {
48
await stopServer(httpsSrv)
@@ -50,20 +77,23 @@ subscribeConfig({ k:CFG_HTTPS_PORT, defaultValue: 443 }, considerHttps)
77
78
async function considerHttps() {
79
await stopServer(httpsSrv)
53
- const port = getConfig('https_port')
54
- httpsSrv = https.createServer({ key, cert }, app.callback());
55
- return await startServer(httpsSrv, !cert || !key ? -1 : port, 's')
80
+ httpsSrv = https.createServer({ key, cert }, app.callback())
81
+ const port = await startServer(httpsSrv, {
82
+ port: !cert || !key ? -1 : getConfig('https_port'),
83
+ name: 'https'
84
+ })
85
+ httpsSrv.on('connection', socket =>
86
+ newConnection(socket, true))
87
+ printUrls(port, 'https')
88
}
89
58
-function startServer(srv: http.Server, port: number, secure:string='') {
59
- return new Promise((resolve, reject) => {
90
+interface StartServer { port: number, name:string, net?:string }
91
+function startServer(srv: http.Server, { port, name, net='0.0.0.0' }: StartServer) {
92
+ return new Promise<number>((resolve, reject) => {
93
try {
61
- if (port < 0) {
62
- console.log('http'+secure+' off')
63
- return resolve(null)
64
- }
65
- srv.listen(port, () => {
66
- const proto = 'http' + secure
94
+ if (port < 0)
95
+ return resolve(0)
96
+ srv.listen(port, net, () => {
97
const ad = srv.address()
98
if (!ad)
99
return reject('no address')
@@ -71,32 +101,13 @@ function startServer(srv: http.Server, port: number, secure:string='') {
101
srv.close()
102
return reject('type of socket not supported')
103
}
74
- port = ad.port
75
- console.log(proto, `serving on port`, port)
76
-
77
- const ignore = /^(lo|.*loopback.*|virtualbox.*|.*\(wsl\).*)$/i // avoid giving too much information
78
- for (const [name, nets] of Object.entries(networkInterfaces()))
79
- if (nets && !ignore.test(name)) {
80
- console.log('network', name)
81
- for (const net of nets) {
82
- if (net.internal) continue
83
- const appendPort = port === (secure ? 443 : 80) ? '' : ':' + port
84
- let { address } = net
85
- if (address.includes(':'))
86
- address = '['+address+']'
87
- console.log('-', proto + '://' + address + appendPort)
88
- }
89
- }
90
-
91
- resolve(null)
104
+ console.log(name, "serving on", net, ':', ad.port)
105
+ resolve(ad.port)
106
}).on('error', e => {
107
const { code } = e as any
94
- console.error(code === 'EADDRINUSE' ? `couldn't listen on busy port ${port}` : e)
108
+ console.error(code === 'EADDRINUSE' ? `couldn't listen on port ${port}` : e)
109
reject(e)
110
})
97
-
98
- srv.on('connection', socket =>
99
- newConnection(socket, Boolean(secure)))
111
}
112
catch(e) {
113
console.error("couldn't listen on port", port, String(e))
@@ -109,6 +120,9 @@ function stopServer(srv: http.Server) {
120
return new Promise(resolve => {
121
if (!srv?.listening)
122
return resolve(null)
123
+ const ad = srv.address()
124
+ if (ad && typeof ad !== 'string')
125
+ console.log('stopped port ' + ad.port)
126
srv.close(err => {
127
if (err && (err as any).code !== 'ERR_SERVER_NOT_RUNNING')
128
console.debug('failed to stop server', String(err))
@@ -123,3 +137,21 @@ export function getStatus() {
137
httpsSrv,
138
}
139
}
140
+
141
+function printUrls(port: number, proto: string) {
142
+ if (!port) return
143
+ const ignore = /^(lo|.*loopback.*|virtualbox.*|.*\(wsl\).*)$/i // avoid giving too much information
144
+ for (const [name, nets] of Object.entries(networkInterfaces())) {
145
+ if (!nets || ignore.test(name)) continue
146
+ console.log('network', name)
147
+ for (const net of nets) {
148
+ if (net.internal) continue
149
+ const appendPort = port === (proto==='https' ? 443 : 80) ? '' : ':' + port
150
+ let { address } = net
151
+ if (address.includes(':'))
152
+ address = '['+address+']'
153
+ console.log('-', proto + '://' + address + appendPort)
154
+ }
155
+ }
156
+}
157
+
src/misc.ts
+17
-4
@@ -119,10 +119,10 @@ export function onlyTruthy<T>(arr: T[]) {
119
120
type PendingPromise<T> = Promise<T> & { resolve: (value: T) => void, reject: (reason?: any) => void }
121
export function pendingPromise<T>() {
122
- // @ts-ignore
123
- const ret: PendingPromise<T> = new Promise<T>((resolve, reject) =>
124
- Object.assign(ret, { resolve, reject }))
125
- return ret
122
+ let takeOut
123
+ const ret = new Promise<T>((resolve, reject) =>
124
+ takeOut = { resolve, reject })
125
+ return Object.assign(ret, takeOut) as PendingPromise<T>
126
}
127
128
// returns an 'uninstall' callback for the handlers you just installed. Pass a map {event:handler}
@@ -135,3 +135,16 @@ export function onOffMap(em: EventEmitter, events: Record<string, (...args: any[
135
em.off(k, events[k])
136
}
137
}
138
+
139
+// avoid for an async function to be overlapped with another execution while awaiting
140
+export function debounceAsync(cb: any, ms: number=100, ...args:any[]) {
141
+ const debounced = _.debounce(cb, ms, ...args)
142
+ let busy = false
143
+ return async () => {
144
+ while (busy)
145
+ await wait(ms)
146
+ busy = true
147
+ try { return await debounced() }
148
+ finally { busy = false }
149
+ }
150
+}
src/serveFrontend.ts
+1
-1
@@ -63,7 +63,7 @@ function serveAdminProxy(port?: string) { // used for development
63
}
64
65
const serveAdminStatic : Koa.Middleware = async (ctx, next) => {
66
- const fullPath = 'admin' + (ctx.path.includes('.') ? ctx.path : 'index.html');
66
+ const fullPath = 'admin' + (ctx.path.includes('.') ? ctx.path : '/index.html');
67
return serveFile(fullPath, 'auto')(ctx, next)
68
}
69
src/vfs.ts
+1
-1
@@ -73,7 +73,7 @@ export class Vfs {
73
}
74
75
export const vfs = new Vfs()
76
-subscribeConfig({ k: 'vfs' }, data =>
76
+subscribeConfig<VfsNode>({ k: 'vfs' }, data =>
77
vfs.root = data)
78
79
function findChildByName(name:string, node:VfsNode) {