admin/internet: "roots" moved here from "shared files"
Massimo Melina committed
Apr 14, 2024 at 17:27 UTC
605deaff5ee3dbe2cb559c59d4bce1d9898be75d
9 files changed
+59
-84
admin/src/FileForm.ts
+3
-3
@@ -324,7 +324,7 @@ function LinkField({ value, statusApi }: LinkFieldProps) {
324
!urls ? 'error' : // check data is ok
325
h(DisplayField, {
326
label: "Link",
327
- value: link || `outside of configured base address (${baseHost})`,
327
+ value: link || `outside of configured main address (${baseHost})`,
328
error,
329
InputProps: link ? { inputComponent: RenderLink } : undefined,
330
end: h(Box, {},
@@ -376,14 +376,14 @@ export async function changeBaseUrl() {
376
const { base_url } = await apiCall('get_config', { only: ['base_url'] })
377
const urls: string[] = res.urls.https || res.urls.http
378
const { close } = newDialog({
379
- title: "Base address",
379
+ title: "Main address",
380
Content() {
381
const [v, setV] = useState(base_url || '')
382
const proto = new URL(v || urls[0]).protocol + '//'
383
const host = urls.includes(v) ? '' : v.slice(proto.length)
384
const check = h(Check, { sx: { ml: 2 } })
385
return h(Box, { display: 'flex', flexDirection: 'column' },
386
- h(Box, { mb: 2 }, "Choose a base address for your links"),
386
+ h(Box, { mb: 2 }, "Choose a main address for your links"),
387
h(MenuList, {},
388
h(MenuItem, {
389
selected: !v,
admin/src/InternetPage.ts
+28
-8
@@ -16,6 +16,8 @@ import _ from 'lodash'
16
import { SvgIconProps } from '@mui/material/SvgIcon/SvgIcon'
17
import { ConfigForm } from './ConfigForm'
18
import { DynamicDnsResult } from '../../src/ddns'
19
+import { ArrayField } from './ArrayField'
20
+import VfsPathField from './VfsPathField'
21
22
const COUNTRIES = ALL.filter(x => WITH_IP.includes(x.code))
23
@@ -44,8 +46,8 @@ export default function InternetPage() {
46
}, [verifyAgain.state])
47
return h(Flex, { vert: true, gap: '2em', maxWidth: '40em' },
48
h(Alert, { severity: 'info' }, "This page makes sure your site is working correctly on the Internet"),
47
- baseUrlBox(),
49
networkBox(),
50
+ baseUrlBox(),
51
httpsBox(),
52
geoBox(),
53
ddnsBox(),
@@ -240,9 +242,10 @@ export default function InternetPage() {
242
const url = config.data?.base_url
243
const hostname = url && new URL(url).hostname
244
const domain = !isIP(hostname) && hostname
243
- return config.element || h(TitleCard, { icon: Public, title: "Address / Domain" },
245
+ return config.element || h(TitleCard, { icon: Public, title: "Address" },
246
h(Flex, { flexWrap: 'wrap' },
245
- url || "Automatic, not configured",
247
+ "Main address: ",
248
+ url ? h('tt', {}, url) : "automatic, not configured",
249
h(Flex, {}, // keep buttons together when wrapping
250
h(Btn, {
251
size: 'small',
@@ -258,14 +261,31 @@ export default function InternetPage() {
261
}, "Check"),
262
),
263
),
261
- h(ConfigForm<{ force_base_url: boolean }>, {
262
- keys: ['force_base_url'],
264
+ h(ConfigForm<{ roots: any, force_address: boolean }>, {
265
saveOnChange: true,
266
+ onSave() {
267
+ status.reload() // this config is affecting status data
268
+ },
269
+ keys: [CFG.roots, CFG.force_address],
270
form: {
271
fields: [
266
- { k: 'force_base_url', comp: BoolField, disabled: !url,
267
- label: "Accept requests only using domain (and localhost)",
268
- helperText: !url && "You must specify an address, for this option"
272
+ {
273
+ k: 'roots',
274
+ label: false,
275
+ helperText: "You can decide different home-folders (in the VFS) for different domains, a bit like virtual hosts. If none is matched, the default home will be used.",
276
+ comp: ArrayField,
277
+ fields: [
278
+ { k: 'host', label: "Domain/Host", helperText: "Wildcards supported: *.domain.com|other.com" },
279
+ { k: 'root', label: "Home/Root", comp: VfsPathField, placeholder: "default", helperText: "Root path in VFS",
280
+ $column: { renderCell({ value }: any) { return value || h('i', {}, 'default') } } },
281
+ ],
282
+ toField: x => Object.entries(x || {}).map(([host,root]) => ({ host, root })),
283
+ fromField: x => Object.fromEntries(x.map((row: any) => [row.host, row.root || ''])),
284
+ },
285
+ {
286
+ k: CFG.force_address,
287
+ label: "Accept requests only using domains above (and localhost)",
288
+ comp: BoolField,
289
}
290
]
291
},
admin/src/VfsMenuBar.ts
+5
-45
@@ -4,12 +4,9 @@ import { createElement as h } from 'react'
4
import { Alert, Box, List, ListItem, ListItemIcon, ListItemText } from '@mui/material'
5
import { Microsoft, Storage } from '@mui/icons-material'
6
import { reloadVfs } from './VfsPage'
7
-import { CFG, newDialog, prefix } from './misc'
8
-import { Btn, Flex, IconBtn, reloadBtn, useBreakpoint } from './mui'
7
+import { prefix } from './misc'
8
+import { Btn, Flex, IconBtn, reloadBtn } from './mui'
9
import { apiCall, ApiObject, useApi } from './api'
10
-import { ConfigForm } from './ConfigForm'
11
-import { ArrayField } from './ArrayField'
12
-import { BoolField } from '@hfs/mui-grid-form'
10
import VfsPathField from './VfsPathField'
11
import { alertDialog, promptDialog } from './dialog'
12
import { formatDiskSpace } from './FilePicker'
@@ -17,13 +14,11 @@ import { getDiskSpaces } from '../../src/util-os'
14
15
export default function VfsMenuBar({ statusApi }: { statusApi: ApiObject }) {
16
return h(Flex, {
20
- mb: 2,
17
zIndex: 2,
18
backgroundColor: 'background.paper',
19
width: 'fit-content',
20
},
25
- h(Btn, { variant: 'outlined', onClick: roots }, "Roots"),
26
- useBreakpoint('sm') && reloadBtn(() => reloadVfs()),
21
+ reloadBtn(() => reloadVfs()),
22
h(IconBtn, {
23
icon: Storage,
24
title: "Disk spaces",
@@ -41,47 +36,12 @@ export default function VfsMenuBar({ statusApi }: { statusApi: ApiObject }) {
36
}),
37
h(SystemIntegrationButton, statusApi.data)
38
)
44
-
45
- function roots() {
46
- const { close } = newDialog({
47
- title: "Roots for different domains",
48
- dialogProps: { maxWidth: 'sm' },
49
- Content: () => h(ConfigForm<{ roots: any, roots_mandatory: boolean }>, {
50
- onSave() {
51
- statusApi.reload() // this config is affecting status data
52
- close()
53
- },
54
- keys: [CFG.roots, CFG.roots_mandatory],
55
- form: {
56
- fields: [
57
- {
58
- k: 'roots',
59
- label: false,
60
- helperText: "You can decide different home-folders (in the VFS) for different domains, a bit like virtual hosts. If none is matched, the default home will be used.",
61
- comp: ArrayField,
62
- fields: [
63
- { k: 'host', label: "Domain/Host", helperText: "Wildcards supported: *.domain.com|other.com" },
64
- { k: 'root', label: "Home/Root", comp: VfsPathField, placeholder: "default", helperText: "Root path in VFS" },
65
- ],
66
- toField: x => Object.entries(x || {}).map(([host,root]) => ({ host, root })),
67
- fromField: x => Object.fromEntries(x.map((row: any) => [row.host, row.root || ''])),
68
- },
69
- {
70
- k: 'roots_mandatory',
71
- label: "Accept requests only using domains above (and localhost)",
72
- comp: BoolField,
73
- }
74
- ]
75
- }
76
- })
77
- })
78
- }
39
}
40
41
function SystemIntegrationButton({ platform }: { platform: string | undefined }) {
42
const isWindows = platform === 'win32'
43
const { data: integrated, reload } = useApi(isWindows && 'windows_integrated')
84
- return !isWindows ? null : h(Btn, {
44
+ return h(Btn, {
45
icon: Microsoft,
46
variant: 'outlined',
47
doneMessage: true,
@@ -91,7 +51,7 @@ function SystemIntegrationButton({ platform }: { platform: string | undefined })
51
const msg = h(Box, {}, "We are going to add a command in the right-click of Windows File Manager",
52
h('img', { src: 'win-shell.png', style: {
53
display: 'block',
94
- width: 'min(30em, 80vw)',
54
+ width: 'min(30em, 100%)',
55
marginTop: '1em',
56
} }),
57
h(Alert, { severity: 'info' }, "It will also automatically copy the URL, ready to paste!"),
admin/src/VfsPage.ts
+4
-2
@@ -144,8 +144,10 @@ export default function VfsPage() {
144
),
145
h(Grid, { container: true, rowSpacing: 1, columnSpacing: 2, top: 0 },
146
h(Grid, { item: true, xs: 12, [sideBreakpoint]: 6, lg: 6, xl: 5 },
147
- h(Typography, { variant: 'h6', mb: 1, }, "Virtual File System"),
148
- h(VfsMenuBar, { statusApi }),
147
+ h(Flex, { mb: 1, flexWrap: 'wrap', gap: [0, 2] },
148
+ h(Typography, { variant: 'h6' }, "Virtual File System"),
149
+ h(VfsMenuBar, { statusApi }),
150
+ ),
151
vfs && h(VfsTree, { id2node, statusApi }) ),
152
isSideBreakpoint && sideContent && h(Grid, { item: true, [sideBreakpoint]: true, maxWidth: '100%' },
153
h(Card, { sx: { overflow: 'initial' } }, // overflow is incompatible with stickyBar
config.md
+1
-2
@@ -83,10 +83,10 @@ Configuration can be done in several ways
83
- `session_duration` after how many seconds should the login session expire. Default is a day.
84
- `acme_domain` domain used for ACME certificate generation. Default is none.
85
- `acme_email` email used for ACME certificate generation. Default is none.
86
-- `force_base_url` disconnect any connection that's not using the domain used for ACME certificate generation. Default is none.
86
- `acme_renew` automatically renew acme certificate close to expiration. Default is false.
87
- `listen_interface` network interface to listen on, by specifying IP address. Default is any.
88
- `base_url` URL to be used for links generation. Default is automatic.
89
+- `force_address` disconnect any request not made with one of the hosts specified in `roots` or `base_url`. Default is false.
90
- `ignore_proxies` stop warning about detected proxies. Default is false.
91
- `descript_ion` enable reading and writing of comments in the old file format *DESCRIPT.ION*. Default is yes.
92
- `descript_ion_encoding` text encoding to be used for file *DESCRIPT.ION*. [List of supported values](https://github.com/ashtuchkin/iconv-lite/wiki/Supported-Encodings). Default is `utf8`.
@@ -99,7 +99,6 @@ Configuration can be done in several ways
99
music.domain.com: /music
100
image.domain.com: /image
101
```
102
-- `roots_mandatory` disconnect any request not made with one of the hosts specified in `roots`. Default is false.
102
- `max_downloads` limit the number of concurrent downloads on the whole server. Default is unlimited.
103
- `max_downloads_per_ip` limit the number of concurrent downloads for the same IP address. Default is unlimited.
104
- `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/cross.ts
+1
-1
@@ -25,7 +25,7 @@ export const SORT_BY_OPTIONS = ['name', 'extension', 'size', 'time']
25
export const THEME_OPTIONS = { auto: '', light: 'light', dark: 'dark' }
26
export const CFG = constMap(['geo_enable', 'geo_allow', 'geo_list', 'geo_allow_unknown', 'dynamic_dns_url',
27
'log', 'error_log', 'log_rotation', 'dont_log_net', 'log_gui', 'log_api', 'log_ua',
28
- 'max_downloads', 'max_downloads_per_ip', 'max_downloads_per_account', 'roots', 'roots_mandatory'])
28
+ 'max_downloads', 'max_downloads_per_ip', 'max_downloads_per_account', 'roots', 'force_address'])
29
export const LIST = { add: '+', remove: '-', update: '=', props: 'props', ready: 'ready', error: 'e' }
30
export type Dict<T=any> = Record<string, T>
31
export type Falsy = false | null | undefined | '' | 0
src/index.ts
+1
-1
@@ -41,9 +41,9 @@ app.use(sessionMiddleware)
41
.use(gzipper)
42
.use(paramsDecoder) // must be done before plugins, so they can manipulate params
43
.use(headRequests)
44
+ .use(rootsMiddleware)
45
.use(logMw)
46
.use(throttler)
46
- .use(rootsMiddleware)
47
.use(pluginsMiddleware)
48
.use(mount(API_URI, apiMiddleware({ ...frontEndApis, ...adminApis })))
49
.use(serveGuiAndSharedFiles)
src/middlewares.ts
+2
-5
@@ -7,11 +7,11 @@ import { DAY, dirTraversal, isLocalHost, splitAt, stream2string, tryJson } from
7
import { Readable } from 'stream'
8
import { applyBlock } from './block'
9
import { Account, accountCanLogin, getAccount } from './perm'
10
-import { Connection, disconnect, normalizeIp, socket2connection, updateConnectionForCtx } from './connections'
10
+import { Connection, normalizeIp, socket2connection, updateConnectionForCtx } from './connections'
11
import basicAuth from 'basic-auth'
12
import { invalidSessions, setLoggedIn, srpCheck } from './auth'
13
import { constants } from 'zlib'
14
-import { baseUrl, getHttpsWorkingPort } from './listen'
14
+import { getHttpsWorkingPort } from './listen'
15
import { defineConfig } from './config'
16
import session from 'koa-session'
17
import { app } from './index'
@@ -19,7 +19,6 @@ import events from './events'
19
20
const forceHttps = defineConfig('force_https', true)
21
const ignoreProxies = defineConfig('ignore_proxies', false)
22
-const forceBaseUrl = defineConfig('force_base_url', false)
22
export const sessionDuration = defineConfig('session_duration', Number(process.env.SESSION_DURATION) || DAY/1000,
23
v => v * 1000)
24
@@ -76,8 +75,6 @@ export const someSecurity: Koa.Middleware = async (ctx, next) => {
75
catch {
76
return ctx.status = HTTP_FOOL
77
}
79
- if (!ctx.state.skipFilters && forceBaseUrl.get() && baseUrl.compiled() && !isLocalHost(ctx) && ctx.host !== baseUrl.compiled())
80
- return disconnect(ctx, 'force-domain')
78
if (!ctx.secure && forceHttps.get() && getHttpsWorkingPort() && !isLocalHost(ctx)) {
79
const { URL } = ctx
80
URL.protocol = 'https'
src/roots.ts
+14
-17
@@ -1,23 +1,30 @@
1
-import { defineConfig } from './config'
1
+import { defineConfig, getConfig } 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'
5
+import { baseUrl } from './listen'
6
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
- }
8
const list = Object.keys(map)
9
const matchers = list.map(hostMask => makeMatcher(hostMask))
10
const values = Object.values(map)
11
return (host: string) => values[matchers.findIndex(m => m(host))]
12
})
17
-const rootsMandatory = defineConfig(CFG.roots_mandatory, false)
13
+const forceAddress = defineConfig(CFG.force_address, false)
14
+forceAddress.sub((v, { version }) => { // convert from legacy configs
15
+ if (version?.olderThan('0.53.0'))
16
+ forceAddress.set(getConfig('force_base_url') || getConfig('roots_mandatory') || false)
17
+})
18
19
export const rootsMiddleware: Koa.Middleware = (ctx, next) =>
20
(() => {
21
+ const root = roots.compiled()?.(ctx.host)
22
+ if (!ctx.state.skipFilters && forceAddress.get())
23
+ if (root === undefined && !isLocalHost(ctx) && ctx.host !== baseUrl.compiled()) {
24
+ disconnect(ctx, forceAddress.key())
25
+ return true // true will avoid calling next
26
+ }
27
+ if (!root || root === '/') return // not transformation is required
28
let params: undefined | typeof ctx.state.params | typeof ctx.query // undefined if we are not going to work on api parameters
29
if (ctx.path.startsWith(SPECIAL_URI)) { // special uris should be excluded...
30
if (!ctx.path.startsWith(API_URI)) return // ...unless it's an api
@@ -26,16 +33,6 @@ export const rootsMiddleware: Koa.Middleware = (ctx, next) =>
33
if (referer?.startsWith(ctx.state.revProxyPath + ADMIN_URI)) return // exclude apis for admin-panel
34
params = ctx.state.params || ctx.query // for api we'll translate params
35
}
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 (ctx.state.skipFilters || !rootsMandatory.get() || isLocalHost(ctx)) return
36
- disconnect(ctx, 'bad-domain')
37
- return true // true will avoid calling next
38
- }
36
if (!params) {
37
ctx.path = join(root, ctx.path)
38
return