"basic" web for legacy browsers

Massimo Melina committed May 10, 2024 at 17:45 UTC 0e01dce33acd5d3451bd5d2a9272cf64e5620259
5 files changed +75 -3
frontend/index.html
+1 -1
@@ -7,7 +7,7 @@
7 <script type="module" src="/src/index.ts"></script>
8 </head>
9 <body>
10 - <noscript>You need to enable JavaScript to run this app.</noscript>
10 + <noscript>Enable JavaScript or <a href="?get=basic">go simple</a></noscript>
11 <div id="root"></div>
12 <script nomodule>document.getElementById('root').innerText = "Please use a newer browser"</script>
13 </body>
src/basicWeb.ts new
+63
@@ -0,0 +1,63 @@
1 +import { getCurrentUsername, setLoggedIn } from './auth'
2 +import { HTTP_UNAUTHORIZED } from './cross-const'
3 +import Koa from 'koa'
4 +import { defineConfig } from './config'
5 +import { getNodeName, nodeIsDirectory, VfsNode, walkNode } from './vfs'
6 +import { asyncGeneratorToReadable, Dict, filterMapGenerator, pathEncode } from './misc'
7 +import _ from 'lodash'
8 +import { title } from './adminApis'
9 +import { getSection } from './customHtml'
10 +
11 +const autoBasic = defineConfig('auto_basic', true)
12 +
13 +export function basicWeb(ctx: Koa.Context, node: VfsNode) {
14 + const { get } = ctx.query
15 + if (get === 'login') {
16 + if (getCurrentUsername(ctx))
17 + ctx.redirect('?')
18 + else {
19 + ctx.set('WWW-Authenticate', 'Basic')
20 + ctx.status = HTTP_UNAUTHORIZED
21 + }
22 + return true
23 + }
24 + if (get === 'logout') {
25 + ctx.body = `<script>location = '?'</script>`
26 + setLoggedIn(ctx, false)
27 + ctx.status = HTTP_UNAUTHORIZED
28 + return true
29 + }
30 + const forced = get === 'basic'
31 + if (forced || detectBasicAgent()) {
32 + ctx.type = 'html'
33 + const force = forced ? '?get=basic' : ''
34 + const walker = walkNode(node, { ctx, depth: 0 })
35 + const stream = asyncGeneratorToReadable(filterMapGenerator(walker, async el => {
36 + const isFolder = await nodeIsDirectory(el)
37 + const name = getNodeName(el) + (isFolder ? '/' : '')
38 + return `<li>${a(pathEncode(name) + (isFolder ? force : ''), name)}\n`
39 + }))
40 + ctx.body = stream
41 + stream.push(`<title>${title.get()}</title><body>`)
42 + stream.push(getSection('basicHeader'))
43 + const links: Dict<string> = getCurrentUsername(ctx) ? { '?get=logout': "Logout" } : { '?get=login': "Login" }
44 + stream.push(_.map(links, (v,k) => a(k, v)).join(' ') + '\n<ul>\n')
45 + if (ctx.state.originalPath.length > 1)
46 + stream.push('<li>' + a('..' + force, '..') + '\n')
47 + stream.push('</ul>\n')
48 + stream.push(getSection('basicFooter'))
49 + return true
50 + }
51 +
52 + function a(href: string, label: string) {
53 + return `<a href='${href}'>${label}</a>`
54 + }
55 +
56 + function detectBasicAgent() {
57 + const ua = ctx.get('user-agent')
58 + const v = autoBasic.get()
59 + return v && (/Mozilla\/4|WebKit\/([234]\d\d|5[012]\d|53[0123456])[. ]|Trident|Lynx|Firefox\/(\d|[123]\d)\./.test(ua)
60 + || _.isString(v) && ua.includes(v))
61 + }
62 +
63 +}
\ No newline at end of file
src/roots.ts
+7
@@ -18,6 +18,7 @@ forceAddress.sub((v, { version }) => { // convert from legacy configs
18
19 export const rootsMiddleware: Koa.Middleware = (ctx, next) =>
20 (() => {
21 + ctx.state.originalPath = ctx.path
22 const root = roots.compiled()?.(ctx.host)
23 if (!ctx.state.skipFilters && forceAddress.get())
24 if (root === undefined && !isLocalHost(ctx) && ctx.host !== baseUrl.compiled()) {
@@ -44,4 +45,10 @@ export const rootsMiddleware: Koa.Middleware = (ctx, next) =>
45
46 function join(a: string, b: any) {
47 return a + (b && b[0] !== '/' ? '/' : '') + b
48 +}
49 +
50 +declare module "koa" {
51 + interface DefaultState {
52 + originalPath: string // before roots is applied
53 + }
54 }
\ No newline at end of file
src/serveGuiAndSharedFiles.ts
+3 -1
@@ -16,6 +16,7 @@ import { serveGuiFiles } from './serveGuiFiles'
16 import mount from 'koa-mount'
17 import { baseUrl } from './listen'
18 import { asyncGeneratorToReadable, filterMapGenerator, pathEncode } from './misc'
19 +import { basicWeb } from './basicWeb'
20
21 const serveFrontendFiles = serveGuiFiles(process.env.FRONTEND_PROXY, FRONTEND_URI)
22 const serveFrontendPrefixed = mount(FRONTEND_URI.slice(0,-1), serveFrontendFiles)
@@ -102,9 +103,10 @@ export const serveGuiAndSharedFiles: Koa.Middleware = async (ctx, next) => {
103 return serveFrontendFiles(ctx, next)
104 }
105 ctx.set({ server: `HFS ${VERSION} ${BUILD_TIMESTAMP}` })
106 + if (basicWeb(ctx, node)) return
107 return get === 'zip' ? zipStreamFromFolder(node, ctx)
108 : get === 'list' ? sendFolderList(node, ctx)
107 - : serveFrontendFiles(ctx, next)
109 + : serveFrontendFiles(ctx, next)
110 }
111
112 async function sendFolderList(node: VfsNode, ctx: Koa.Context) {
src/serveGuiFiles.ts
+1 -1
@@ -89,7 +89,7 @@ async function treatIndex(ctx: Koa.Context, filesUri: string, body: string) {
89 const timestamp = await getFaviconTimestamp()
90 const lang = await getLangData(ctx)
91 return body
92 - .replace(/((?:src|href) *= *['"])\/?(?!([a-z]+:\/)?\/)/g, '$1' + ctx.state.revProxyPath + filesUri)
92 + .replace(/((?:src|href) *= *['"])\/?(?!([a-z]+:\/)?\/)(?!\?)/g, '$1' + ctx.state.revProxyPath + filesUri)
93 .replace(/<(\/)?(head|body)>/g, (all, isClose, name) => { // must make these changes in one .replace call, otherwise we may encounter head/body tags due to customHtml. This simple trick makes html parsing unnecessary.
94 const isHead = name === 'head'
95 const isBody = !isHead