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')