admin/options: force https

Massimo Melina committed Apr 1, 2023 at 22:04 UTC 48a9d23dd9b63dbcc6a0d23ee65ea181e33ef114
3 files changed +27 -7
admin/src/OptionsPage.ts
+10 -6
@@ -64,6 +64,7 @@ export default function OptionsPage() {
64 placeholder: "no limit",
65 md: 3,
66 }
67 + const httpsEnabled = values.https_port >= 0
68 return h(Form, {
69 sx: { maxWidth: '60em' },
70 values,
@@ -91,30 +92,33 @@ export default function OptionsPage() {
92 { k: 'port', comp: ServerPort, md: 3, label:"HTTP port", status: status?.http||true, suggestedPort: 80 },
93 { k: 'https_port', comp: ServerPort, md: 3, label: "HTTPS port", status: status?.https||true, suggestedPort: 443,
94 onChange(v: number) {
94 - if (v >= 0 && values.https_port < 0 && !values.cert)
95 + if (v >= 0 && !httpsEnabled && !values.cert)
96 suggestMakingCert()
97 return v
98 }
99 },
100 { k: 'max_kbps', ...maxSpeedDefaults, label: "Limit output KB/s", helperText: "Doesn't apply to localhost" },
101 { k: 'max_kbps_per_ip', ...maxSpeedDefaults, label: "Limit output KB/s per-ip" },
101 - values.https_port >= 0 && { k: 'cert', comp: FileField, label: "HTTPS certificate file",
102 + httpsEnabled && values.port >= 0 && { k: 'force_https', comp: BoolField, md: 4, label: "Force HTTPS",
103 + helperText: "Not applied to localhost"
104 + },
105 + httpsEnabled && { k: 'cert', comp: FileField, md: 4, label: "HTTPS certificate file",
106 ...with_(status?.https.error, e => isCertError(e) ? {
107 error: true,
108 helperText: [e, ' - ', h(Link, { key: 'fix', sx: { cursor: 'pointer' }, onClick: makeCertAndSave }, "make one")]
109 } : null)
110 },
107 - values.https_port >= 0 && { k: 'private_key', comp: FileField, label: "HTTPS private key file",
111 + httpsEnabled && { k: 'private_key', comp: FileField, md: 4, label: "HTTPS private key file",
112 ...with_(status?.https.error, e => isKeyError(e) ? { error: true, helperText: e } : null)
113 },
114 { k: 'open_browser_at_start', comp: BoolField, label: "Open Admin-panel at start", md: 4,
115 helperText: "Browser is automatically launched with HFS"
116 },
113 - { k: 'localhost_admin', comp: BoolField, label: "Admin access for localhost connections", md: 5,
117 + { k: 'localhost_admin', comp: BoolField, label: "Unprotected admin on localhost", md: 4,
118 getError: x => !x && admins?.length===0 && "First create at least one admin account",
115 - helperText: "To access Admin without entering credentials"
119 + helperText: "Access Admin-panel without entering credentials"
120 },
117 - { k: 'file_menu_on_link', comp: SelectField, label: "Access file menu", sm: 12, md: 3,
121 + { k: 'file_menu_on_link', comp: SelectField, label: "Access file menu", sm: 12, md: 4,
122 options: { "by clicking on file name": true, "by dedicated button": false }
123 },
124 { k: 'title', helperText: "You can see this in the tab of your browser" },
src/listen.ts
+4
@@ -20,6 +20,10 @@ let httpsSrv: http.Server & ServerExtra
20
21 const openBrowserAtStart = defineConfig('open_browser_at_start', !DEV)
22
23 +export function getHttpsWorkingPort() {
24 + return httpsSrv.listening && (httpsSrv.address() as any)?.port
25 +}
26 +
27 export const portCfg = defineConfig<number>('port', 80)
28 portCfg.sub(async port => {
29 while (!app)
src/middlewares.ts
+13 -1
@@ -12,7 +12,7 @@ import {
12 } from './const'
13 import { FRONTEND_URI } from './const'
14 import { statusCodeForMissingPerm, nodeIsDirectory, urlToNode, vfs } from './vfs'
15 -import { dirTraversal, newObj, stream2string, tryJson } from './misc'
15 +import { dirTraversal, isLocalHost, newObj, stream2string, tryJson } from './misc'
16 import { zipStreamFromFolder } from './zip'
17 import { serveFile, serveFileNode } from './serveFile'
18 import { serveGuiFiles } from './serveGuiFiles'
@@ -30,6 +30,10 @@ import formidable from 'formidable'
30 import { uploadWriter } from './upload'
31 import { allowAdmin, favicon } from './adminApis'
32 import { constants } from 'zlib'
33 +import { getHttpsWorkingPort } from './listen'
34 +import { defineConfig } from './config'
35 +
36 +const forceHttps = defineConfig('force_https', true)
37
38 export const gzipper = compress({
39 threshold: 2048,
@@ -78,6 +82,14 @@ export const serveGuiAndSharedFiles: Koa.Middleware = async (ctx, next) => {
82 }
83 if (ctx.body)
84 return next()
85 + if (!ctx.secure && forceHttps.get() && getHttpsWorkingPort() && !isLocalHost(ctx)) {
86 + const { URL } = ctx
87 + URL.protocol = 'https'
88 + URL.port = getHttpsWorkingPort()
89 + ctx.status = 307 // this ensures the client doesn't switch to a simpler GET request
90 + return ctx.redirect(URL.href)
91 + }
92 +
93 if (path.startsWith(FRONTEND_URI))
94 return serveFrontendPrefixed(ctx,next)
95 if (path.length === ADMIN_URI.length - 1 && ADMIN_URI.startsWith(path))