admin: validation of net-masks
Massimo Melina committed
Mar 22, 2025 at 12:43 UTC
ca5ee6670117f76d5a8e8e6228466ef40a5ab023
7 files changed
+59
-43
admin/src/AccountForm.ts
+1
-2
@@ -99,8 +99,7 @@ export default function AccountForm({ account, done, groups, addToBar, reload }:
99
+ (!isGroup ? '' : ". A group can inherit from another group")
100
+ (belongsOptions.length ? '' : ". Now disabled because there are no groups to select, create one first.")
101
},
102
- { k: 'allow_net', comp: NetmaskField, label: "Allowed network address", helperText: h(WildcardsSupported), sm: 6,
103
- placeholder: "Allow from any address" },
102
+ { k: 'allow_net', comp: NetmaskField, label: "Allowed network address", sm: 6, placeholder: "Allow from any address" },
103
{ k: 'expire', label: "Expiration", xs: true, comp: DateTimeField, toField: x => x && new Date(x),
104
helperText: "When expired, login won't be allowed" },
105
{ k: 'days_to_live', xs: 12, sm: 6, comp: NumberField, disabled: expired, step: 'any', min: 1/1000, // 10 minutes
admin/src/LogsPage.ts
+1
-3
@@ -77,9 +77,7 @@ export default function LogsPage() {
77
{ k: CFG.log_rotation, comp: SelectField, sm: 6, options: [{ value:'', label:"disabled" }, 'daily', 'weekly', 'monthly' ],
78
helperText: wikiLink('Logs#rotation', "To keep log-files smaller"),
79
},
80
- { k: CFG.dont_log_net, comp: NetmaskField, label: "Don't log address", sm: 6, placeholder: "no exception",
81
- helperText: h(WildcardsSupported)
82
- },
80
+ { k: CFG.dont_log_net, comp: NetmaskField, label: "Don't log address", sm: 6, placeholder: "no exception" },
81
{ k: CFG.log_gui, sm: 6, comp: BoolField, label: "Log interface loading", helperText: "Some requests are necessary to load the interface" },
82
{ k: CFG.log_api, sm: 6, comp: BoolField, label: "Log API requests", helperText: "Requests for commands" },
83
{ k: CFG.log_ua, sm: 6, comp: BoolField, label: "Log User-Agent", helperText: "Contains browser and possibly OS information. Can double the size of your logs on disk." },
admin/src/OptionsPage.ts
+3
-3
@@ -1,7 +1,7 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
import { Box, Button, Divider, FormHelperText } from '@mui/material';
4
-import { createElement as h, Fragment, useEffect, useRef } from 'react';
4
+import { createElement as h, useEffect, useRef } from 'react';
5
import { apiCall, useApiEx } from './api'
6
import { state, useSnapState } from './state'
7
import { Link as RouterLink } from 'react-router-dom'
@@ -148,7 +148,7 @@ export default function OptionsPage() {
148
{ k : CFG.max_downloads_per_account, ...maxDownloadsDefaults, label: "Max downloads per-account", helperText: "Overrides other limits" },
149
150
{ k: 'admin_net', comp: NetmaskField, label: "Admin-panel accessible from", placeholder: "any address",
151
- helperText: h(Fragment, {}, "IP address of browser machine. ", h(WildcardsSupported))
151
+ helperText: "IP address of browser machine"
152
},
153
{ k: 'localhost_admin', comp: BoolField, label: "Unprotected Admin-panel on localhost",
154
getError: x => !x && admins?.length===0 && "First create at least one admin account",
@@ -169,7 +169,7 @@ export default function OptionsPage() {
169
fields: [
170
{ k: 'ip', label: "Blocked IP", sm: 12, required: true, wrap: true, $width: 2, comp: NetmaskField,
171
$column: { mergeRender: { comment: {}, expire: {} } },
172
- helperText: h(Flex, { component: 'span' }, h(WildcardsSupported), "Be careful to not kick yourself out, by blocking also your IP."),
172
+ helperText: "Be careful to not kick yourself out, by blocking also your IP",
173
},
174
{ k: 'expire', $type: 'dateTime', minDate: new Date(), sm: 6, $hideUnder: 'sm',
175
helperText: "Leave empty for no expiration" },
admin/src/mui.ts
+7
-1
@@ -277,9 +277,15 @@ export function useToggleButton(onTitle: string, offTitle: undefined | string, i
277
return [state, el, setState] as const
278
}
279
280
-export function NetmaskField(props: StringFieldProps) {
280
+export function NetmaskField({ setApi, helperText, ...props }: StringFieldProps) {
281
const warned = useRef(false)
282
+ setApi?.({
283
+ getError() {
284
+ return props.value && apiCall('validate_net_mask', { mask: props.value }).then(x => !x.result && "Invalid mask")
285
+ }
286
+ })
287
return h(StringField, {
288
+ helperText: h('span', {}, helperText, helperText && ' – ', wikiLink('Wildcards#network-masks', "Wildcards supported")),
289
...props,
290
onTyping(v) {
291
if (!warned.current && v?.includes('127.0.0.1') && !v.includes('::1')) {
mui-grid-form/index.ts
+7
-4
@@ -159,10 +159,13 @@ export function Form<Values extends Dict>({
159
field.helperText = h(Fragment, {}, ...field.helperText)
160
if (errMsg) // special rendering when we have both error and helperText. "hr" would be nice but issues a warning because contained in a <p>
161
field.helperText = !field.helperText ? errMsg
162
- : h(Fragment, {},
163
- h('span', { style: { borderBottom: '1px solid' } }, errMsg),
164
- h(Box, { color: 'text.primary', component: 'span', /*avoid console warning*/ display: 'block' },
165
- field.helperText),
162
+ : h(Box, { color: 'text.primary', component: 'span' },
163
+ h(Box, {
164
+ color: 'error.main',
165
+ style: { borderBottom: '1px solid' },
166
+ component: 'span', display: 'block' // avoid console warning, but keep it on separate line
167
+ }, errMsg),
168
+ field.helperText,
169
)
170
if (field.label === undefined)
171
field.label = labelFromKey(k)
src/adminApis.ts
+6
-1
@@ -16,7 +16,7 @@ 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'
19
+import { apiAssertTypes, debounceAsync, isLocalHost, makeNetMatcher, try_, typedEntries, waitFor } from './misc'
20
import { accountCanLoginAdmin, accounts } from './perm'
21
import Koa from 'koa'
22
import { cloudflareDetected, getProxyDetected } from './middlewares'
@@ -168,6 +168,11 @@ export const adminApis = {
168
return { country: await ip2country(ip) }
169
},
170
171
+ validate_net_mask({ mask }) {
172
+ apiAssertTypes({ string: { mask } })
173
+ return { result: Boolean(try_(() => makeNetMatcher(mask))) }
174
+ },
175
+
176
} satisfies ApiHandlers
177
178
for (const [k, was] of typedEntries(adminApis))
src/misc.ts
+34
-29
@@ -39,38 +39,43 @@ export function makeNetMatcher(mask: string, emptyMaskReturns=false) {
39
return () => emptyMaskReturns
40
mask = mask.replaceAll(' ','')
41
mask = mask.replace('localhost', '::1|127.0.0.1')
42
- if (!/\/|-(?![^\[]*\])/.test(mask)) // when no CIDR and no ranges are used, then we use standard matcher, otherwise BlockList. For "-" we must skip those inside []
43
- return makeMatcher(mask)
44
- const all = mask.split('|')
45
- const neg = all[0]?.[0] === '!'
46
- if (neg)
47
- all[0] = all[0]!.slice(1)
48
- const bl = new BlockList()
49
- for (const x of all) {
50
- const m = /^([.:\da-f]+)(?:\/(\d+)|-(.+)|)$/i.exec(x) // parse cidr or range
51
- if (!m) { // we don't support wildcards in this case
52
- console.warn("error in network mask", x)
53
- continue
42
+ try {
43
+ if (!/\/|-(?![^\[]*\])/.test(mask)) { // when no CIDR and no ranges are used, then we use standard matcher, otherwise BlockList. For "-" we must skip those inside []
44
+ if (/[^.:\da-fA-F*?|()!]/.test(mask))
45
+ throw mask
46
+ return makeMatcher(mask)
47
}
55
- const address = try_(() => parseAddress(m[1]!),
56
- () => console.error("invalid address " + m[1]))
57
- if (!address) continue
58
- if (m[2])
59
- try { bl.addSubnet(address, Number(m[2])) }
60
- catch { console.error("invalid net mask " + x) }
61
- else if (m[3])
62
- try { bl.addRange(address, parseAddress(m[2]!)) }
63
- catch { console.error("invalid address " + m[2]) }
64
- else
65
- bl.addAddress(address)
66
- }
67
- return (ip: string) => {
68
- try { return neg !== bl.check(parseAddress(ip)) }
69
- catch {
70
- console.error("invalid address ", ip)
71
- return false
48
+ const all = mask.split('|')
49
+ const neg = all[0]?.[0] === '!'
50
+ if (neg)
51
+ all[0] = all[0]!.slice(1)
52
+ const bl = new BlockList()
53
+ for (const x of all) {
54
+ const m = /^([.:\da-f]+)(?:\/(\d+)|-([.:\da-f]+)|)$/i.exec(x) // parse cidr or range
55
+ if (!m) throw x // we don't support wildcards in this case
56
+ const address = try_(() => parseAddress(m[1]!),
57
+ () => { throw m[1] })
58
+ if (!address) continue
59
+ if (m[2])
60
+ try { bl.addSubnet(address, Number(m[2])) }
61
+ catch { throw x }
62
+ else if (m[3])
63
+ try { bl.addRange(address, parseAddress(m[2]!)) }
64
+ catch { throw m[2] }
65
+ else
66
+ bl.addAddress(address)
67
+ }
68
+ return (ip: string) => {
69
+ try { return neg !== bl.check(parseAddress(ip)) }
70
+ catch {
71
+ console.error("invalid address ", ip)
72
+ return false
73
+ }
74
}
75
}
76
+ catch(e: any) {
77
+ throw "error in net-mask: " + e
78
+ }
79
}
80
81
// can throw ERR_INVALID_ADDRESS