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?