admin/options: admin-panel accessible from
Massimo Melina committed
Mar 23, 2023 at 11:19 UTC
fb95c8ca28fefe6b3f0aab67903d082f01dc306d
7 files changed
+39
-14
admin/src/OptionsPage.ts
+6
-1
@@ -39,6 +39,8 @@ export const logLabels = {
39
error_log: "Access error log file"
40
}
41
42
+const NetmaskField = StringField
43
+
44
export default function OptionsPage() {
45
const { data, reload: reloadConfig, element } = useApiEx('get_config', { omit: ['vfs'] })
46
const snap = useSnapState()
@@ -129,6 +131,9 @@ export default function OptionsPage() {
131
helperText: "Leave empty to never delete" },
132
{ k: 'min_available_mb', comp: NumberField, md: 3, min : 0, unit: "MBytes", placeholder: "None",
133
label: "Min. available disk space", helperText: "Reject uploads that don't comply" },
134
+ { k: 'admin_net', comp: NetmaskField, label: "Admin-panel accessible from", placeholder: "any address",
135
+ helperText: "IP address of browser machine. Wildcards supported."
136
+ },
137
{ k: 'zip_calculate_size_for_seconds', comp: NumberField, label: "Calculate ZIP size for", unit: "seconds",
138
helperText: "If time is not enough, the browser will not show download percentage" },
139
{ k: 'mime', comp: StringStringField,
@@ -260,7 +265,7 @@ function AllowedReferer({ label, value, onChange, error }: FieldProps<string>) {
265
placeholder: 'example.com',
266
onChange,
267
error,
263
- helperText: "Masks supported"
268
+ helperText: "Wildcards supported"
269
})
270
)
271
}
plugins/vhosting/plugin.js
+1
-1
@@ -7,7 +7,7 @@ exports.config = {
7
label: '',
8
type: 'array',
9
fields: {
10
- host: { label: "Domain", helperText: "Masks supported: domain.*|other.*" },
10
+ host: { label: "Domain", helperText: "Wildcards supported: domain.*|other.*" },
11
root: { helperText: "Root path in VFS" },
12
}
13
},
src/adminApis.ts
+15
-5
@@ -10,9 +10,7 @@ import {
10
HFS_STARTED,
11
IS_WINDOWS,
12
VERSION,
13
- HTTP_UNAUTHORIZED,
14
- HTTP_NOT_FOUND,
15
- HTTP_BAD_REQUEST, HTTP_SERVER_ERROR
13
+ HTTP_UNAUTHORIZED, HTTP_NOT_FOUND, HTTP_BAD_REQUEST, HTTP_SERVER_ERROR, HTTP_FORBIDDEN
14
} from './const'
15
import vfsApis from './api.vfs'
16
import accountsApis from './api.accounts'
@@ -20,9 +18,9 @@ import pluginsApis from './api.plugins'
18
import monitorApis from './api.monitor'
19
import langApis from './api.lang'
20
import { getConnections } from './connections'
23
-import { debounceAsync, isLocalHost, onOff, waitFor } from './misc'
21
+import { debounceAsync, isLocalHost, matchesNet, onOff, waitFor } from './misc'
22
import events from './events'
25
-import { anyAccountCanLoginAdmin, getFromAccount } from './perm'
23
+import { accountCanLoginAdmin, accountsConfig, getFromAccount } from './perm'
24
import Koa from 'koa'
25
import { getProxyDetected } from './middlewares'
26
import { writeFile } from 'fs/promises'
@@ -32,6 +30,7 @@ import { loggers } from './log'
30
import { execFile } from 'child_process'
31
import { promisify } from 'util'
32
import { customHtmlSections, customHtmlState, saveCustomHtml } from './customHtml'
33
+import _ from 'lodash'
34
35
export const adminApis: ApiHandlers = {
36
@@ -150,6 +149,8 @@ export const adminApis: ApiHandlers = {
149
150
for (const [k, was] of Object.entries(adminApis))
151
adminApis[k] = (params, ctx) => {
152
+ if (!allowAdmin(ctx))
153
+ return new ApiError(HTTP_FORBIDDEN)
154
if (ctxAdminAccess(ctx))
155
return was(params, ctx)
156
const props = { any: anyAccountCanLoginAdmin() }
@@ -159,6 +160,7 @@ for (const [k, was] of Object.entries(adminApis))
160
}
161
162
export const localhostAdmin = defineConfig('localhost_admin', true)
163
+export const adminNet = defineConfig('admin_net', '')
164
export const favicon = defineConfig<string>('favicon')
165
export const title = defineConfig('title', "File server")
166
@@ -173,3 +175,11 @@ const frpDebounced = debounceAsync(async () => {
175
const { stdout } = await promisify(execFile)('tasklist', ['/fi','imagename eq frpc.exe','/nh'])
176
return stdout.includes('frpc')
177
})
178
+
179
+export function anyAccountCanLoginAdmin() {
180
+ return Boolean(_.find(accountsConfig.get(), accountCanLoginAdmin))
181
+}
182
+
183
+export function allowAdmin(ctx: Koa.Context) {
184
+ return matchesNet(ctx, adminNet.get(), true)
185
+}
\ No newline at end of file
src/listen.ts
+1
-1
@@ -11,7 +11,7 @@ import open from 'open'
11
import { debounceAsync, onlyTruthy, wait } from './misc'
12
import { ADMIN_URI, DEV } from './const'
13
import findProcess from 'find-process'
14
-import { anyAccountCanLoginAdmin } from './perm'
14
+import { anyAccountCanLoginAdmin } from './adminApis'
15
import _ from 'lodash'
16
17
interface ServerExtra { name: string, error?: string, busy?: Promise<string> }
src/middlewares.ts
+3
-2
@@ -28,7 +28,7 @@ import { basename, dirname } from 'path'
28
import { pipeline } from 'stream/promises'
29
import formidable from 'formidable'
30
import { uploadWriter } from './upload'
31
-import { favicon } from './adminApis'
31
+import { allowAdmin, favicon } from './adminApis'
32
33
export const gzipper = compress({
34
threshold: 2048,
@@ -86,7 +86,8 @@ export const serveGuiAndSharedFiles: Koa.Middleware = async (ctx, next) => {
86
if (path.length === ADMIN_URI.length - 1 && ADMIN_URI.startsWith(path))
87
return ctx.redirect(ctx.state.revProxyPath + ADMIN_URI)
88
if (path.startsWith(ADMIN_URI))
89
- return serveAdminPrefixed(ctx,next)
89
+ return allowAdmin(ctx) ? serveAdminPrefixed(ctx,next)
90
+ : (ctx.status = HTTP_FORBIDDEN)
91
if (ctx.method === 'PUT') { // curl -T file url/
92
const decPath = decodeURI(path)
93
let rest = basename(decPath)
src/misc.ts
+13
@@ -11,6 +11,7 @@ export * from './util-generators'
11
export * from './util-files'
12
import debounceAsync from './debounceAsync'
13
import { Readable } from 'stream'
14
+import { isMatch } from 'micromatch'
15
export { debounceAsync }
16
17
export type Callback<IN=void, OUT=void> = (x:IN) => OUT
@@ -171,6 +172,18 @@ export function isLocalHost(c: Connection | Koa.Context) {
172
return ip && (ip === '::1' || ip.endsWith('127.0.0.1'))
173
}
174
175
+export function matchesNet(ip: Koa.Context | string, mask: string, emptyReturns=false) {
176
+ if (!mask)
177
+ return emptyReturns
178
+ if (typeof ip !== 'string')
179
+ ip = ip.ip
180
+ return matches(ip, mask)
181
+}
182
+
183
+function matches(s: string, mask: string) {
184
+ return isMatch(s, '(' + mask + ')') // adding () will allow us to use the pipe at root level
185
+}
186
+
187
export function same(a: any, b: any) {
188
try {
189
assert.deepStrictEqual(a, b)
src/perm.ts
-4
@@ -188,7 +188,3 @@ export function accountCanLogin(account: Account) {
188
export function accountCanLoginAdmin(account: Account) {
189
return accountCanLogin(account) && Boolean(getFromAccount(account, a => a.admin))
190
}
191
-
192
-export function anyAccountCanLoginAdmin() {
193
- return Boolean(_.find(accountsConfig.get(), accountCanLoginAdmin))
194
-}