nicer yaml format for roots
Massimo Melina committed
Dec 22, 2023 at 22:19 UTC
bc2d67f6bb152e7153d0134db714555a5dc5dc5f
7 files changed
+35
-14
admin/src/FileForm.ts
+1
-1
@@ -273,7 +273,7 @@ function LinkField({ value, statusApi }: LinkFieldProps) {
273
const { data, reload, error } = statusApi
274
const urls: string[] = data?.urls.https || data?.urls.http
275
const baseHost = data?.baseUrl && new URL(data.baseUrl).hostname
276
- const root = useMemo(() => baseHost && data.roots?.find((row: any) => matches(baseHost, row.host))?.root,
276
+ const root = useMemo(() => baseHost && _.find(data.roots, (root, host) => matches(baseHost, host)),
277
[data])
278
if (root)
279
value &&= value.indexOf(root) === 1 ? value.slice(root.length) : undefined
admin/src/VfsMenuBar.ts
+3
-1
@@ -82,7 +82,9 @@ export default function VfsMenuBar({ statusApi }: { statusApi: ApiObject }) {
82
fields: [
83
{ k: 'host', label: "Domain/Host", helperText: "Wildcards supported: domain.*|other.*" },
84
{ k: 'root', label: "Home/Root", comp: VfsPathField, placeholder: "default", helperText: "Root path in VFS" },
85
- ]
85
+ ],
86
+ toField: x => Object.entries(x || {}).map(([host,root]) => ({ host, root })),
87
+ fromField: x => Object.fromEntries(x.map((row: any) => [row.host, row.root])),
88
},
89
{
90
k: 'roots_mandatory',
admin/src/VfsTree.ts
+3
-1
@@ -12,6 +12,7 @@ import { onlyTruthy, Who, with_ } from './misc'
12
import { iconTooltip } from './mui'
13
import { apiCall, ApiObject } from './api'
14
import { alertDialog, confirmDialog } from './dialog'
15
+import _ from 'lodash'
16
17
export const FolderIcon = Folder
18
export const FileIcon = InsertDriveFileOutlined
@@ -96,7 +97,8 @@ export default function VfsTree({ id2node, statusApi }:{ id2node: Map<string, Vf
97
node.default && iconTooltip(Web, "Act as website"),
98
node.masks && iconTooltip(TheaterComedy, "Masks"),
99
node.size === -1 && iconTooltip(HighlightOff, "Source not found"),
99
- with_(statusApi.data?.roots?.find((row: any) => row.root === id.slice(1)), row => row && iconTooltip(Home, `home for ${row.host}`))
100
+ with_(_.findKey(statusApi.data?.roots, root => root === id.slice(1)), host =>
101
+ host && iconTooltip(Home, `home for ${host}`))
102
),
103
),
104
isRoot ? "Home" : (() => { // special rendering if the whole source is not too long, and the name was not customized
config.md
+7
@@ -90,6 +90,13 @@ Configuration can be done in several ways
90
- `server_code` javascript code that works similarly to [a plugin](dev-plugins.md).
91
- `tiles_size` starting value for frontend's tiles size. Default is 0.
92
- `update_to_beta` includes beta versions searching for updates. Default is false.
93
+- `roots` maps hosts (or mask of hosts) to a root different from the home folder. Default is none. E.g.
94
+ ```
95
+ roots:
96
+ music.domain.com: /music
97
+ image.domain.com: /image
98
+ ```
99
+- `roots_mandatory` disconnect any request not made with one of the hosts specified in `roots`. Default is false.
100
- `max_downloads` limit the number of concurrent downloads on the whole server. Default is unlimited.
101
- `max_downloads_per_ip` limit the number of concurrent downloads for the same IP address. Default is unlimited.
102
- `max_downloads_per_account` limit the number of concurrent downloads for each account. This is enforced only for connections that are logged in, and will override other similar settings. Default is unlimited.
src/roots.ts
+16
-8
@@ -2,10 +2,17 @@ import { defineConfig } from './config'
2
import { ADMIN_URI, API_URI, CFG, isLocalHost, makeMatcher, SPECIAL_URI } from './misc'
3
import Koa from 'koa'
4
import { disconnect } from './connections'
5
+import _ from 'lodash'
6
6
-export const roots = defineConfig(CFG.roots, [] as { host: string, root: string }[], list => {
7
- const matchers = list.map((row: any) => typeof row?.host === 'string' ? makeMatcher(row.host) : () => false)
8
- return (host: string) => list[matchers.findIndex(m => m(host))]
7
+export const roots = defineConfig(CFG.roots, {} as { [hostMask: string]: string }, map => {
8
+ if (_.isArray(map)) { // legacy pre 0.51.0-alpha5, remove in 0.52
9
+ roots.set(Object.fromEntries(map.map(x => [x.host, x.root])))
10
+ return
11
+ }
12
+ const list = Object.keys(map)
13
+ const matchers = list.map(hostMask => makeMatcher(hostMask))
14
+ const values = Object.values(map)
15
+ return (host: string) => values[matchers.findIndex(m => m(host))]
16
})
17
const rootsMandatory = defineConfig(CFG.roots_mandatory, false)
18
@@ -19,15 +26,16 @@ export const rootsMiddleware: Koa.Middleware = (ctx, next) =>
26
if (referer?.startsWith(ctx.state.revProxyPath + ADMIN_URI)) return // exclude apis for admin-panel
27
params = ctx.params || ctx.query // for api we'll translate params
28
}
22
- if (!roots.get()?.length) return
23
- const row = roots.compiled()(ctx.host)
24
- if (!row) {
29
+ if (_.isEmpty(roots.get())) return
30
+ const host2root = roots.compiled()
31
+ if (!host2root) return
32
+ const root = host2root(ctx.host)
33
+ if (root === '' || root === '/') return
34
+ if (root === undefined) {
35
if (!rootsMandatory.get() || isLocalHost(ctx)) return
36
disconnect(ctx)
37
return true // true will avoid calling next
38
}
29
- const { root='' } = row
30
- if (!root || root === '/') return
39
if (!params) {
40
ctx.path = join(root, ctx.path)
41
return
tests/config.yaml
+2
@@ -131,3 +131,5 @@ accounts:
131
version: 0.51.0-alpha4
132
max_downloads_per_account: 2
133
max_downloads: 1
134
+roots:
135
+ 127.0.0.1:*: f1/
tests/test.ts
+3
-3
@@ -38,6 +38,7 @@ describe('basics', () => {
38
it('bad range', req('/f1/f2/alfa.txt', 416, {
39
headers: { Range: 'bytes=7-' }
40
}))
41
+ it('roots', req('/f2/alfa.txt', 200, { baseUrl: BASE_URL.replace('localhost', '127.0.0.1') })) // host 127.0.0.1 is rooted in /f1
42
it('website', req('/f1/page/', { re:/This is a test/, mime:'text/html' }))
43
it('traversal', req('/f1/page/.%2e/.%2e/README.md', 418))
44
it('custom mime from above', req('/tests/page/index.html', { status: 200, mime:'text/plain' }))
@@ -184,9 +185,9 @@ type Tester = number
185
186
const jar = {}
187
187
-function req(url: string, test:Tester, requestOptions: XRequestOptions & { throttle?: number }={}) {
188
+function req(url: string, test:Tester, { baseUrl, throttle, ...requestOptions }: XRequestOptions & { throttle?: number, baseUrl?: string }={}) {
189
// passing 'path' keeps it as it is, avoiding internal resolving
189
- return () => httpStream(BASE_URL + url, { path: url, jar, ...requestOptions }).catch(e => {
190
+ return () => httpStream((baseUrl || BASE_URL) + url, { path: url, jar, ...requestOptions }).catch(e => {
191
if (e.code === "ECONNREFUSED")
192
throw e
193
return e.cause
@@ -198,7 +199,6 @@ function req(url: string, test:Tester, requestOptions: XRequestOptions & { throt
199
test = { re:test }
200
if (typeof test === 'number')
201
test = { status: test }
201
- const { throttle } = requestOptions
202
const stream = throttle ? res.pipe(new ThrottledStream(new ThrottleGroup(throttle))) : res
203
const data = await stream2string(stream)
204
const obj = tryJson(data)