config.block
Massimo Melina committed
Mar 10, 2022 at 10:20 UTC
ceea9cbbd322d2e56006855c77ccdb36869b062c
6 files changed
+71
-14
README.md
+5
@@ -208,6 +208,11 @@ Supported entries are:
208
- `https_port` listen on a specific port. Default is 443.
209
- `cert` use this file for https certificate. Minimum to start https is to give a cert and a private_key. Default is none.
210
- `private_key` use this file for https private key. Default is none.
211
+- `block` a list of rules that will block connections. At the moment only `ip` rules are supported. E.g.:
212
+ ```
213
+ block:
214
+ - ip: 192.168.0.90
215
+ ```
216
- `plugins_config` this is a generic place where you can find/put configuration for each plugin, at least those that need configuration.
217
218
## Virtual File System (VFS)
admin/src/ConfigPage.ts
+4
@@ -80,6 +80,10 @@ export default function ConfigPage() {
80
{ k: 'open_browser_at_start', comp: BoolField },
81
{ k: 'zip_calculate_size_for_seconds', comp: NumberField, md: 6, xl:4, label: "Calculate ZIP size for seconds",
82
helperText: "If time is not enough, the browser will not show download percentage" },
83
+ { k: 'block', label: "Blocked IPs", comp: StringField, multiline: true, minRows:3, helperText: "Enter an IP for each line",
84
+ fromField: (all:string) => all.split('\n').map(s => s.trim()).filter(Boolean).map(ip => ({ ip })),
85
+ toField: (all: any) => !Array.isArray(all) ? '' : all.map(x => x?.ip).filter(Boolean).join('\n')
86
+ },
87
{ k: 'mime', comp: StringStringField,
88
keyLabel: "Files", keyWidth: 7,
89
valueLabel: "Mime type", valueWidth: 4
admin/src/MonitorPage.ts
+18
-11
@@ -3,13 +3,11 @@
3
import _ from "lodash"
4
import { isValidElement, createElement as h, useMemo, Fragment } from "react"
5
import { apiCall, useApiComp, useApiList } from "./api"
6
-import { Delete, Lock } from '@mui/icons-material'
6
+import { Delete, Lock, Block } from '@mui/icons-material'
7
import { Box, Chip, Typography } from '@mui/material'
8
import { DataGrid } from "@mui/x-data-grid"
9
import { Alert } from '@mui/material'
10
-import { formatBytes, IconBtn, iconTooltip } from "./misc"
11
-import { alertDialog } from "./dialog"
12
-import { prefix } from './misc'
10
+import { prefix, formatBytes, IconBtn, iconTooltip, manipulateConfig } from "./misc"
11
12
export default function MonitorPage() {
13
return h(Fragment, {},
@@ -111,15 +109,24 @@ function Connections() {
109
width: 80,
110
align: 'center',
111
renderCell({ row }) {
114
- return h(IconBtn, {
115
- icon: Delete,
116
- title: 'Disconnect',
117
- onClick() {
118
- apiCall('disconnect', _.pick(row, ['ip', 'port'])).catch(alertDialog)
119
- }
120
- })
112
+ return h('div', {},
113
+ h(IconBtn, {
114
+ icon: Delete,
115
+ title: 'Disconnect',
116
+ onClick: () => apiCall('disconnect', _.pick(row, ['ip', 'port'])),
117
+ }),
118
+ h(IconBtn, {
119
+ icon: Block,
120
+ title: 'Block IP',
121
+ onClick: () => blockIp(row.ip),
122
+ }),
123
+ )
124
}
125
}
126
]
127
})
128
}
129
+
130
+function blockIp(ip: string) {
131
+ return manipulateConfig('block', data => [...data, { ip }])
132
+}
admin/src/misc.ts
+23
-2
@@ -5,6 +5,9 @@ import { Box, CircularProgress, IconButton, Link, Tooltip } from '@mui/material'
5
import { Link as RouterLink } from 'react-router-dom'
6
import { SxProps } from '@mui/system'
7
import { SvgIconComponent } from '@mui/icons-material'
8
+import { alertDialog } from './dialog'
9
+import { apiCall } from './api'
10
+import { useStateMounted } from '@hfs/shared'
11
export * from '@hfs/shared'
12
13
export function spinner() {
@@ -21,8 +24,19 @@ export function isEqualLax(a: any,b: any): boolean {
24
&& Object.entries(a).every(([k,v]) => isEqualLax(v, b[k])) )
25
}
26
24
-export function IconBtn({ title, icon, ...rest }: { title?: string, icon: SvgIconComponent, [rest:string]:any }) {
25
- const ret = h(IconButton, { ...rest }, h(icon))
27
+export function IconBtn({ title, icon, onClick, ...rest }: { title?: string, icon: SvgIconComponent, [rest:string]:any }) {
28
+ const [loading, setLoading] = useStateMounted(false)
29
+ const ret = h(IconButton, {
30
+ disabled: loading,
31
+ ...rest,
32
+ onClick() {
33
+ const ret = onClick?.apply(this,arguments)
34
+ if (ret && ret instanceof Promise) {
35
+ setLoading(true)
36
+ ret.catch(alertDialog).finally(()=> setLoading(false))
37
+ }
38
+ }
39
+ }, h(icon))
40
return title ? h(Tooltip, { title, children: ret }) : ret
41
}
42
@@ -38,3 +52,10 @@ export function Center(props: any) {
52
return h(Box, { display:'flex', height:'100%', width:'100%', justifyContent:'center', alignItems:'center', ...props })
53
}
54
55
+export async function manipulateConfig(k: string, work:(data:any) => any) {
56
+ const cfg = await apiCall('get_config', { only: [k] })
57
+ const was = cfg[k]
58
+ const will = await work(was)
59
+ if (JSON.stringify(was) !== JSON.stringify(will))
60
+ await apiCall('set_config', { values: { [k]: will } })
61
+}
server/src/middlewares.ts
+15
@@ -14,6 +14,9 @@ import { serveFrontend } from './serveFrontend'
14
import mount from 'koa-mount'
15
import { Readable } from 'stream'
16
import { getAccount, getCurrentUsername, getCurrentUsernameExpanded } from './perm'
17
+import { getConfig, subscribeConfig } from './config'
18
+import { getConnections } from './connections'
19
+import { Socket } from 'net'
20
21
export const gzipper = compress({
22
threshold: 2048,
@@ -85,6 +88,8 @@ export const someSecurity: Koa.Middleware = async (ctx, next) => {
88
try {
89
if (dirTraversal(decodeURI(ctx.path)))
90
return ctx.status = 418
91
+ if (applyBlock(ctx.socket))
92
+ return
93
}
94
catch {
95
return ctx.status = 418
@@ -92,6 +97,16 @@ export const someSecurity: Koa.Middleware = async (ctx, next) => {
97
return next()
98
}
99
100
+subscribeConfig({ k: 'block', defaultValue: [] }, () => {
101
+ for (const { socket } of getConnections())
102
+ applyBlock(socket)
103
+})
104
+
105
+function applyBlock(socket: Socket) {
106
+ if (getConfig('block').find((rule:any) => rule.ip === socket.remoteAddress))
107
+ return socket.destroy()
108
+}
109
+
110
export function prepareState(admin=false): Koa.Middleware {
111
return async (ctx, next) => {
112
ctx.state.usernames = getCurrentUsernameExpanded(ctx) // accounts chained via .belongs for permissions check
todo.md
+6
-1
@@ -9,7 +9,13 @@
9
- admin: warn in case of items with same name
10
- allowed referer
11
- admin/plugins
12
+ - show list of available plugins (skip -disabled)
13
+ - enable/disable (disable_plugins, no dir rename)
14
+ - show some properties, which ones are loaded
15
+ - plugin config: plugin must expose gui producing object or form configuration
16
- download-counter: expose results on admin
17
+- plugin to show country by ip in admin/monitor
18
+- config.proxies:number (will enable koa.proxy:true + maxIpsCount, default 0)
19
- log filter option
20
- log filter plugin
21
- publish to npm (so people can "npm install hfs")
@@ -23,7 +29,6 @@
29
- updater (stop,unzip,start)
30
- node.comment
31
- config: max connections/downloads (total/per-ip)
26
-- config: bans
32
- config: min disk space
33
- thumbnails support
34
- webdav?