plugins: frontend will now receive vfs_path config values adjusted with both reverse-proxy-path and root

Massimo Melina committed Mar 23, 2025 at 15:49 UTC 5b8eb3782b328878179df40f9b9f600dc800b8c8
4 files changed +36 -7
dev-plugins.md
+5 -1
@@ -210,7 +210,11 @@ Based on `type`, other properties are supported:
210 - `vfs_path` path to VFS
211 - `folders: boolean` set false to forbid selection of folders. Default is true.
212 - `files: boolean | string` set force to forbid selection of files. If you set a string, it will be used as a file-mask.
213 - E.g. `*.jpg|*.png` Default is true.
213 + E.g. `*.jpg|*.png` Default is true.
214 + Note: the path you configure inside the admin-panel may differ to what the frontend sees, because:
215 + - a "root" is applied to the specific host/domain;
216 + - a reverse-proxy may add something in front.
217 + For this reason, if your config is marked with `frontend: true`, the value that will be actually sent to the frontend may be adjusted if necessary.
218 - `username`
219 - `groups: undefined | boolean` true if you want only groups, false if you want only users. Default is undefined.
220 - `multiple: boolean` if you set this to true, the field will allow the selection of multiple accounts,
src/middlewares.ts
+1 -1
@@ -141,7 +141,7 @@ declare module "koa" {
141 interface DefaultState {
142 params: Record<string, any>
143 account?: Account // user logged in
144 - revProxyPath: string
144 + revProxyPath: string // must not have final slash
145 connection: Connection
146 }
147 }
src/roots.ts
+7 -3
@@ -1,5 +1,8 @@
1 import { defineConfig, getConfig } from './config'
2 -import { ADMIN_URI, API_URI, Callback, CFG, isLocalHost, join, makeMatcher, removeStarting, SPECIAL_URI, try_ } from './misc'
2 +import {
3 + ADMIN_URI, API_URI, Callback, CFG, isLocalHost, join, makeMatcher, removeStarting, SPECIAL_URI, try_,
4 + enforceFinal, enforceStarting
5 +} from './misc'
6 import Koa from 'koa'
7 import { disconnect } from './connections'
8 import { baseUrl } from './listen'
@@ -8,7 +11,7 @@ import _ from 'lodash'
11 export const roots = defineConfig(CFG.roots, {} as { [hostMask: string]: string }, map => {
12 const list = Object.keys(map)
13 const matchers = list.map(hostMask => makeMatcher(hostMask))
11 - const values = Object.values(map)
14 + const values = Object.values(map).map(x => enforceFinal('/', enforceStarting('/', x)))
15 return (host: string) => values[matchers.findIndex(m => m(host))]
16 })
17 const forceAddress = defineConfig(CFG.force_address, false)
@@ -29,7 +32,7 @@ export const rootsMiddleware: Koa.Middleware = (ctx, next) =>
32 if (referer && try_(() => new URL(referer).pathname.startsWith(ctx.state.revProxyPath + ADMIN_URI))) return // exclude apis for admin-panel
33 }
34 if (_.isEmpty(roots.get())) return
32 - const root = roots.compiled()?.(ctx.host)
35 + const root = ctx.state.root = roots.compiled()?.(ctx.host)
36 if (!ctx.state.skipFilters && forceAddress.get())
37 if (root === undefined && !isLocalHost(ctx) && ctx.host !== baseUrl.compiled()) {
38 disconnect(ctx, forceAddress.key())
@@ -51,5 +54,6 @@ export const rootsMiddleware: Koa.Middleware = (ctx, next) =>
54 declare module "koa" {
55 interface DefaultState {
56 originalPath: string // before roots is applied
57 + root?: string
58 }
59 }
\ No newline at end of file
src/serveGuiFiles.ts
+23 -2
@@ -12,7 +12,8 @@ import { refresh_session } from './api.auth'
12 import { ApiError } from './apiMiddleware'
13 import { join, extname } from 'path'
14 import {
15 - CFG, debounceAsync, formatBytes, FRONTEND_OPTIONS, isPrimitive, newObj, objSameKeys, onlyTruthy, parseFileContent
15 + CFG, debounceAsync, formatBytes, FRONTEND_OPTIONS, isPrimitive, newObj, objSameKeys, onlyTruthy, parseFileContent,
16 + enforceStarting
17 } from './misc'
18 import { favicon, title } from './adminApis'
19 import { customHtml, getAllSections, getSection } from './customHtml'
@@ -84,7 +85,7 @@ async function treatIndex(ctx: Koa.Context, filesUri: string, body: string) {
85 const plugins = Object.fromEntries(onlyTruthy(mapPlugins((pl,name) => {
86 let configs = newObj(getPluginConfigFields(name), (v, k, skip) =>
87 !v.frontend ? skip() :
87 - (pluginsConfig.get()?.[name]?.[k] ?? pl.getData().config?.[k]?.defaultValue)
88 + adjustValueByConfig(pluginsConfig.get()?.[name]?.[k], pl.getData().config?.[k])
89 )
90 configs = getPluginInfo(name).onFrontendConfig?.(configs) || configs
91 return !_.isEmpty(configs) && [name, configs]
@@ -150,6 +151,26 @@ async function treatIndex(ctx: Koa.Context, filesUri: string, body: string) {
151 return getSection('bottom') + all
152 return all // unchanged
153 })
154 +
155 + function adjustValueByConfig(v: any, cfg: any) {
156 + v ??= cfg.defaultValue
157 + const {type} = cfg
158 + if (v && type === 'vfs_path') {
159 + v = enforceStarting('/', v)
160 + const { root } = ctx.state
161 + if (root)
162 + if (v.startsWith(root))
163 + v = v.slice(root.length - 1)
164 + else
165 + return
166 + if (ctx.state.revProxyPath)
167 + v = ctx.state.revProxyPath + v
168 + }
169 + else if (type === 'array' && Array.isArray(v))
170 + v = v.map(x => objSameKeys(x, (xv, xk) => adjustValueByConfig(xv, cfg.fields[xk])))
171 + return v
172 + }
173 +
174 }
175
176 function serializeCss(v: any) {