admin/config: better errors for https
Massimo Melina committed
Jun 2, 2022 at 23:59 UTC
080f1f2b979888290e9e3667fb0e8484fd928b9b
4 files changed
+69
-46
admin/src/ConfigPage.ts
+29
-10
@@ -81,8 +81,15 @@ export default function ConfigPage() {
81
return v
82
}
83
},
84
- values.https_port >= 0 && { k: 'cert', comp: FileField, label: "HTTPS certificate file" },
85
- values.https_port >= 0 && { k: 'private_key', comp: FileField, label: "HTTPS private key file" },
84
+ values.https_port >= 0 && { k: 'cert', comp: FileField, label: "HTTPS certificate file",
85
+ ...with_(status?.https.error, e => isCertError(e) ? {
86
+ error: true,
87
+ helperText: [e, ' - ', h(Link, { key: 'fix', sx: { cursor: 'pointer' }, onClick: makeCertAndSave }, "make one")]
88
+ } : null)
89
+ },
90
+ values.https_port >= 0 && { k: 'private_key', comp: FileField, label: "HTTPS private key file",
91
+ ...with_(status?.https.error, e => isKeyError(e) ? { error: true, helperText: e } : null)
92
+ },
93
{ k: 'open_browser_at_start', comp: BoolField },
94
{ k: 'localhost_admin', comp: BoolField, label: "Admin access for localhost connections",
95
getError: x => !x && admins?.length===0 && "First create at least one admin account",
@@ -149,17 +156,24 @@ function recalculateChanges() {
156
}
157
158
export function isCertError(error: any) {
152
- return typeof error === 'string' && /certificate|key/.test(error)
159
+ return /certificate/.test(error)
160
+}
161
+
162
+export function isKeyError(error: any) {
163
+ return /private key/.test(error)
164
}
165
155
-function ServerPort({ label, value, onChange, status, suggestedPort=1 }: FieldProps<number | null>) {
166
+function ServerPort({ label, value, onChange, getApi, status, suggestedPort=1, error }: FieldProps<number | null>) {
167
const lastCustom = useRef(suggestedPort)
168
if (value! > 0)
169
lastCustom.current = value!
170
const selectValue = Number(value! > 0 ? lastCustom.current : value) || 0
160
- let error = status?.error
161
- if (isCertError(error))
162
- error = [error, ' - ', h(Link, { key: 'fix', sx: { cursor: 'pointer' }, onClick: makeCertAndSave }, "make one")]
171
+ let errMsg = status?.error
172
+ if (errMsg)
173
+ if (isCertError(errMsg) || isKeyError(errMsg))
174
+ errMsg = undefined // never mind, we'll show this error elsewhere
175
+ else
176
+ error = true
177
return h(Box, {},
178
h(Box, { display: 'flex' },
179
h(SelectField as Field<number>, {
@@ -173,11 +187,11 @@ function ServerPort({ label, value, onChange, status, suggestedPort=1 }: FieldPr
187
],
188
onChange,
189
}),
176
- value! > 0 && h(NumberField, { label: 'Number', fullWidth: false, value, onChange, min: 1, max: 65535, sx: { minWidth:'5.5em' } }),
190
+ value! > 0 && h(NumberField, { label: 'Number', fullWidth: false, value, onChange, getApi, error, min: 1, max: 65535, sx: { minWidth:'5.5em' } }),
191
),
178
- status && h(FormHelperText, { error: Boolean(error) },
192
+ status && h(FormHelperText, { error },
193
status === true ? '...'
180
- : error ?? (status?.listening && "Correctly working on port "+ status.port) )
194
+ : errMsg ?? (status?.listening && "Correctly working on port " + status.port) )
195
)
196
}
197
@@ -243,3 +257,8 @@ async function makeCert(attributes: Record<string, string>) {
257
private_key: pki.privateKeyToPem(keys.privateKey),
258
}
259
}
260
+
261
+export function with_<T,RT>(par:T, cb: (par:T) => RT) {
262
+ return cb(par)
263
+}
264
+
admin/src/HomePage.ts
+3
-4
@@ -8,7 +8,7 @@ import { CheckCircle, Error, Info, Launch, Warning } from '@mui/icons-material'
8
import md from './md'
9
import { useSnapState } from './state'
10
import { confirmDialog } from './dialog'
11
-import { isCertError, makeCertAndSave } from './ConfigPage'
11
+import { isCertError, isKeyError, makeCertAndSave } from './ConfigPage'
12
import { VfsNode } from './VfsPage'
13
import { Account } from './AccountsPage'
14
@@ -33,10 +33,9 @@ export default function HomePage() {
33
: v.error )
34
const errors = errorMap && onlyTruthy(Object.entries(errorMap).map(([k,v]) =>
35
v && [md(`Protocol _${k}_ cannot work: `), v,
36
- isCertError(v) && [
36
+ (isCertError(v) || isKeyError(v)) && [
37
SOLUTION_SEP, h(Link, { sx: { cursor: 'pointer' }, onClick() { makeCertAndSave().then(reloadCfg).then(reloadStatus) } }, "make one"),
38
- " or ",
39
- SOLUTION_SEP, cfgLink("provide adequate files")
38
+ " or ", SOLUTION_SEP, cfgLink("provide adequate files")
39
]]))
40
return h(Box, { display:'flex', gap: 2, flexDirection:'column' },
41
username && entry('', "Welcome "+username),
mui-grid-form/src/Form.ts
+21
-20
@@ -36,6 +36,18 @@ export interface FieldDescriptor<T=any> {
36
// it seems necessary to cast (Multi)SelectField sometimes
37
export type Field<T> = FC<FieldProps<T>>
38
39
+type Promisable<T> = T | Promise<T>
40
+interface FieldApi { getError: () => Promisable<ValidationError>, [rest: string]: any }
41
+export interface FieldProps<T> {
42
+ label?: string | ReactElement
43
+ value?: T
44
+ onChange: (v: T, more: { was?: T, event: any, [rest: string]: any }) => void
45
+ getApi?: (api: FieldApi) => void
46
+ error?: true
47
+ helperText?: ReactNode
48
+ [rest: string]: any
49
+}
50
+
51
type Dict<T=any> = Record<string,T>
52
53
export interface FormProps<Values> extends Partial<BoxProps> {
@@ -91,21 +103,21 @@ export function Form<Values extends Dict>({ fields, values, set, defaults, save,
103
return null
104
if (isValidElement(row))
105
return h(Grid, { key: idx, item: true, xs: 12 }, row)
94
- const { k, fromField=_.identity, toField=_.identity, getError, ...field } = row
95
- let error = errors[k]
96
- if (error === true)
97
- error = "Not valid"
106
+ const { k, fromField=_.identity, toField=_.identity, getError, error, ...field } = row
107
+ let errMsg = errors[k]
108
+ if (errMsg === true)
109
+ errMsg = "Not valid"
110
if (k) {
111
const originalValue = values?.[k]
112
const whole = { ...row, ...field }
113
Object.assign(field, {
114
value: toField(originalValue),
103
- error: Boolean(error) || undefined,
115
+ error: Boolean(errMsg || error) || undefined,
116
getApi(api) { apis[k] = api },
117
onBlur() {
118
pleaseValidate(k)
119
},
108
- async onChange(v, { event }) {
120
+ onChange(v, { event }) {
121
try {
122
v = fromField(v)
123
if (_.isEqual(v, originalValue)) return
@@ -120,9 +132,9 @@ export function Form<Values extends Dict>({ fields, values, set, defaults, save,
132
}
133
},
134
} as Partial<FieldProps<any>>)
123
- if (error) // special rendering when we have both error and helperText. "hr" would be nice but issues a warning because contained in a <p>
124
- field.helperText = field.helperText ? h(Fragment, {}, h('span', { style: { borderBottom: '1px solid' } }, error), h('br'), field.helperText)
125
- : error
135
+ if (errMsg) // special rendering when we have both error and helperText. "hr" would be nice but issues a warning because contained in a <p>
136
+ field.helperText = field.helperText ? h(Fragment, {}, h('span', { style: { borderBottom: '1px solid' } }, errMsg), h('br'), field.helperText)
137
+ : errMsg
138
if (field.label === undefined)
139
field.label = labelFromKey(k)
140
_.defaults(field, defaults?.(whole))
@@ -205,14 +217,3 @@ export function Form<Values extends Dict>({ fields, values, set, defaults, save,
217
export function labelFromKey(k: string) {
218
return _.capitalize(k.replace(/_/g, ' '))
219
}
208
-
209
-type Promisable<T> = T | Promise<T>
210
-interface FieldApi { getError: () => Promisable<ValidationError>, [rest: string]: any }
211
-export interface FieldProps<T> {
212
- label?: string | ReactElement
213
- value?: T
214
- onChange: (v: T, more: { was?: T, event: any, [rest: string]: any }) => void
215
- getApi?: (api: FieldApi) => void
216
- error?: true
217
- [rest: string]: any
218
-}
server/src/listen.ts
+16
-12
@@ -8,7 +8,7 @@ import { watchLoad } from './watchLoad'
8
import { networkInterfaces } from 'os';
9
import { newConnection } from './connections'
10
import open from 'open'
11
-import { debounceAsync, onlyTruthy, prefix, wait } from './misc'
11
+import { debounceAsync, onlyTruthy, wait } from './misc'
12
import { ADMIN_URI, DEV } from './const'
13
import findProcess from 'find-process'
14
@@ -39,21 +39,25 @@ const considerHttps = debounceAsync(async () => {
39
while (!app)
40
await wait(100)
41
httpsSrv = Object.assign(
42
- https.createServer(port < 0 ? {} : httpsOptions, app.callback()),
43
- { name: 'https' }
42
+ https.createServer(port < 0 ? {} : { key: httpsOptions.private_key, cert: httpsOptions.cert }, app.callback()),
43
+ { name: 'https', error: undefined }
44
)
45
- const missingCfg = httpsNeeds.find(x => !x.get())
46
- httpsSrv.error = port < 0 ? undefined
47
- : missingCfg && prefix(missingCfg.get() ? "cannot read file for " : "missing ", (httpsNeedsNames as any)[missingCfg.key()])
48
- if (httpsSrv.error)
49
- return
45
+ if (port >= 0) {
46
+ const namesForOutput: any = { cert: 'certificate', private_key: 'private key' }
47
+ const missing = httpsNeeds.find(x => !x.get())?.key()
48
+ if (missing)
49
+ return httpsSrv.error = "missing " + namesForOutput[missing]
50
+ const cantRead = httpsNeeds.find(x => !httpsOptions[x.key() as HttpsKeys])?.key()
51
+ if (cantRead)
52
+ return httpsSrv.error = "cannot read " + namesForOutput[cantRead]
53
+ }
54
}
55
catch(e) {
56
httpsSrv.error = "bad private key or certificate"
57
console.log("failed to create https server: check your private key and certificate", String(e))
58
return
59
}
56
- port = await startServer(httpsSrv, { port: httpsPortCfg.get() })
60
+ port = await startServer(httpsSrv, { port })
61
if (!port) return
62
httpsSrv.on('connection', socket =>
63
newConnection(socket, true))
@@ -64,13 +68,13 @@ const considerHttps = debounceAsync(async () => {
68
const cert = defineConfig<string>('cert')
69
const privateKey = defineConfig<string>('private_key')
70
const httpsNeeds = [cert, privateKey]
67
-const httpsNeedsNames = { cert: 'certificate', private_key: 'private key' }
68
-const httpsOptions = { key: '', cert: '' }
71
+const httpsOptions = { cert: '', private_key: '' }
72
+type HttpsKeys = keyof typeof httpsOptions
73
for (const cfg of httpsNeeds) {
74
let unwatch: ReturnType<typeof watchLoad>['unwatch']
75
cfg.sub(async v => {
76
unwatch?.()
73
- const k = cfg.key() === 'private_key' ? 'key' : 'cert'
77
+ const k = cfg.key() as HttpsKeys
78
httpsOptions[k] = v
79
if (!v || v.includes('\n'))
80
return considerHttps()