"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