admin/internet: "Accept requests only using domain"
Massimo Melina committed
Nov 16, 2023 at 23:49 UTC
4cb1f23c415a4c0ddf3a156db4251a6bd268848d
7 files changed
+50
-20
README.md
+2
-2
@@ -173,9 +173,9 @@ While this project focuses on ease of use, we care about security.
173
174
Some actions you can take for improved security:
175
- use https, better if using a proper certificate, even free with [Letsencrypt](https://letsencrypt.org/).
176
-- have a domain (ddns is ok too), start vhosting plugin, configure your domain, enable "Block requests that are not using any of the domains above"
176
+- have a domain (ddns is ok too), configure it in "Internet" page, and enable "Accept requests only using domain"
177
- install rejetto/antidos plugin
178
-- start antibrute plugin (but it's started by default)
178
+- ensure "antibrute" plugin is running
179
- disable "unprotected admin on localhost"
180
181
## Hidden features
admin/src/InternetPage.ts
+28
-8
@@ -182,7 +182,16 @@ export default function InternetPage() {
182
.then(() => alertDialog("Domain seems ok", 'success'))
183
}, "Check"),
184
),
185
- )
185
+ ),
186
+ h(ConfigForm<{ force_base_url: boolean }>, {
187
+ keys: ['force_base_url'],
188
+ saveOnChange: true,
189
+ form: {
190
+ fields: [
191
+ { k: 'force_base_url', comp: BoolField, label: "Accept requests only using domain (and localhost)" }
192
+ ]
193
+ },
194
+ })
195
)
196
}
197
@@ -251,7 +260,7 @@ export default function InternetPage() {
260
catch { mapPort(HIGHER_PORT, '') }
261
toast("Port forwarded, now verify again", 'success')
262
retry()
254
- })
263
+ })
264
const cfg = await apiCall('get_config', { only: [CFG.geo_enable, CFG.geo_allow] })
265
const { close } = alertDialog(h(Box, {}, msg + "Possible causes:", h('ul', {},
266
cfg[CFG.geo_enable] && cfg[CFG.geo_allow] != null && h('li', {}, "You may be blocking a country from where the test is performed"),
@@ -345,22 +354,29 @@ function TitleCard({ title, icon, color, children }: { title: ReactNode, icon?:
354
)))
355
}
356
348
-type FormRest<T> = Omit<FormProps<T>, 'values' | 'set' | 'save'>
349
-function ConfigForm<T=any>({ keys, form, ...rest }: Partial<FormRest<T>> & { keys: (keyof T)[], form: ((values: T) => FormRest<T>) }) {
357
+type FormRest<T> = Omit<FormProps<T>, 'values' | 'set' | 'save'> & Partial<Pick<FormProps<T>, 'save'>>
358
+function ConfigForm<T=any>({ keys, form, saveOnChange, ...rest }: Partial<FormRest<T>> & {
359
+ keys: (keyof T)[],
360
+ form: FormRest<T> | ((values: T) => FormRest<T>),
361
+ saveOnChange?: boolean
362
+}) {
363
const config = useApiEx('get_config', { only: keys })
364
const [values, setValues] = useState<any>(config.data)
365
useEffect(() => setValues((v: any) => config.data || v), [config.data])
366
+ const modified = values && !_.isEqual(values, config.data)
367
+ useEffect(() => {
368
+ if (modified && saveOnChange) save()
369
+ }, [modified])
370
if (!values)
371
return config.element
355
- const formProps = form(values)
356
- const modified = !_.isEqual(values, config.data)
372
+ const formProps = _.isFunction(form) ? form(values) : form
373
return h(Form, {
374
values,
375
set(v, k) {
376
setValues((was: any) => ({ ...was, [k]: v }))
377
},
362
- save: {
363
- onClick: () => apiCall('set_config', { values }).then(config.reload),
378
+ save: saveOnChange ? false : {
379
+ onClick: save,
380
sx: modifiedSx(modified),
381
},
382
...Array.isArray(formProps) ? { fields: formProps } : formProps,
@@ -376,4 +392,8 @@ function ConfigForm<T=any>({ keys, form, ...rest }: Partial<FormRest<T>> & { key
392
...rest.addToBar||[],
393
],
394
})
395
+
396
+ function save() {
397
+ return apiCall('set_config', { values }).then(config.reload)
398
+ }
399
}
\ No newline at end of file
src/block.ts
+2
-2
@@ -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 { defineConfig } from './config'
4
-import { getConnections, normalizeIp } from './connections'
4
+import { disconnect, getConnections, normalizeIp } from './connections'
5
import { makeNetMatcher, MINUTE, onlyTruthy } from './misc'
6
import { Socket } from 'net'
7
@@ -22,7 +22,7 @@ const block = defineConfig('block', [] as BlockingRule[], rules => {
22
23
export function applyBlock(socket: Socket, ip=normalizeIp(socket.remoteAddress||'')) {
24
if (ip && block.compiled().find(rule => rule(ip)))
25
- return socket.destroy()
25
+ return disconnect(socket)
26
}
27
28
setInterval(() => { // twice a minute, check if any block has expired
src/connections.ts
+6
@@ -68,3 +68,9 @@ export function updateConnection(conn: Connection, change: Partial<Connection>)
68
Object.assign(conn, change)
69
events.emit('connectionUpdated', conn, change)
70
}
71
+
72
+export function disconnect(what: Context | Socket) {
73
+ if ('socket' in what)
74
+ what = what.socket
75
+ return what.destroy()
76
+}
\ No newline at end of file
src/geo.ts
+2
-2
@@ -4,7 +4,7 @@ import { stat, rename, unlink } from 'node:fs/promises'
4
import { IP2Location } from 'ip2location-nodejs'
5
import _ from 'lodash'
6
import { Middleware } from 'koa'
7
-import { updateConnection } from './connections'
7
+import { disconnect, updateConnection } from './connections'
8
9
const ip2location = new IP2Location()
10
const enabled = defineConfig(CFG.geo_enable, false)
@@ -22,7 +22,7 @@ export const geoFilter: Middleware = async (ctx, next) => {
22
const country = connection.country ??= await ip2country(ctx.ip)
23
updateConnection(connection, { country })
24
if (country ? list.get().includes(country) !== allow.get() : !allowUnknown.get())
25
- return ctx.socket.destroy()
25
+ return disconnect(ctx)
26
}
27
return next()
28
}
src/listen.ts
+2
-1
@@ -24,7 +24,8 @@ let httpsSrv: undefined | http.Server & ServerExtra
24
25
const openBrowserAtStart = defineConfig('open_browser_at_start', !DEV)
26
27
-export const baseUrl = defineConfig('base_url', '')
27
+export const baseUrl = defineConfig('base_url', '',
28
+ x => /\/\/[^\/]+/.exec(x)?.[1]) // compiled is host only
29
30
export function getBaseUrlOrDefault() {
31
return baseUrl.get() || defaultBaseUrl.get()
src/middlewares.ts
+8
-5
@@ -1,14 +1,14 @@
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 compress from 'koa-compress'
4
-import Koa, { Middleware } from 'koa'
4
+import Koa from 'koa'
5
import { ADMIN_URI, API_URI, BUILD_TIMESTAMP, DEV,
6
HTTP_FORBIDDEN, HTTP_NOT_FOUND, HTTP_FOOL, HTTP_UNAUTHORIZED, HTTP_BAD_REQUEST, HTTP_METHOD_NOT_ALLOWED,
7
} from './const'
8
import { FRONTEND_URI } from './const'
9
import { statusCodeForMissingPerm, nodeIsDirectory, urlToNode, vfs, walkNode, VfsNode, getNodeName } from './vfs'
10
-import { DAY, asyncGeneratorToReadable, dirTraversal, filterMapGenerator, isLocalHost, stream2string, tryJson,
11
- splitAt } from './misc'
10
+import { DAY, asyncGeneratorToReadable, dirTraversal, filterMapGenerator, isLocalHost, stream2string, tryJson, splitAt
11
+} from './misc'
12
import { zipStreamFromFolder } from './zip'
13
import { serveFile, serveFileNode } from './serveFile'
14
import { serveGuiFiles } from './serveGuiFiles'
@@ -16,7 +16,7 @@ import mount from 'koa-mount'
16
import { Readable } from 'stream'
17
import { applyBlock } from './block'
18
import { accountCanLogin, getAccount } from './perm'
19
-import { socket2connection, updateConnection, normalizeIp } from './connections'
19
+import { socket2connection, updateConnection, normalizeIp, disconnect } from './connections'
20
import basicAuth from 'basic-auth'
21
import { invalidSessions, srpCheck } from './auth'
22
import { basename, dirname } from 'path'
@@ -33,6 +33,7 @@ import { app } from './index'
33
34
const forceHttps = defineConfig('force_https', true)
35
const ignoreProxies = defineConfig('ignore_proxies', false)
36
+const forceBaseUrl = defineConfig('force_base_url', false)
37
export const sessionDuration = defineConfig('session_duration', Number(process.env.SESSION_DURATION) || DAY/1000,
38
v => v * 1000)
39
@@ -196,6 +197,8 @@ export const someSecurity: Koa.Middleware = async (ctx, next) => {
197
catch {
198
return ctx.status = HTTP_FOOL
199
}
200
+ if (forceBaseUrl.get() && !isLocalHost(ctx) && ctx.host === baseUrl.compiled())
201
+ return disconnect(ctx)
202
return next()
203
}
204
@@ -248,7 +251,7 @@ export const paramsDecoder: Koa.Middleware = async (ctx, next) => {
251
await next()
252
}
253
251
-export const sessionMiddleware: Middleware = (ctx, next) =>
254
+export const sessionMiddleware: Koa.Middleware = (ctx, next) =>
255
session({
256
key: 'hfs_$id' + (ctx.secure ? '' : '_http'), // once https cookie is created, http cannot
257
signed: true,