better code: more consistent api for config properties, with typed get/set and fewer literals involved
Massimo Melina committed
Apr 29, 2022 at 01:01 UTC
efc4dfb2e0011f83658f48e0e53f97e2f1811f3d
15 files changed
+102
-105
server/src/adminApis.ts
+15
-16
@@ -1,10 +1,9 @@
1
// This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
import { ApiError, ApiHandlers } from './apiMiddleware'
4
-import { defineConfig, getConfig, getWholeConfig, setConfig } from './config'
5
-import { getStatus, getUrls } from './listen'
6
-import { API_VERSION, BUILD_TIMESTAMP, CFG_ENABLE_PLUGINS, CFG_PLUGINS_CONFIG,
7
- COMPATIBLE_API_VERSION, FORBIDDEN, HFS_STARTED, IS_WINDOWS, VERSION } from './const'
4
+import { defineConfig, getWholeConfig, setConfig } from './config'
5
+import { getStatus, getUrls, httpsPortCfg, portCfg } from './listen'
6
+import { API_VERSION, BUILD_TIMESTAMP, COMPATIBLE_API_VERSION, FORBIDDEN, HFS_STARTED, IS_WINDOWS, VERSION } from './const'
7
import vfsApis from './api.vfs'
8
import accountsApis from './api.accounts'
9
import { Connection, getConnections } from './connections'
@@ -19,7 +18,7 @@ import { writeFile } from 'fs/promises'
18
import { createReadStream } from 'fs'
19
import * as readline from 'readline'
20
import { loggers } from './log'
22
-import { mapPlugins, getAvailablePlugins, Plugin, AvailablePlugin } from './plugins'
21
+import { mapPlugins, getAvailablePlugins, Plugin, AvailablePlugin, enablePlugins, pluginsConfig } from './plugins'
22
import { execFile } from 'child_process'
23
import { promisify } from 'util'
24
@@ -31,8 +30,8 @@ export const adminApis: ApiHandlers = {
30
async set_config({ values: v }) {
31
if (v) {
32
const st = getStatus()
34
- const noHttp = (v.port ?? getConfig('port')) < 0 || !st.httpSrv.listening
35
- const noHttps = (v.https_port ?? getConfig('https_port')) < 0 || !st.httpsSrv.listening
33
+ const noHttp = (v.port ?? portCfg.get()) < 0 || !st.httpSrv.listening
34
+ const noHttps = (v.https_port ?? httpsPortCfg.get()) < 0 || !st.httpsSrv.listening
35
if (noHttp && noHttps)
36
return new ApiError(FORBIDDEN, "You cannot switch off both http and https ports")
37
await setConfig(v)
@@ -52,11 +51,11 @@ export const adminApis: ApiHandlers = {
51
version: VERSION,
52
apiVersion: API_VERSION,
53
compatibleApiVersion: COMPATIBLE_API_VERSION,
55
- http: serverStatus(st.httpSrv, getConfig('port')),
56
- https: serverStatus(st.httpsSrv, getConfig('https_port')),
54
+ http: serverStatus(st.httpSrv, portCfg.get()),
55
+ https: serverStatus(st.httpsSrv, httpsPortCfg.get()),
56
urls: getUrls(),
57
proxyDetected: getProxyDetected(),
59
- frpDetected: getConfig('localhost_admin') && !getProxyDetected()
58
+ frpDetected: localhostAdmin.get() && !getProxyDetected()
59
&& getConnections().every(isLocalHost)
60
&& await frpDebounced(),
61
}
@@ -170,18 +169,18 @@ export const adminApis: ApiHandlers = {
169
170
async set_plugin({ id, enabled, config }) {
171
if (enabled !== undefined) {
173
- const a = getConfig(CFG_ENABLE_PLUGINS)
172
+ const a = enablePlugins.get()
173
if (a.includes(id) !== enabled)
175
- setConfig({ [CFG_ENABLE_PLUGINS]: enabled ? [...a, id] : a.filter((x: string) => x !== id) })
174
+ enablePlugins.set( enabled ? [...a, id] : a.filter((x: string) => x !== id) )
175
}
176
if (config) {
177
config = _.pickBy(config, v => v !== null)
179
- const o = { ...getConfig(CFG_PLUGINS_CONFIG) }
178
+ const o = { ...pluginsConfig.get() }
179
if (_.isEmpty(config))
180
delete o[id]
181
else
182
o[id] = config
184
- setConfig({ [CFG_PLUGINS_CONFIG]: _.isEmpty(o) ? undefined : o })
183
+ pluginsConfig.set( _.isEmpty(o) ? undefined : o )
184
}
185
return {}
186
},
@@ -223,10 +222,10 @@ for (const k in adminApis) {
222
: new ApiError(401)
223
}
224
226
-defineConfig('localhost_admin', { defaultValue: true })
225
+export const localhostAdmin = defineConfig('localhost_admin', true)
226
227
export function ctxAdminAccess(ctx: Koa.Context) {
229
- return isLocalHost(ctx) && getConfig('localhost_admin')
228
+ return isLocalHost(ctx) && localhostAdmin.get()
229
&& !ctx.state.proxiedFor // this may detect an http-proxied request on localhost
230
|| getFromAccount(ctx.state.account, a => a.admin)
231
}
server/src/api.helpers.ts
+2
-3
@@ -1,8 +1,7 @@
1
// This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
-import { Account, saveSrpInfo, updateAccount } from './perm'
3
+import { Account, allowClearTextLogin, saveSrpInfo, updateAccount } from './perm'
4
import { ApiError } from './apiMiddleware'
5
-import { CFG_ALLOW_CLEAR_TEXT_LOGIN, getConfig } from './config'
5
6
export async function changePasswordHelper(account: Account | undefined, newPassword: string) {
7
if (!newPassword) // clear text version
@@ -16,7 +15,7 @@ export async function changePasswordHelper(account: Account | undefined, newPass
15
}
16
17
export async function changeSrpHelper(account: Account | undefined, salt: string, verifier: string) {
19
- if (getConfig(CFG_ALLOW_CLEAR_TEXT_LOGIN))
18
+ if (allowClearTextLogin.get())
19
return new ApiError(406)
20
if (!salt || !verifier)
21
return Error('missing parameters')
server/src/block.ts
+2
-2
@@ -1,11 +1,11 @@
1
-import { subscribeConfig } from './config'
1
+import { defineConfig } from './config'
2
import { getConnections } from './connections'
3
import { onlyTruthy, with_ } from './misc'
4
import cidr from 'cidr-tools'
5
import _ from 'lodash'
6
import { Socket } from 'net'
7
8
-subscribeConfig({ k: 'block', defaultValue: [] }, (rules: any) => {
8
+defineConfig<string[]>('block', []).sub(rules => {
9
compileBlock(rules)
10
for (const { socket, ip } of getConnections())
11
applyBlock(socket, ip)
server/src/config.ts
+19
-28
@@ -9,8 +9,6 @@ import { debounceAsync, objSameKeys, onOff, wait } from './misc'
9
import { exists } from 'fs'
10
import { promisify } from 'util'
11
12
-export const CFG_ALLOW_CLEAR_TEXT_LOGIN = 'allow_clear_text_login'
13
-
12
const PATH = 'config.yaml'
13
14
const configProps:Record<string, ConfigProps<any>> = {}
@@ -30,34 +28,39 @@ const { save } = watchLoad(path, values => setConfig(values||{}, false), {
28
29
interface ConfigProps<T> {
30
defaultValue?: T,
33
- arg?: T,
34
- caster: (argV:string)=> T
31
}
36
-export function defineConfig<T>(k: string, definition: Partial<ConfigProps<T>>) {
37
- const { caster = _.identity } = definition
38
- configProps[k] = {
39
- caster,
40
- ...definition,
41
- defaultValue: _.cloneDeep(definition.defaultValue),
32
+export function defineConfig<T>(k: string, defaultValue?: T) {
33
+ configProps[k] = { defaultValue }
34
+ return {
35
+ key() {
36
+ return k
37
+ },
38
+ get() {
39
+ return getConfig(k)
40
+ },
41
+ sub(cb: (v:T, was?:T)=>void) {
42
+ return subscribeConfig(k, cb)
43
+ },
44
+ set(v: T) {
45
+ setConfig({ [k]: v })
46
+ }
47
}
48
}
49
45
-export function subscribeConfig<T>({ k, ...definition }:{ k:string } & Partial<ConfigProps<T>>, cb:(v:T, was?:T)=>void) {
46
- if (definition)
47
- defineConfig(k, definition)
50
+function subscribeConfig<T>(k:string, cb: (v:T, was?:T)=>void) {
51
const { defaultValue } = configProps[k] ?? {}
52
const eventName = 'new.'+k
53
if (started) {
54
let v = state[k]
55
if (v === undefined)
53
- state[k] = v = _.cloneDeep(defaultValue)
56
+ state[k] = v = _.cloneDeep(defaultValue) // clone to avoid changing
57
if (v !== undefined)
58
cb(v)
59
}
60
return onOff(cfgEvents, { [eventName]: cb })
61
}
62
60
-export function getConfig(k:string) {
63
+function getConfig(k:string) {
64
return k in state ? state[k] : configProps[k]?.defaultValue
65
}
66
@@ -103,10 +106,8 @@ export function setConfig(newCfg: Record<string,any>, save?: boolean) {
106
function check(k: string) {
107
const oldV = started ? getConfig(k) : state[k] // from second time consider also defaultValue
108
const newV = newCfg[k]
106
- const { caster, defaultValue } = configProps[k] ?? {}
109
+ const { defaultValue } = configProps[k] ?? {}
110
let v = newV ?? _.cloneDeep(defaultValue) // if we have an object we may get into troubles letting others change ours
108
- if (caster)
109
- v = caster(v)
111
const j = JSON.stringify(v)
112
if (j === JSON.stringify(oldV)) return // no change
113
state[k] = v
@@ -130,13 +131,3 @@ export const saveConfigAsap = debounceAsync(async () => {
131
save(path, txt)
132
.catch(err => console.error('Failed at saving config file, please ensure it is writable.', String(err)))
133
})
133
-
134
-// async version of getConfig, allowing you to wait for config to be ready
135
-export async function getConfigReady<T>(k: string, definition?: object) {
136
- return new Promise<T>(resolve => {
137
- const off = subscribeConfig({ k, ...definition }, v => {
138
- off?.()
139
- resolve(v as T)
140
- })
141
- })
142
-}
server/src/const.ts
-3
@@ -29,9 +29,6 @@ export const FORBIDDEN = 403
29
30
export const IS_WINDOWS = process.platform === 'win32'
31
32
-export const CFG_PLUGINS_CONFIG = 'plugins_config'
33
-export const CFG_ENABLE_PLUGINS = 'enable_plugins'
34
-
32
// we want this to be the first stuff to be printed, then we print it in this module, that is executed at the beginning
33
if (DEV) console.clear()
34
else console.debug = ()=>{}
server/src/frontEndApis.ts
+4
-2
@@ -3,13 +3,15 @@
3
import { ApiHandlers } from './apiMiddleware'
4
import { file_list } from './api.file_list'
5
import * as api_auth from './api.auth'
6
-import { getConfig } from './config'
6
+import { defineConfig } from './config'
7
+
8
+const customHeader = defineConfig('custom_header')
9
10
export const frontEndApis: ApiHandlers = {
11
file_list,
12
...api_auth,
13
14
config() {
13
- return Object.fromEntries(['custom_header'].map(k => [k, getConfig(k)]))
15
+ return Object.fromEntries([customHeader].map(x => [x.key(), x.get()]))
16
}
17
}
server/src/index.ts
+2
-2
@@ -11,7 +11,7 @@ import { throttler } from './throttler'
11
import { headRequests, gzipper, sessions, serveGuiAndSharedFiles, someSecurity, prepareState } from './middlewares'
12
import './listen'
13
import { adminApis } from './adminApis'
14
-import { subscribeConfig } from './config'
14
+import { defineConfig } from './config'
15
import { ok } from 'assert'
16
import _ from 'lodash'
17
@@ -43,7 +43,7 @@ process.on('uncaughtException', err => {
43
console.error(err)
44
})
45
46
-subscribeConfig({ k: 'proxies', defaultValue: 0 }, n => {
46
+defineConfig('proxies', 0).sub(n => {
47
app.proxy = n > 0
48
app.maxIpsCount = n
49
})
server/src/listen.ts
+21
-19
@@ -1,7 +1,7 @@
1
// This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
import * as http from 'http'
4
-import { defineConfig, getConfig, subscribeConfig } from './config'
4
+import { defineConfig } from './config'
5
import { app } from './index'
6
import * as https from 'https'
7
import { watchLoad } from './watchLoad'
@@ -11,13 +11,15 @@ import open from 'open'
11
import { onlyTruthy, prefix, wait } from './misc'
12
import { ADMIN_URI, DEV } from './const'
13
import findProcess from 'find-process'
14
-import _ from 'lodash'
14
15
interface ServerExtra { name: string, error?: string, busy?: string }
16
let httpSrv: http.Server & ServerExtra
17
let httpsSrv: http.Server & ServerExtra
18
20
-subscribeConfig<number>({ k:'port', defaultValue: 80 }, async port => {
19
+const openBrowserAtStart = defineConfig('open_browser_at_start', !DEV)
20
+
21
+export const portCfg = defineConfig<number>('port', 80)
22
+portCfg.sub(async port => {
23
while (!app)
24
await wait(100)
25
stopServer(httpSrv).then()
@@ -26,47 +28,47 @@ subscribeConfig<number>({ k:'port', defaultValue: 80 }, async port => {
28
if (!port) return
29
httpSrv.on('connection', newConnection)
30
printUrls(port, 'http')
29
- if (getConfig('open_browser_at_start'))
31
+ if (openBrowserAtStart.get())
32
open('http://localhost' + (port === 80 ? '' : ':' + port) + ADMIN_URI).then()
33
})
34
33
-defineConfig('open_browser_at_start', { defaultValue: !DEV })
34
-
35
-const httpsNeeds = { cert:'', private_key:'' }
35
+const cert = defineConfig<string>('cert')
36
+const privateKey = defineConfig<string>('private_key')
37
+const httpsNeeds = [cert, privateKey]
38
const httpsNeedsNames = { cert: 'certificate', private_key: 'private key' }
37
-for (const k of Object.keys(httpsNeeds) as (keyof typeof httpsNeeds)[]) { // please be smarter typescript
39
+for (const cfg of httpsNeeds) {
40
let unwatch: ReturnType<typeof watchLoad>['unwatch']
39
- subscribeConfig({ k }, async (v: string) => {
41
+ cfg.sub(async v => {
42
unwatch?.()
41
- httpsNeeds[k] = v
43
+ cfg.set(v)
44
if (!v || v.includes('\n'))
45
return considerHttps()
46
// it's a path
45
- httpsNeeds[k] = ''
47
+ cfg.set('')
48
unwatch = watchLoad(v, data => {
47
- httpsNeeds[k] = data
49
+ cfg.set(data)
50
considerHttps()
51
}).unwatch
52
await considerHttps()
53
})
54
}
55
54
-const CFG_HTTPS_PORT = 'https_port'
55
-subscribeConfig({ k:CFG_HTTPS_PORT, defaultValue: -1 }, considerHttps)
56
+export const httpsPortCfg = defineConfig('https_port', -1)
57
+httpsPortCfg.sub(considerHttps)
58
59
async function considerHttps() {
60
stopServer(httpsSrv).then()
59
- let port = getConfig('https_port')
61
+ let port = httpsPortCfg.get()
62
try {
63
while (!app)
64
await wait(100)
65
httpsSrv = Object.assign(
64
- https.createServer(port < 0 ? {} : { key: httpsNeeds.private_key, cert: httpsNeeds.cert }, app.callback()),
66
+ https.createServer(port < 0 ? {} : { key: privateKey.get(), cert: cert.get() }, app.callback()),
67
{ name: 'https' }
68
)
67
- const missingKey = _.findKey(httpsNeeds, v => !v) as keyof typeof httpsNeeds
69
+ const missingCfg = httpsNeeds.find(x => !x.get())
70
httpsSrv.error = port < 0 ? undefined
69
- : missingKey && prefix(getConfig(missingKey) ? "cannot read file for " : "missing ", httpsNeedsNames[missingKey])
71
+ : missingCfg && prefix(missingCfg.get() ? "cannot read file for " : "missing ", (httpsNeedsNames as any)[missingCfg.key()])
72
if (httpsSrv.error)
73
return
74
}
@@ -75,7 +77,7 @@ async function considerHttps() {
77
console.log("failed to create https server: check your private key and certificate", String(e))
78
return
79
}
78
- port = await startServer(httpsSrv, { port: getConfig('https_port') })
80
+ port = await startServer(httpsSrv, { port: httpsPortCfg.get() })
81
if (!port) return
82
httpsSrv.on('connection', socket =>
83
newConnection(socket, true))
server/src/log.ts
+6
-5
@@ -2,7 +2,7 @@
2
3
import Koa from 'koa'
4
import { Writable } from 'stream'
5
-import { defineConfig, getConfig, subscribeConfig } from './config'
5
+import { defineConfig } from './config'
6
import { createWriteStream, existsSync, renameSync, WriteStream } from 'fs'
7
import * as util from 'util'
8
import { stat } from 'fs/promises'
@@ -42,24 +42,25 @@ const accessLogger = new Logger('log')
42
const errorLogger = new Logger('error_log')
43
export const loggers = [accessLogger, errorLogger]
44
45
-subscribeConfig({ k: 'log', defaultValue: 'access.log' }, path => {
45
+defineConfig('log', 'access.log').sub(path => {
46
console.debug('log file: ' + (path || 'disabled'))
47
accessLogger.setPath(path)
48
})
49
50
-subscribeConfig({ k: 'error_log', defaultValue: 'error.log' }, path => {
50
+const errorLogFile = defineConfig('error_log', 'error.log')
51
+errorLogFile.sub(path => {
52
console.debug('error log: ' + (path || 'disabled'))
53
errorLogger.setPath(path)
54
})
55
55
-defineConfig('log_rotation', { defaultValue: 'weekly' })
56
+const logRotation = defineConfig('log_rotation', 'weekly')
57
58
export function log(): Koa.Middleware {
59
return async (ctx, next) => { // wrapping in a function will make it use current 'mw' value
60
await next()
61
const isError = ctx.status >= 400
62
const logger = isError && errorLogger || accessLogger
62
- const rotate = getConfig('log_rotation')?.[0]
63
+ const rotate = logRotation.get()?.[0]
64
let { stream, last, path } = logger
65
if (!stream) return
66
const now = new Date()
server/src/perm.ts
+5
-3
@@ -4,7 +4,7 @@ import _ from 'lodash'
4
import { hashPassword } from './crypt'
5
import { objRenameKey, setHidden, wantArray } from './misc'
6
import Koa from 'koa'
7
-import { CFG_ALLOW_CLEAR_TEXT_LOGIN, getConfig, saveConfigAsap, setConfig, subscribeConfig } from './config'
7
+import { defineConfig, saveConfigAsap, setConfig } from './config'
8
import { createVerifierAndSalt, SRPParameters, SRPRoutines } from 'tssrp6a'
9
import events from './events'
10
import { watchLoad } from './watchLoad'
@@ -54,6 +54,8 @@ export function saveSrpInfo(account:Account, salt:string | bigint, verifier: str
54
account.srp = String(salt) + '|' + String(verifier)
55
}
56
57
+export const allowClearTextLogin = defineConfig('allow_clear_text_login')
58
+
59
const srp6aNimbusRoutines = new SRPRoutines(new SRPParameters())
60
61
type Changer = (account:Account)=> void | Promise<void>
@@ -63,7 +65,7 @@ export async function updateAccount(account: Account, changer?:Changer) {
65
const { username } = account
66
if (account.password) {
67
console.debug('hashing password for', username)
66
- if (getConfig(CFG_ALLOW_CLEAR_TEXT_LOGIN))
68
+ if (allowClearTextLogin.get())
69
account.hashed_password = await hashPassword(account.password)
70
const res = await createVerifierAndSalt(srp6aNimbusRoutines, username, account.password)
71
saveSrpInfo(account, res.s, res.v)
@@ -90,7 +92,7 @@ watchLoad('accounts.yaml', accounts => {
92
unlink('accounts.yaml', () => console.log("accounts file merged"))
93
})
94
93
-subscribeConfig<Accounts>({ k:'accounts', defaultValue: {} }, async v => {
95
+defineConfig<Accounts>('accounts', {}).sub(async v => {
96
// we should validate content here
97
accounts = v // keep local reference
98
await Promise.all(_.map(accounts, async (rec,k) => {
server/src/plugins.ts
+8
-7
@@ -4,11 +4,11 @@ import glob from 'fast-glob'
4
import { watchLoad } from './watchLoad'
5
import _ from 'lodash'
6
import pathLib from 'path'
7
-import { API_VERSION, CFG_ENABLE_PLUGINS, CFG_PLUGINS_CONFIG, COMPATIBLE_API_VERSION, PLUGINS_PUB_URI } from './const'
7
+import { API_VERSION, COMPATIBLE_API_VERSION, PLUGINS_PUB_URI } from './const'
8
import * as Const from './const'
9
import Koa from 'koa'
10
import { debounceAsync, getOrSet, onProcessExit, wantArray, watchDir } from './misc'
11
-import { getConfig, subscribeConfig } from './config'
11
+import { defineConfig } from './config'
12
import { DirEntry } from './api.file_list'
13
import { VfsNode } from './vfs'
14
import { serveFile } from './serveFile'
@@ -143,18 +143,19 @@ if (!existsSync(PATH))
143
catch {}
144
watchDir(PATH, rescanAsap)
145
146
-const defaultValue = ['antibrute']
147
-subscribeConfig({ k: CFG_ENABLE_PLUGINS, defaultValue }, rescanAsap)
146
+export const enablePlugins = defineConfig('enable_plugins', ['antibrute'])
147
+enablePlugins.sub(rescanAsap)
148
+
149
+export const pluginsConfig = defineConfig('plugins_config')
150
151
async function rescan() {
152
console.debug('scanning plugins')
153
const found = []
154
const foundDisabled: typeof availablePlugins = {}
153
- const enable_plugins = wantArray(getConfig(CFG_ENABLE_PLUGINS))
155
for (let f of await glob(PATH+'/*/plugin.js')) {
156
const id = f.split('/').slice(-2)[0]
157
if (id.endsWith('-disabled')) continue
157
- if (!enable_plugins.includes(id)) {
158
+ if (!enablePlugins.get().includes(id)) {
159
const pl = foundDisabled[id] = { id } as typeof foundDisabled[0]
160
try {
161
const source = await readFile(f, 'utf8')
@@ -189,7 +190,7 @@ async function rescan() {
190
getConnections,
191
events,
192
getConfig: (cfgKey: string) =>
192
- getConfig(CFG_PLUGINS_CONFIG)?.[id]?.[cfgKey]
193
+ pluginsConfig.get()?.[id]?.[cfgKey]
194
})
195
Object.assign(data, res)
196
new Plugin(id, data, unwatch)
server/src/serveFile.ts
+8
-7
@@ -6,24 +6,26 @@ import fs from 'fs/promises'
6
import { FORBIDDEN, METHOD_NOT_ALLOWED, NO_CONTENT } from './const'
7
import { getNodeName, MIME_AUTO, VfsNode } from './vfs'
8
import mimetypes from 'mime-types'
9
-import { defineConfig, getConfig } from './config'
9
+import { defineConfig } from './config'
10
import mm, { isMatch } from 'micromatch'
11
import _ from 'lodash'
12
import path from 'path'
13
import { promisify } from 'util'
14
import { updateConnection } from './connections'
15
16
+const allowedReferer = defineConfig('allowed_referer')
17
+
18
export function serveFileNode(node: VfsNode) : Koa.Middleware {
19
const { source, mime } = node
20
const name = getNodeName(node)
21
const mimeString = typeof mime === 'string' ? mime
22
: _.find(mime, (val,mask) => isMatch(name, mask))
23
return (ctx, next) => {
22
- const allowedRef = getConfig('allowed_referer')
23
- if (allowedRef) {
24
+ const allowed = allowedReferer.get()
25
+ if (allowed) {
26
const ref = /\/\/([^:/]+)/.exec(ctx.get('referer'))?.[1] // extract host from url
27
if (ref && ref !== host() // automatic accept if referer is basically the hosting domain
26
- && !isMatch(ref, allowedRef))
28
+ && !isMatch(ref, allowed))
29
return ctx.status = FORBIDDEN
30
31
function host() {
@@ -37,7 +39,7 @@ export function serveFileNode(node: VfsNode) : Koa.Middleware {
39
}
40
}
41
40
-defineConfig('mime', { defaultValue:{ '*.jpg|*.png|*.mp3|*.txt': 'auto' } })
42
+const mimeCfg = defineConfig('mime', { '*.jpg|*.png|*.mp3|*.txt': 'auto' })
43
44
export function serveFile(source:string, mime?:string, modifier?:(s:string)=>string) : Koa.Middleware {
45
return async (ctx) => {
@@ -45,9 +47,8 @@ export function serveFile(source:string, mime?:string, modifier?:(s:string)=>str
47
return
48
const { range } = ctx.request.header
49
ctx.set('Accept-Ranges', 'bytes')
48
- const mimeCfg = getConfig('mime')
50
const fn = path.basename(source)
50
- mime = mime ?? _.find(mimeCfg, (v,k) => k && mm.isMatch(fn, k))
51
+ mime = mime ?? _.find(mimeCfg.get(), (v,k) => k && mm.isMatch(fn, k))
52
if (mime === MIME_AUTO)
53
mime = mimetypes.lookup(source) || ''
54
if (mime)
server/src/throttler.ts
+5
-3
@@ -3,14 +3,14 @@
3
import { Readable } from 'stream'
4
import Koa from 'koa'
5
import { ThrottledStream, ThrottleGroup } from './ThrottledStream'
6
-import { subscribeConfig } from './config'
6
+import { defineConfig } from './config'
7
import { getOrSet, isLocalHost } from './misc'
8
import { updateConnection } from './connections'
9
import _ from 'lodash'
10
11
const mainThrottleGroup = new ThrottleGroup(Infinity)
12
13
-subscribeConfig({ k:'max_kbps', defaultValue:null }, v =>
13
+defineConfig('max_kbps', null).sub(v =>
14
mainThrottleGroup.updateLimit(v ?? Infinity))
15
16
const ip2group: Record<string, {
@@ -22,6 +22,8 @@ const ip2group: Record<string, {
22
const SymThrStr = Symbol('stream')
23
const SymTimeout = Symbol('timeout')
24
25
+const maxKbpsPerIp = defineConfig('max_kbps_per_ip', null)
26
+
27
export const throttler: Koa.Middleware = async (ctx, next) => {
28
await next()
29
const { body } = ctx
@@ -32,7 +34,7 @@ export const throttler: Koa.Middleware = async (ctx, next) => {
34
const doLimit = ctx.state.account?.ignore_limits || isLocalHost(ctx) ? undefined : true
35
const group = new ThrottleGroup(Infinity, doLimit && mainThrottleGroup)
36
35
- const unsub = doLimit && subscribeConfig({ k:'max_kbps_per_ip', defaultValue:null }, v =>
37
+ const unsub = doLimit && maxKbpsPerIp.sub(v =>
38
group.updateLimit(v ?? Infinity))
39
return { group, count:0, destroy: unsub }
40
})
server/src/vfs.ts
+2
-2
@@ -7,7 +7,7 @@ import { dirStream, dirTraversal, enforceFinal, getOrSet, isDirectory, typedKeys
7
import Koa from 'koa'
8
import glob from 'fast-glob'
9
import _ from 'lodash'
10
-import { setConfig, subscribeConfig } from './config'
10
+import { defineConfig, setConfig } from './config'
11
import { FORBIDDEN, IS_WINDOWS } from './const'
12
import events from './events'
13
import { getCurrentUsernameExpanded } from './perm'
@@ -115,7 +115,7 @@ export async function urlToNode(url: string, ctx?: Koa.Context, parent: VfsNode=
115
}
116
117
export let vfs: VfsNode = {}
118
-subscribeConfig<VfsNode>({ k: 'vfs', defaultValue: {} }, data =>
118
+defineConfig<VfsNode>('vfs', {}).sub(data =>
119
vfs = data)
120
121
export function saveVfs() {
server/src/zip.ts
+3
-3
@@ -6,7 +6,7 @@ import { filterMapGenerator, pattern2filter, prefix } from './misc'
6
import { QuickZipStream } from './QuickZipStream'
7
import { createReadStream } from 'fs'
8
import fs from 'fs/promises'
9
-import { defineConfig, getConfig } from './config'
9
+import { defineConfig } from './config'
10
import { dirname } from 'path'
11
12
export async function zipStreamFromFolder(node: VfsNode, ctx: Koa.Context) {
@@ -48,10 +48,10 @@ export async function zipStreamFromFolder(node: VfsNode, ctx: Koa.Context) {
48
catch {}
49
})
50
const zip = new QuickZipStream(mappedWalker)
51
- const time = 1000 * (getConfig('zip_calculate_size_for_seconds'))
51
+ const time = 1000 * zipSeconds.get()
52
ctx.response.length = await zip.calculateSize(time)
53
ctx.body = zip
54
ctx.req.on('close', ()=> zip.destroy())
55
}
56
57
-defineConfig('zip_calculate_size_for_seconds', { defaultValue: 1 })
57
+const zipSeconds = defineConfig('zip_calculate_size_for_seconds', 1)