@samitouri / QOSami-HFS / commits / 4cb1f23c

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,