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)