fix: admin/fs: were suggesting URLs that couldn't work because of force_address
Massimo Melina committed
Dec 7, 2024 at 00:18 UTC
2dccaa227c66011098769f281b8d4b9d565cc20f
6 files changed
+35
-27
admin/src/FileForm.ts
+4
-4
@@ -9,7 +9,7 @@ import { apiCall, UseApi } from './api'
9
import {
10
basename, defaultPerms, formatBytes, formatTimestamp, isWhoObject, newDialog, objSameKeys,
11
onlyTruthy, prefix, VfsPerms, wantArray, Who, WhoObject, matches, HTTP_MESSAGES, xlate, md, Callback,
12
- useRequestRender, splitAt, IMAGE_FILEMASK, copyTextToClipboard
12
+ useRequestRender, splitAt, IMAGE_FILEMASK, copyTextToClipboard, normalizeHost, CFG
13
} from './misc'
14
import { isModifiedConfig } from './AccountForm'
15
import { Btn, Flex, IconBtn, LinkBtn, propsForModifiedValues, useBreakpoint, wikiLink } from './mui'
@@ -329,7 +329,7 @@ function LinkField({ value, statusApi }: LinkFieldProps) {
329
const data = statusApi.getData()
330
331
const urls: string[] = data?.urls.https || data?.urls.http
332
- const baseHost = data?.baseUrl && new URL(data.baseUrl).host
332
+ const baseHost = data?.baseUrl && normalizeHost(new URL(data.baseUrl).host)
333
const root = useMemo(() => baseHost && _.find(data.roots, (root, host) => matches(baseHost, host)),
334
[data])
335
if (root)
@@ -397,7 +397,7 @@ function LinkField({ value, statusApi }: LinkFieldProps) {
397
export async function changeBaseUrl() {
398
return new Promise(async resolve => {
399
const res = await apiCall('get_status')
400
- const { base_url, roots } = await apiCall('get_config', { only: ['base_url', 'roots'] })
400
+ const { base_url, roots } = await apiCall('get_config', { only: [CFG.base_url, CFG.roots] })
401
const urls: string[] = res.urls.https || res.urls.http
402
const domainsFromRoots = Object.keys(roots).map(x => x.split('|')).flat().filter(x => !/[*?]/.test(x))
403
const proto = splitAt('//', urls[0])[0] + '//'
@@ -443,7 +443,7 @@ export async function changeBaseUrl() {
443
children: "Save",
444
async onClick() {
445
if (v !== base_url)
446
- await apiCall('set_config', { values: { base_url: v.replace(/\/$/, '') } })
446
+ await apiCall('set_config', { values: { [CFG.base_url]: v.replace(/\/$/, '') } })
447
resolve(v)
448
close()
449
},
admin/src/InternetPage.ts
+6
-6
@@ -33,8 +33,8 @@ export default function InternetPage({ setTitleSide }: PageProps) {
33
const [checking, setChecking] = useState(false)
34
const [mapping, setMapping] = useState(false)
35
const status = useApiEx('get_status')
36
- const config = useApiEx('get_config', { only: ['base_url'] })
37
- const base_url = config.data?.base_url
36
+ const config = useApiEx('get_config', { only: [CFG.base_url] })
37
+ const baseUrl = config.data?.[CFG.base_url]
38
const localColor = with_([status.data?.http?.error, status.data?.https?.error], ([h, s]) =>
39
h && s ? 'error' : h || s ? 'warning' : 'success')
40
type GetNat = Awaited<ReturnType<typeof getNatInfo>>
@@ -249,7 +249,7 @@ export default function InternetPage({ setTitleSide }: PageProps) {
249
return config.element || h(TitleCard, { icon: Public, title: "Address" },
250
h(Flex, { flexWrap: 'wrap' },
251
"Main address: ",
252
- base_url ? h('tt', {}, base_url) : "automatic, not configured",
252
+ baseUrl ? h('tt', {}, baseUrl) : "automatic, not configured",
253
h(Btn, {
254
size: 'small',
255
variant: 'outlined',
@@ -311,7 +311,7 @@ export default function InternetPage({ setTitleSide }: PageProps) {
311
doubleNat && h(LinkBtn, { display: 'block', onClick: () => alertDialog(MSG_ISP, 'warning') }, "Double NAT"),
312
checkResult ? "Working!" : checkResult === false ? "Failed!" : '',
313
' ',
314
- (base_url > '' || publicIps.length > 0) && data.internalPort && h(LinkBtn, { onClick: () => verify() }, "Verify")
314
+ (baseUrl > '' || publicIps.length > 0) && data.internalPort && h(LinkBtn, { onClick: () => verify() }, "Verify")
315
)
316
}),
317
)
@@ -330,8 +330,8 @@ export default function InternetPage({ setTitleSide }: PageProps) {
330
if (!again && !await confirmDialog("This test will check if your server is working properly on the Internet")) return
331
setChecking(true)
332
try {
333
- const hostname = base_url && new URL(base_url).hostname
334
- const checkUrl = !isIpLan(hostname) && base_url
333
+ const hostname = baseUrl && new URL(baseUrl).hostname
334
+ const checkUrl = !isIpLan(hostname) && baseUrl
335
if (!isIP(hostname) && await stopOnCheckDomain(hostname)) return
336
const urlResult = checkUrl && await apiCall('self_check', { url: checkUrl }).catch(e =>
337
alertDialog(!e.code ? e : "Sorry, this function is not available at the moment. Retry later.", 'error'))
admin/src/VfsPage.ts
+14
-7
@@ -5,7 +5,7 @@ import { apiCall, useApiEx } from './api'
5
import { Alert, Box, Button, Card, CardContent, Grid, Link, List, ListItem, ListItemText, Typography } from '@mui/material'
6
import { state, useSnapState } from './state'
7
import VfsTree, { vfsNodeIcon } from './VfsTree'
8
-import { newDialog, onlyTruthy, prefix, VfsNodeAdminSend } from './misc'
8
+import { CFG, matches, newDialog, normalizeHost, onlyTruthy, prefix, VfsNodeAdminSend } from './misc'
9
import { Flex, useBreakpoint } from './mui'
10
import { reactJoin } from '@hfs/shared'
11
import _ from 'lodash'
@@ -22,15 +22,23 @@ export default function VfsPage({ setTitleSide }: PageProps) {
22
const { vfs, selectedFiles, movingFile } = useSnapState()
23
const { data, reload, element } = useApiEx('get_vfs')
24
useMemo(() => vfs || reload(), [vfs, reload])
25
+ const { data: config } = useApiEx('get_config', { only: [CFG.force_address, CFG.base_url] })
26
const sideBreakpoint = 'md'
27
const isSideBreakpoint = useBreakpoint(sideBreakpoint)
28
const statusApi = useApiEx('get_status')
29
const { data: status } = statusApi
30
const urls = useMemo<string[]>(() => {
30
- const b = status?.baseUrl
31
- const ret = status?.urls.https || status?.urls.http
32
- return b && !ret.includes(b) ? [b, ...ret] : ret
33
- }, [status])
31
+ const force = config?.[CFG.force_address] // when force_address, we'll only suggest urls that will be accepted
32
+ const ret = (status?.urls.https || status?.urls.http)?.filter((url: string) => {
33
+ if (!force) return true
34
+ const host = normalizeHost(new URL(url).host)
35
+ return Object.keys(status.roots).some(mask => matches(host, mask))
36
+ })
37
+ const b = force ? config?.[CFG.base_url] : status?.baseUrl // when force_address, we should only consider user-inputted urls, otherwise 'automatic' is also good
38
+ if (b && ret && !ret.includes(b))
39
+ ret.unshift(b)
40
+ return ret
41
+ }, [status, config])
42
const single = selectedFiles?.length < 2 && (selectedFiles[0] as VfsNode || vfs)
43
const accountsApi = useApiEx<{ list: Account[] }>('get_accounts') // load accounts once and for all, or !isSideBreakpoint will cause a call for each selection
44
@@ -45,7 +53,7 @@ export default function VfsPage({ setTitleSide }: PageProps) {
53
const alert: AlertProps | false = useMemo(() => anythingShared ? {
54
severity: 'warning',
55
children: "Add something to your shared files — click Add"
48
- } : urls && {
56
+ } : urls?.length > 0 && {
57
severity: 'info',
58
children: [
59
"Your shared files can be browsed from ",
@@ -53,7 +61,6 @@ export default function VfsPage({ setTitleSide }: PageProps) {
61
]
62
}, [anythingShared, urls])
63
56
-
64
setTitleSide(useMemo(() => h(Box, { sx: { display: { xs: 'none', md: 'block' } } },
65
h(Alert, { severity: 'info' }, "If you rename or delete here, it's virtual, and only affects what is presented to the users"),
66
alert && h(Alert, alert),
src/cross.ts
+5
-1
@@ -28,7 +28,7 @@ export const THEME_OPTIONS = { auto: '', light: 'light', dark: 'dark' }
28
export const CFG = constMap(['geo_enable', 'geo_allow', 'geo_list', 'geo_allow_unknown', 'dynamic_dns_url',
29
'log', 'error_log', 'log_rotation', 'dont_log_net', 'log_gui', 'log_api', 'log_ua', 'log_spam', 'track_ips',
30
'max_downloads', 'max_downloads_per_ip', 'max_downloads_per_account', 'roots', 'force_address', 'split_uploads',
31
- 'allow_session_ip_change', 'force_lang', 'suspend_plugins'])
31
+ 'allow_session_ip_change', 'force_lang', 'suspend_plugins', 'base_url'])
32
export const LIST = { add: '+', remove: '-', update: '=', props: 'props', ready: 'ready', error: 'e' }
33
export type Dict<T=any> = Record<string, T>
34
export type Falsy = false | null | undefined | '' | 0
@@ -403,6 +403,10 @@ export function xlate(input: any, table: Record<string, any>) {
403
return table[input] ?? input
404
}
405
406
+export function normalizeHost(host: string) {
407
+ return host[0] === '[' ? host.slice(1, host.indexOf(']')) : host?.split(':')[0]
408
+}
409
+
410
export function isIpLocalHost(ip: string) {
411
return ip === '::1' || ip.endsWith('127.0.0.1')
412
}
src/listen.ts
+4
-2
@@ -8,7 +8,9 @@ import { watchLoad } from './watchLoad'
8
import { networkInterfaces } from 'os';
9
import { newConnection } from './connections'
10
import open from 'open'
11
-import { debounceAsync, ipForUrl, makeNetMatcher, MINUTE, objSameKeys, onlyTruthy, prefix, runAt, wait, xlate } from './misc'
11
+import {
12
+ CFG, debounceAsync, ipForUrl, makeNetMatcher, MINUTE, objSameKeys, onlyTruthy, prefix, runAt, wait, xlate
13
+} from './misc'
14
import { PORT_DISABLED, ADMIN_URI, argv, DEV, IS_WINDOWS } from './const'
15
import findProcess from 'find-process'
16
import { anyAccountCanLoginAdmin } from './adminApis'
@@ -25,7 +27,7 @@ let httpsSrv: undefined | http.Server & ServerExtra
27
28
const openBrowserAtStart = defineConfig('open_browser_at_start', !DEV)
29
28
-export const baseUrl = defineConfig('base_url', '',
30
+export const baseUrl = defineConfig(CFG.base_url, '',
31
x => /(?<=\/\/)[^\/]+/.exec(x)?.[0]) // compiled is host only
32
33
export async function getBaseUrlOrDefault() {
src/serveFile.ts
+2
-7
@@ -7,7 +7,7 @@ import { HTTP_BAD_REQUEST, HTTP_FORBIDDEN, HTTP_METHOD_NOT_ALLOWED, HTTP_NO_CONT
7
import { getNodeName, VfsNode } from './vfs'
8
import mimetypes from 'mime-types'
9
import { defineConfig } from './config'
10
-import { CFG, Dict, makeMatcher, matches, with_ } from './misc'
10
+import { CFG, Dict, makeMatcher, matches, normalizeHost, with_ } from './misc'
11
import _ from 'lodash'
12
import { basename } from 'path'
13
import { promisify } from 'util'
@@ -40,7 +40,7 @@ export async function serveFileNode(ctx: Koa.Context, node: VfsNode) {
40
: _.find(mime, (val,mask) => matches(name, mask))
41
if (allowedReferer.get()) {
42
const ref = /\/\/([^:/]+)/.exec(ctx.get('referer'))?.[1] // extract host from url
43
- if (ref && ref !== host() // automatically accept if referer is basically the hosting domain
43
+ if (ref && ref !== normalizeHost(ctx.host) // automatically accept if referer is basically the hosting domain
44
&& !matches(ref, allowedReferer.get()))
45
return ctx.status = HTTP_FORBIDDEN
46
}
@@ -55,11 +55,6 @@ export async function serveFileNode(ctx: Koa.Context, node: VfsNode) {
55
56
if (await maxDownloadsPerAccount(ctx) === undefined) // returning false will not execute other limits
57
await maxDownloads(ctx) || await maxDownloadsPerIp(ctx)
58
-
59
- function host() {
60
- const s = ctx.host
61
- return s[0] === '[' ? s.slice(1, s.indexOf(']')) : s?.split(':')[0]
62
- }
58
}
59
60
const mimeCfg = defineConfig<Dict<string>, (name: string) => string | undefined>('mime', {}, obj => {