better code: use constants

Massimo Melina committed Jan 13, 2023 at 19:09 UTC 2e0c40333eb4a6ed6342cee66b44c35d3d7d9829
16 files changed +139 -95
src/adminApis.ts
+9 -7
@@ -7,11 +7,13 @@ import {
7 API_VERSION,
8 BUILD_TIMESTAMP,
9 COMPATIBLE_API_VERSION,
10 - FORBIDDEN,
10 HFS_STARTED,
11 IS_WINDOWS,
13 - UNAUTHORIZED,
14 - VERSION
12 + VERSION,
13 + HTTP_UNAUTHORIZED,
14 + HTTP_FORBIDDEN,
15 + HTTP_NOT_FOUND,
16 + HTTP_BAD_REQUEST
17 } from './const'
18 import vfsApis from './api.vfs'
19 import accountsApis from './api.accounts'
@@ -44,7 +46,7 @@ export const adminApis: ApiHandlers = {
46 const noHttp = (v.port ?? portCfg.get()) < 0 || !st.httpSrv.listening
47 const noHttps = (v.https_port ?? httpsPortCfg.get()) < 0 || !st.httpsSrv.listening
48 if (noHttp && noHttps)
47 - return new ApiError(FORBIDDEN, "You cannot switch off both http and https ports")
49 + return new ApiError(HTTP_FORBIDDEN, "You cannot switch off both http and https ports")
50 await setConfig(v)
51 }
52 return {}
@@ -82,7 +84,7 @@ export const adminApis: ApiHandlers = {
84
85 async save_pem({ cert, private_key, name='self' }) {
86 if (!cert || !private_key)
85 - return new ApiError(400)
87 + return new ApiError(HTTP_BAD_REQUEST)
88 const files = { cert: name + '.cert', private_key: name + '.key' }
89 await writeFile(files.private_key, private_key)
90 await writeFile(files.cert, cert)
@@ -95,7 +97,7 @@ export const adminApis: ApiHandlers = {
97 doAtStart(list) {
98 const logger = loggers.find(l => l.name === file)
99 if (!logger)
98 - return list.error(404, true)
100 + return list.error(HTTP_NOT_FOUND, true)
101 const input = createReadStream(logger.path)
102 input.on('error', async (e: any) => {
103 if (e.code === 'ENOENT') // ignore ENOENT, consider it an empty log
@@ -143,7 +145,7 @@ for (const k in adminApis) {
145 const was = adminApis[k]
146 adminApis[k] = (params, ctx) =>
147 ctxAdminAccess(ctx) ? was(params, ctx)
146 - : new ApiError(UNAUTHORIZED)
148 + : new ApiError(HTTP_UNAUTHORIZED)
149 }
150
151 export const localhostAdmin = defineConfig('localhost_admin', true)
src/api.accounts.ts
+7 -7
@@ -14,7 +14,7 @@ import {
14 setAccount
15 } from './perm'
16 import _ from 'lodash'
17 -import { FORBIDDEN } from './const'
17 +import { HTTP_BAD_REQUEST, HTTP_FORBIDDEN, HTTP_NOT_FOUND } from './const'
18
19 function prepareAccount(ac: Account | undefined) {
20 return ac && {
@@ -33,7 +33,7 @@ const apis: ApiHandlers = {
33
34 get_account({ username }, ctx) {
35 return prepareAccount(getAccount(username || getCurrentUsername(ctx)))
36 - || new ApiError(404)
36 + || new ApiError(HTTP_NOT_FOUND)
37 },
38
39 get_accounts() {
@@ -49,20 +49,20 @@ const apis: ApiHandlers = {
49 if (admin === null)
50 changes.admin = undefined
51 else if (admin !== undefined && typeof admin !== 'boolean')
52 - return new ApiError(400, "invalid admin")
52 + return new ApiError(HTTP_BAD_REQUEST, "invalid admin")
53 const acc = setAccount(username, changes)
54 - return acc ? _.pick(acc, 'username') : new ApiError(400)
54 + return acc ? _.pick(acc, 'username') : new ApiError(HTTP_BAD_REQUEST)
55 },
56
57 add_account({ username, ...rest }) {
58 if (getAccount(username))
59 - return new ApiError(FORBIDDEN)
59 + return new ApiError(HTTP_FORBIDDEN)
60 const acc = addAccount(username, rest)
61 - return acc ? _.pick(acc, 'username') : new ApiError(400)
61 + return acc ? _.pick(acc, 'username') : new ApiError(HTTP_BAD_REQUEST)
62 },
63
64 del_account({ username }) {
65 - return delAccount(username) ? {} : new ApiError(400)
65 + return delAccount(username) ? {} : new ApiError(HTTP_BAD_REQUEST)
66 },
67
68 async change_password_others({ username, newPassword }) {
src/api.auth.ts
+24 -17
@@ -4,7 +4,14 @@ import { Account, getAccount, getCurrentUsername, normalizeUsername } from './pe
4 import { verifyPassword } from './crypt'
5 import { ApiError, ApiHandler } from './apiMiddleware'
6 import { SRPParameters, SRPRoutines, SRPServerSession, SRPServerSessionStep1 } from 'tssrp6a'
7 -import { ADMIN_URI, SESSION_DURATION, UNAUTHORIZED } from './const'
7 +import {
8 + ADMIN_URI,
9 + HTTP_SERVER_ERROR,
10 + SESSION_DURATION,
11 + HTTP_UNAUTHORIZED,
12 + HTTP_BAD_REQUEST,
13 + HTTP_NOT_ACCEPTABLE, HTTP_CONFLICT
14 +} from './const'
15 import { randomId } from './misc'
16 import Koa from 'koa'
17 import { changeSrpHelper, changePasswordHelper } from './api.helpers'
@@ -18,7 +25,7 @@ const ongoingLogins:Record<string,SRPServerSessionStep1> = {} // store data that
25 async function loggedIn(ctx:Koa.Context, username: string | false) {
26 const s = ctx.session
27 if (!s)
21 - return ctx.throw(500,'session')
28 + return ctx.throw(HTTP_SERVER_ERROR,'session')
29 if (username === false) {
30 delete s.username
31 ctx.cookies.set('csrf', '')
@@ -36,28 +43,28 @@ function makeExp() {
43
44 export const login: ApiHandler = async ({ username, password }, ctx) => {
45 if (!username || !password) // some validation
39 - return new ApiError(400)
46 + return new ApiError(HTTP_BAD_REQUEST)
47 const acc = getAccount(username)
48 if (!acc)
42 - return new ApiError(UNAUTHORIZED)
49 + return new ApiError(HTTP_UNAUTHORIZED)
50 if (!acc.hashed_password)
44 - return new ApiError(406)
51 + return new ApiError(HTTP_NOT_ACCEPTABLE)
52 if (!await verifyPassword(acc.hashed_password, password))
46 - return new ApiError(UNAUTHORIZED)
53 + return new ApiError(HTTP_UNAUTHORIZED)
54 if (!ctx.session)
48 - return new ApiError(500)
55 + return new ApiError(HTTP_SERVER_ERROR)
56 await loggedIn(ctx, username)
57 return { ...makeExp(), redirect: acc.redirect }
58 }
59
60 export const loginSrp1: ApiHandler = async ({ username }, ctx) => {
61 if (!username)
55 - return new ApiError(400)
62 + return new ApiError(HTTP_BAD_REQUEST)
63 const account = getAccount(username)
64 if (!ctx.session)
58 - return new ApiError(500)
65 + return new ApiError(HTTP_SERVER_ERROR)
66 if (!account) // TODO simulate fake account to prevent knowing valid usernames
60 - return new ApiError(UNAUTHORIZED)
67 + return new ApiError(HTTP_UNAUTHORIZED)
68 try {
69 const { step1, ...rest } = await srpStep1(account)
70 const sid = Math.random()
@@ -73,7 +80,7 @@ export const loginSrp1: ApiHandler = async ({ username }, ctx) => {
80
81 export async function srpStep1(account: Account) {
82 if (!account.srp)
76 - throw 406 // unacceptable
83 + throw HTTP_NOT_ACCEPTABLE
84 const [salt, verifier] = account.srp.split('|')
85 const srpSession = new SRPServerSession(srp6aNimbusRoutines)
86 const step1 = await srpSession.step1(account.username, BigInt(salt), BigInt(verifier))
@@ -82,9 +89,9 @@ export async function srpStep1(account: Account) {
89
90 export const loginSrp2: ApiHandler = async ({ pubKey, proof }, ctx) => {
91 if (!ctx.session)
85 - return new ApiError(500)
92 + return new ApiError(HTTP_SERVER_ERROR)
93 if (!ctx.session.login)
87 - return new ApiError(409)
94 + return new ApiError(HTTP_CONFLICT)
95 const { username, sid } = ctx.session.login
96 const step1 = ongoingLogins[sid]
97 try {
@@ -97,7 +104,7 @@ export const loginSrp2: ApiHandler = async ({ pubKey, proof }, ctx) => {
104 }
105 }
106 catch(e) {
100 - return new ApiError(UNAUTHORIZED, String(e))
107 + return new ApiError(HTTP_UNAUTHORIZED, String(e))
108 }
109 finally {
110 delete ongoingLogins[sid]
@@ -106,14 +113,14 @@ export const loginSrp2: ApiHandler = async ({ pubKey, proof }, ctx) => {
113
114 export const logout: ApiHandler = async ({}, ctx) => {
115 if (!ctx.session)
109 - return new ApiError(500)
116 + return new ApiError(HTTP_SERVER_ERROR)
117 await loggedIn(ctx, false)
118 // 401 is a convenient code for OK: the browser clears a possible http authentication (hopefully), and Admin automatically triggers login dialog
112 - return new ApiError(401)
119 + return new ApiError(HTTP_UNAUTHORIZED)
120 }
121
122 export const refresh_session: ApiHandler = async ({}, ctx) => {
116 - return !ctx.session ? new ApiError(500) : {
123 + return !ctx.session ? new ApiError(HTTP_SERVER_ERROR) : {
124 username: getCurrentUsername(ctx),
125 adminUrl: ctxAdminAccess(ctx) ? ADMIN_URI : undefined,
126 ...makeExp(),
src/api.file_list.ts
+5 -4
@@ -6,20 +6,21 @@ import { stat } from 'fs/promises'
6 import { mapPlugins } from './plugins'
7 import { asyncGeneratorToArray, dirTraversal, pattern2filter } from './misc'
8 import _ from 'lodash'
9 +import { HTTP_BAD_REQUEST, HTTP_FOOL, HTTP_METHOD_NOT_ALLOWED, HTTP_NOT_FOUND } from './const'
10
11 export const file_list: ApiHandler = async ({ path, offset, limit, search, omit, sse }, ctx) => {
12 let node = await urlToNode(path || '/', ctx)
13 const list = new SendListReadable()
14 if (!node)
14 - return fail(404)
15 + return fail(HTTP_NOT_FOUND)
16 if (!hasPermission(node,'can_read',ctx))
17 return fail(cantReadStatusCode(node))
18 if (dirTraversal(search))
18 - return fail(418)
19 + return fail(HTTP_FOOL)
20 if (node.default)
21 return (sse ? list.custom : _.identity)({ redirect: path }) // sse will wrap the object in a 'custom' message, otherwise we plainly return the object
22 if (!await nodeIsDirectory(node))
22 - return fail(405) // method not allowed on target
23 + return fail(HTTP_METHOD_NOT_ALLOWED)
24 offset = Number(offset)
25 limit = Number(limit)
26 const filter = pattern2filter(search)
@@ -64,7 +65,7 @@ export const file_list: ApiHandler = async ({ path, offset, limit, search, omit,
65 }
66 if (omit) {
67 if (omit !== 'c')
67 - ctx.throw(400, 'omit')
68 + ctx.throw(HTTP_BAD_REQUEST, 'omit')
69 if (!entry.m)
70 entry.m = entry.c
71 delete entry.c
src/api.helpers.ts
+6 -6
@@ -2,13 +2,13 @@
2
3 import { Account, allowClearTextLogin, saveSrpInfo, updateAccount } from './perm'
4 import { ApiError } from './apiMiddleware'
5 -import { UNAUTHORIZED } from './const'
5 +import { HTTP_BAD_REQUEST, HTTP_NOT_ACCEPTABLE, HTTP_UNAUTHORIZED } from './const'
6
7 export async function changePasswordHelper(account: Account | undefined, newPassword: string) {
8 if (!newPassword) // clear text version
9 - return new ApiError(400, 'missing parameters')
9 + return new ApiError(HTTP_BAD_REQUEST, 'missing parameters')
10 if (!account)
11 - return new ApiError(UNAUTHORIZED)
11 + return new ApiError(HTTP_UNAUTHORIZED)
12 await updateAccount(account, account => {
13 account.password = newPassword
14 })
@@ -17,11 +17,11 @@ export async function changePasswordHelper(account: Account | undefined, newPass
17
18 export async function changeSrpHelper(account: Account | undefined, salt: string, verifier: string) {
19 if (allowClearTextLogin.get())
20 - return new ApiError(406)
20 + return new ApiError(HTTP_NOT_ACCEPTABLE)
21 if (!salt || !verifier)
22 - return new ApiError(400, 'missing parameters')
22 + return new ApiError(HTTP_BAD_REQUEST, 'missing parameters')
23 if (!account)
24 - return new ApiError(UNAUTHORIZED)
24 + return new ApiError(HTTP_UNAUTHORIZED)
25 await updateAccount(account, account => {
26 saveSrpInfo(account, salt, verifier)
27 delete account.hashed_password // remove leftovers
src/api.vfs.ts
+10 -10
@@ -8,7 +8,7 @@ import { dirname, join, resolve } from 'path'
8 import { dirStream, isWindowsDrive, objSameKeys } from './misc'
9 import { exec } from 'child_process'
10 import { promisify } from 'util'
11 -import { FORBIDDEN, IS_WINDOWS } from './const'
11 +import { HTTP_BAD_REQUEST, HTTP_FORBIDDEN, IS_WINDOWS, HTTP_NOT_FOUND, HTTP_SERVER_ERROR, HTTP_CONFLICT } from './const'
12 import { isMatch } from 'micromatch'
13
14 type VfsAdmin = {
@@ -59,7 +59,7 @@ const apis: ApiHandlers = {
59 async set_vfs({ uri, props }) {
60 const n = await urlToNodeOriginal(uri)
61 if (!n)
62 - return new ApiError(404, 'path not found')
62 + return new ApiError(HTTP_NOT_FOUND, 'path not found')
63 props = pickProps(props, ['name','source','can_see','can_read','masks','default'])
64 props = objSameKeys(props, v => v === null ? undefined : v) // null is a way to serialize undefined, that will restore default values
65 if (props.masks && typeof props.masks !== 'object')
@@ -74,14 +74,14 @@ const apis: ApiHandlers = {
74 async add_vfs({ under, source, name }) {
75 const n = under ? await urlToNodeOriginal(under) : vfs
76 if (!n)
77 - return new ApiError(404, 'invalid under')
77 + return new ApiError(HTTP_NOT_FOUND, 'invalid under')
78 if (n.isTemp || !await nodeIsDirectory(n))
79 - return new ApiError(FORBIDDEN, 'invalid under')
79 + return new ApiError(HTTP_FORBIDDEN, 'invalid under')
80 if (isWindowsDrive(source))
81 source += '\\' // slash must be included, otherwise it will refer to the cwd of that drive
82 const a = n.children || (n.children = [])
83 if (source && a.find(x => x.source === source))
84 - return new ApiError(409, 'already present')
84 + return new ApiError(HTTP_CONFLICT, 'already present')
85 a.unshift({ source, name })
86 await saveVfs()
87 return {}
@@ -89,21 +89,21 @@ const apis: ApiHandlers = {
89
90 async del_vfs({ uris }) {
91 if (!uris || !Array.isArray(uris))
92 - return new ApiError(400, 'invalid uris')
92 + return new ApiError(HTTP_BAD_REQUEST, 'invalid uris')
93 return {
94 errors: await Promise.all(uris.map(async uri => {
95 if (typeof uri !== 'string')
96 - return 400
96 + return HTTP_BAD_REQUEST
97 const node = await urlToNodeOriginal(uri)
98 if (!node)
99 - return 404
99 + return HTTP_NOT_FOUND
100 const parent = dirname(uri)
101 const parentNode = await urlToNodeOriginal(parent)
102 if (!parentNode)
103 - return FORBIDDEN
103 + return HTTP_FORBIDDEN
104 const { children } = parentNode
105 if (!children) // shouldn't happen
106 - return 500
106 + return HTTP_SERVER_ERROR
107 const idx = children.indexOf(node)
108 children.splice(idx, 1)
109 saveVfs()
src/apiMiddleware.ts
+4 -4
@@ -5,7 +5,7 @@ import createSSE from './sse'
5 import { Readable } from 'stream'
6 import { asyncGeneratorToReadable, onOff } from './misc'
7 import events from './events'
8 -import { UNAUTHORIZED } from './const'
8 +import { HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_UNAUTHORIZED } from './const'
9 import _, { DebouncedFunc } from 'lodash'
10
11 export class ApiError extends Error {
@@ -23,11 +23,11 @@ export function apiMiddleware(apis: ApiHandlers) : Koa.Middleware {
23 console.debug('API', ctx.method, ctx.path, { ...params })
24 if (!apis.hasOwnProperty(ctx.path)) {
25 ctx.body = 'invalid api'
26 - return ctx.status = 404
26 + return ctx.status = HTTP_NOT_FOUND
27 }
28 const csrf = ctx.cookies.get('csrf')
29 // we don't rely on SameSite cookie option because it's https-only
30 - let res = csrf && csrf !== params.csrf ? new ApiError(UNAUTHORIZED, 'csrf')
30 + let res = csrf && csrf !== params.csrf ? new ApiError(HTTP_UNAUTHORIZED, 'csrf')
31 : await apis[ctx.path](params || {}, ctx)
32 if (isAsyncGenerator(res))
33 res = asyncGeneratorToReadable(res)
@@ -44,7 +44,7 @@ export function apiMiddleware(apis: ApiHandlers) : Koa.Middleware {
44 }
45 if (res instanceof Error) { // generic exception
46 ctx.body = String(res)
47 - return ctx.status = 400
47 + return ctx.status = HTTP_BAD_REQUEST
48 }
49 ctx.body = res
50 }
src/const.ts
+16 -4
@@ -5,6 +5,7 @@ import * as fs from 'fs'
5 import { homedir } from 'os'
6 import { mkdirSync } from 'fs'
7 import { basename, dirname, join } from 'path'
8 +import http2 from 'http2'
9
10 export const argv = minimist(process.argv.slice(2))
11 export const DEV = process.env.DEV || argv.dev ? 'DEV' : ''
@@ -26,10 +27,21 @@ export const ADMIN_URI = SPECIAL_URI + 'admin/'
27 export const API_URI = SPECIAL_URI + 'api/'
28 export const PLUGINS_PUB_URI = SPECIAL_URI + 'plugins/'
29
29 -export const METHOD_NOT_ALLOWED = 405
30 -export const NO_CONTENT = 204
31 -export const FORBIDDEN = 403
32 -export const UNAUTHORIZED = 401
30 +export const HTTP_OK = 200
31 +export const HTTP_NO_CONTENT = 204
32 +export const HTTP_PARTIAL_CONTENT = 206
33 +export const HTTP_TEMPORARY_REDIRECT = 302
34 +export const HTTP_NOT_MODIFIED = 304
35 +export const HTTP_BAD_REQUEST = 400
36 +export const HTTP_UNAUTHORIZED = 401
37 +export const HTTP_FORBIDDEN = 403
38 +export const HTTP_NOT_FOUND = 404
39 +export const HTTP_METHOD_NOT_ALLOWED = 405
40 +export const HTTP_NOT_ACCEPTABLE = 406
41 +export const HTTP_CONFLICT = 409
42 +export const HTTP_RANGE_NOT_SATISFIABLE = 416
43 +export const HTTP_FOOL = 418
44 +export const HTTP_SERVER_ERROR = 500
45
46 export const IS_WINDOWS = process.platform === 'win32'
47 const IS_BINARY = !basename(process.argv0).includes('node') // this won't be node if pkg was used
src/github.ts
+2 -1
@@ -5,6 +5,7 @@ import { getAvailablePlugins, mapPlugins, parsePluginSource, PATH as PLUGINS_PAT
5 import unzipper from 'unzip-stream'
6 import { ApiError } from './apiMiddleware'
7 import _ from 'lodash'
8 +import { HTTP_CONFLICT } from './const'
9
10 const DIST_ROOT = 'dist/'
11
@@ -21,7 +22,7 @@ function downloadProgress(id: string, status: DownloadStatus) {
22
23 export async function downloadPlugin(repo: string, branch='', overwrite?: boolean) {
24 if (downloading[repo])
24 - return new ApiError(409, "already downloading")
25 + return new ApiError(HTTP_CONFLICT, "already downloading")
26 downloadProgress(repo, true)
27 const rec = await getRepoInfo(repo)
28 if (!branch)
src/middlewares.ts
+24 -14
@@ -3,10 +3,18 @@
3 import compress from 'koa-compress'
4 import Koa from 'koa'
5 import session from 'koa-session'
6 -import { ADMIN_URI, BUILD_TIMESTAMP, DEV, FORBIDDEN, SESSION_DURATION } from './const'
7 -import Application from 'koa'
6 +import {
7 + HTTP_FOOL,
8 + ADMIN_URI,
9 + BUILD_TIMESTAMP,
10 + DEV,
11 + HTTP_FORBIDDEN,
12 + SESSION_DURATION,
13 + HTTP_UNAUTHORIZED,
14 + HTTP_NOT_FOUND
15 +} from './const'
16 import { FRONTEND_URI } from './const'
9 -import { cantReadStatusCode, hasPermission, nodeIsDirectory, urlToNode } from './vfs'
17 +import { cantReadStatusCode, hasPermission, nodeIsDirectory, urlToNode, vfs, VfsNode } from './vfs'
18 import { dirTraversal, objSameKeys, tryJson } from './misc'
19 import { zipStreamFromFolder } from './zip'
20 import { serveFileNode } from './serveFile'
@@ -19,7 +27,9 @@ import { socket2connection, updateConnection, normalizeIp } from './connections'
27 import basicAuth from 'basic-auth'
28 import { SRPClientSession, SRPParameters, SRPRoutines } from 'tssrp6a'
29 import { srpStep1 } from './api.auth'
22 -import { IncomingMessage } from 'http'
30 +import { basename, dirname, join } from 'path'
31 +import { createWriteStream, mkdirSync } from 'fs'
32 +import { pipeline } from 'stream/promises'
33
34 export const gzipper = compress({
35 threshold: 2048,
@@ -45,7 +55,7 @@ export const headRequests: Koa.Middleware = async (ctx, next) => {
55 ctx.response.length = length
56 }
57
48 -export const sessions = (app: Application) => session({
58 +export const sessions = (app: Koa) => session({
59 key: 'hfs_$id',
60 signed: true,
61 rolling: true,
@@ -68,7 +78,7 @@ export const serveGuiAndSharedFiles: Koa.Middleware = async (ctx, next) => {
78 return serveAdminPrefixed(ctx,next)
79 const node = await urlToNode(path, ctx)
80 if (!node)
71 - return ctx.status = 404
81 + return ctx.status = HTTP_NOT_FOUND
82 const canRead = hasPermission(node, 'can_read', ctx)
83 const isFolder = await nodeIsDirectory(node)
84 if (isFolder && !path.endsWith('/'))
@@ -78,7 +88,7 @@ export const serveGuiAndSharedFiles: Koa.Middleware = async (ctx, next) => {
88 : next()
89 if (!canRead) {
90 ctx.status = cantReadStatusCode(node)
81 - if (ctx.status === FORBIDDEN)
91 + if (ctx.status === HTTP_FORBIDDEN)
92 return
93 const browserDetected = ctx.get('Upgrade-Insecure-Requests') || ctx.get('Sec-Fetch-Mode') // ugh, heuristics
94 if (!browserDetected) // we don't want to trigger basic authentication on browsers, it's meant for download managers only
@@ -108,14 +118,14 @@ export const someSecurity: Koa.Middleware = async (ctx, next) => {
118 if (DEV && proxy && [process.env.FRONTEND_PROXY, process.env.ADMIN_PROXY].includes(ctx.get('X-Forwarded-port')))
119 proxy = ''
120 if (dirTraversal(decodeURI(ctx.path)))
111 - return ctx.status = 418
121 + return ctx.status = HTTP_FOOL
122 if (applyBlock(ctx.socket, ctx.ip))
123 return
124 proxyDetected ||= proxy > ''
125 ctx.state.proxiedFor = proxy
126 }
127 catch {
118 - return ctx.status = 418
128 + return ctx.status = HTTP_FOOL
129 }
130 return next()
131 }
@@ -152,18 +162,18 @@ async function srpCheck(username: string, password: string) {
162
163 // unify get/post parameters, with JSON decoding to not be limited to strings
164 export const paramsDecoder: Koa.Middleware = async (ctx, next) => {
155 - ctx.params = ctx.method === 'POST' ? tryJson(await getReqData(ctx.req))
165 + ctx.params = ctx.method === 'POST' ? tryJson(await stream2string(ctx.req))
166 : objSameKeys(ctx.query, x => Array.isArray(x) ? x : tryJson(x))
167 await next()
168 }
169
160 -async function getReqData(req: IncomingMessage): Promise<any> {
170 +async function stream2string(stream: Readable): Promise<string> {
171 return new Promise((resolve, reject) => {
172 let data = ''
163 - req.on('data', chunk =>
173 + stream.on('data', chunk =>
174 data += chunk)
165 - req.on('error', reject)
166 - req.on('end', () => {
175 + stream.on('error', reject)
176 + stream.on('end', () => {
177 try {
178 resolve(data)
179 }
src/serveFile.ts
+19 -11
@@ -2,7 +2,15 @@
2
3 import Koa from 'koa'
4 import { createReadStream, stat } from 'fs'
5 -import { FORBIDDEN, METHOD_NOT_ALLOWED, NO_CONTENT } from './const'
5 +import {
6 + HTTP_BAD_REQUEST,
7 + HTTP_FORBIDDEN,
8 + HTTP_METHOD_NOT_ALLOWED,
9 + HTTP_NO_CONTENT,
10 + HTTP_NOT_FOUND,
11 + HTTP_NOT_MODIFIED,
12 + HTTP_OK, HTTP_PARTIAL_CONTENT, HTTP_RANGE_NOT_SATISFIABLE
13 +} from './const'
14 import { getNodeName, MIME_AUTO, VfsNode } from './vfs'
15 import mimetypes from 'mime-types'
16 import { defineConfig } from './config'
@@ -24,7 +32,7 @@ export function serveFileNode(node: VfsNode) : Koa.Middleware {
32 const ref = /\/\/([^:/]+)/.exec(ctx.get('referer'))?.[1] // extract host from url
33 if (ref && ref !== host() // automatic accept if referer is basically the hosting domain
34 && !isMatch(ref, allowed))
27 - return ctx.status = FORBIDDEN
35 + return ctx.status = HTTP_FORBIDDEN
36
37 function host() {
38 const s = ctx.get('host')
@@ -50,26 +58,26 @@ export function serveFile(source:string, mime?:string, content?: string | Buffer
58 if (mime)
59 ctx.type = mime
60 if (ctx.method === 'OPTIONS') {
53 - ctx.status = NO_CONTENT
61 + ctx.status = HTTP_NO_CONTENT
62 ctx.set({ Allow: 'OPTIONS, GET, HEAD' })
63 return
64 }
65 if (ctx.method !== 'GET')
58 - return ctx.status = METHOD_NOT_ALLOWED
66 + return ctx.status = HTTP_METHOD_NOT_ALLOWED
67 try {
68 const stats = await promisify(stat)(source) // using fs's function instead of fs/promises, because only the former is supported by pkg
69 ctx.set('Last-Modified', stats.mtime.toUTCString())
70 ctx.fileSource = source
63 - ctx.status = 200
71 + ctx.status = HTTP_OK
72 if (ctx.fresh)
65 - return ctx.status = 304
73 + return ctx.status = HTTP_NOT_MODIFIED
74 if (content !== undefined)
75 return ctx.body = content
76 const range = getRange(ctx, stats.size)
77 ctx.body = createReadStream(source, range)
78 }
79 catch {
72 - return ctx.status = 404
80 + return ctx.status = HTTP_NOT_FOUND
81 }
82 }
83 }
@@ -83,21 +91,21 @@ export function getRange(ctx: Koa.Context, totalSize: number) {
91 }
92 const ranges = range.split('=')[1]
93 if (ranges.includes(','))
86 - return ctx.throw(400, 'multi-range not supported')
94 + return ctx.throw(HTTP_BAD_REQUEST, 'multi-range not supported')
95 let bytes = ranges?.split('-')
96 if (!bytes?.length)
89 - return ctx.throw(400, 'bad range')
97 + return ctx.throw(HTTP_BAD_REQUEST, 'bad range')
98 const max = totalSize - 1
99 const start = bytes[0] ? Number(bytes[0]) : Math.max(0, totalSize-Number(bytes[1])) // a negative start is relative to the end
100 const end = bytes[0] ? Number(bytes[1] || max) : max
101 // we don't support last-bytes without knowing max
102 if (isNaN(end) && isNaN(max) || end > max || start > max) {
95 - ctx.status = 416
103 + ctx.status = HTTP_RANGE_NOT_SATISFIABLE
104 ctx.set('Content-Range', `bytes ${totalSize}`)
105 ctx.body = 'Requested Range Not Satisfiable'
106 return
107 }
100 - ctx.status = 206
108 + ctx.status = HTTP_PARTIAL_CONTENT
109 ctx.set('Content-Range', `bytes ${start}-${isNaN(end) ? '' : end}/${isNaN(totalSize) ? '*' : totalSize}`)
110 ctx.response.length = end - start + 1
111 return { start, end }
src/serveGuiFiles.ts
+4 -4
@@ -2,7 +2,7 @@
2
3 import Koa from 'koa'
4 import fs from 'fs/promises'
5 -import { METHOD_NOT_ALLOWED, NO_CONTENT, PLUGINS_PUB_URI } from './const'
5 +import { HTTP_METHOD_NOT_ALLOWED, HTTP_NO_CONTENT, HTTP_NOT_FOUND, PLUGINS_PUB_URI } from './const'
6 import { serveFile } from './serveFile'
7 import { mapPlugins } from './plugins'
8 import { refresh_session } from './api.auth'
@@ -18,12 +18,12 @@ function serveStatic(uri: string): Koa.Middleware {
18 const cache: Record<string, Promise<string>> = {}
19 return async (ctx, next) => {
20 if(ctx.method === 'OPTIONS') {
21 - ctx.status = NO_CONTENT
21 + ctx.status = HTTP_NO_CONTENT
22 ctx.set({ Allow: 'OPTIONS, GET' })
23 return
24 }
25 if (ctx.method !== 'GET')
26 - return ctx.status = METHOD_NOT_ALLOWED
26 + return ctx.status = HTTP_METHOD_NOT_ALLOWED
27 const serveApp = shouldServeApp(ctx)
28 const fullPath = join(__dirname, '..', DEV_STATIC, folder, serveApp ? '/index.html': ctx.path)
29 const content = await getOrSet(cache, ctx.path, async () => {
@@ -32,7 +32,7 @@ function serveStatic(uri: string): Koa.Middleware {
32 : adjustBundlerLinks(ctx.path, uri, data)
33 })
34 if (content === null)
35 - return ctx.status = 404
35 + return ctx.status = HTTP_NOT_FOUND
36 if (!serveApp)
37 return serveFile(fullPath, 'auto', content)(ctx, next)
38 // we don't cache the index as it's small and may prevent plugins change to apply
src/sse.ts
+2 -1
@@ -2,6 +2,7 @@
2
3 import Koa from 'koa'
4 import { Transform } from 'stream'
5 +import { HTTP_OK } from './const'
6
7 export default function createSSE(ctx: Koa.Context) {
8 const { socket } = ctx.req
@@ -14,7 +15,7 @@ export default function createSSE(ctx: Koa.Context) {
15 'Connection': 'keep-alive',
16 'X-Accel-Buffering': 'no', // avoid buffering when reverse-proxied through nginx
17 })
17 - ctx.status = 200
18 + ctx.status = HTTP_OK
19 return ctx.body = new Transform({
20 objectMode: true,
21 transform(chunk, encoding, cb) {
src/util-http.ts
+2 -1
@@ -1,6 +1,7 @@
1 import { RequestOptions } from 'https'
2 import { IncomingMessage } from 'node:http'
3 import https from 'node:https'
4 +import { HTTP_TEMPORARY_REDIRECT } from './const'
5
6 export function httpsString(url: string, options:RequestOptions={}): Promise<IncomingMessage & { ok: boolean, body: string }> {
7 return httpsStream(url, options).then(res =>
@@ -20,7 +21,7 @@ export function httpsStream(url: string, options:RequestOptions={}): Promise<Inc
21 https.request(url, options, res => {
22 if (!res.statusCode || res.statusCode >= 400)
23 throw res
23 - if (res.statusCode === 302 && res.headers.location)
24 + if (res.statusCode === HTTP_TEMPORARY_REDIRECT && res.headers.location)
25 return resolve(httpsStream(res.headers.location, options))
26 resolve(res)
27 }).on('error', reject).end()
src/vfs.ts
+3 -3
@@ -7,7 +7,7 @@ import { dirStream, dirTraversal, enforceFinal, getOrSet, isDirectory, typedKeys
7 import Koa from 'koa'
8 import _ from 'lodash'
9 import { defineConfig, setConfig } from './config'
10 -import { FORBIDDEN, IS_WINDOWS, UNAUTHORIZED } from './const'
10 +import { HTTP_FOOL, HTTP_FORBIDDEN, IS_WINDOWS, HTTP_UNAUTHORIZED } from './const'
11 import events from './events'
12 import { getCurrentUsernameExpanded } from './perm'
13 import { with_ } from './misc'
@@ -74,7 +74,7 @@ export async function urlToNode(url: string, ctx?: Koa.Context, parent: VfsNode=
74 const rest = nextSlash < 0 ? '' : url.slice(nextSlash+1, url.endsWith('/') ? -1 : undefined)
75 if (dirTraversal(name) || /[\/]/.test(name)) {
76 if (ctx)
77 - ctx.status = 418
77 + ctx.status = HTTP_FOOL
78 return
79 }
80 const parents = parent.parents || [] // don't waste time cloning the array, as we won't keep intermediate nodes
@@ -232,7 +232,7 @@ function matchWho(who: Who, ctx: Koa.Context) {
232 }
233
234 export function cantReadStatusCode(node: VfsNode) {
235 - return node.can_read === false ? FORBIDDEN : UNAUTHORIZED
235 + return node.can_read === false ? HTTP_FORBIDDEN : HTTP_UNAUTHORIZED
236 }
237
238 events.on('accountRenamed', (from, to) => {
src/zip.ts
+2 -1
@@ -9,9 +9,10 @@ import fs from 'fs/promises'
9 import { defineConfig } from './config'
10 import { dirname } from 'path'
11 import { getRange } from './serveFile'
12 +import { HTTP_OK } from './const'
13
14 export async function zipStreamFromFolder(node: VfsNode, ctx: Koa.Context) {
14 - ctx.status = 200
15 + ctx.status = HTTP_OK
16 ctx.mime = 'zip'
17 const name = getNodeName(node)
18 ctx.attachment((name || 'archive') + '.zip')