better code: expect ApiError from ApiHandlers

Massimo Melina committed Jan 10, 2022 at 14:30 UTC 2a89d184574bc5d2526423069974fd7367317a05
4 files changed +30 -23
src/api.auth.ts
+13 -14
@@ -1,7 +1,7 @@
1 import { getAccount, saveSrpInfo, updateAccount } from './perm'
2 import { verifyPassword } from './crypt'
3 import { CFG_ALLOW_CLEAR_TEXT_LOGIN, getConfig } from './config'
4 -import { ApiHandler } from './apis'
4 +import { ApiError, ApiHandler } from './apis'
5 import { SRPParameters, SRPRoutines, SRPServerSession, SRPServerSessionStep1 } from 'tssrp6a'
6 import { SESSION_DURATION } from './index'
7
@@ -15,17 +15,17 @@ function makeExp() {
15
16 export const login: ApiHandler = async ({ username, password }, ctx) => {
17 if (!username)
18 - return ctx.status = 400
18 + return new ApiError(400)
19 if (!password)
20 - return ctx.status = 400
20 + return new ApiError(400)
21 username = username.toLocaleLowerCase()
22 const acc = getAccount(username)
23 if (!acc)
24 - return ctx.status = 401
24 + return new ApiError(401)
25 if (!acc.hashed_password)
26 - return ctx.status = 406
26 + return new ApiError(406)
27 if (!await verifyPassword(acc.hashed_password, password))
28 - return ctx.status = 401
28 + return new ApiError(401)
29 if (ctx.session)
30 ctx.session.username = username
31 return makeExp()
@@ -33,15 +33,15 @@ export const login: ApiHandler = async ({ username, password }, ctx) => {
33
34 export const loginSrp1: ApiHandler = async ({ username }, ctx) => {
35 if (!username)
36 - return ctx.status = 400
36 + return new ApiError(400)
37 username = username.toLocaleLowerCase()
38 const account = getAccount(username)
39 if (!ctx.session)
40 return ctx.throw(500)
41 if (!account) // TODO simulate fake account to prevent knowing valid usernames
42 - return ctx.status = 401
42 + return new ApiError(401)
43 if (!account.srp)
44 - return ctx.status = 406 // unacceptable
44 + return new ApiError(406) // unacceptable
45
46 const [salt, verifier] = account.srp.split('|')
47 const step1 = await srpSession.step1(account.username, BigInt(salt), BigInt(verifier))
@@ -64,8 +64,7 @@ export const loginSrp2: ApiHandler = async ({ pubKey, proof }, ctx) => {
64 return { proof: String(M2), ...makeExp() }
65 }
66 catch(e) {
67 - ctx.body = String(e)
68 - ctx.status = 401
67 + return new ApiError(401, String(e))
68 }
69 }
70
@@ -84,7 +83,7 @@ export const change_password: ApiHandler = async ({ newPassword }, ctx) => {
83 if (!newPassword) // clear text version
84 return Error('missing parameters')
85 if (!ctx.account)
87 - return ctx.status = 401
86 + return new ApiError(401)
87 await updateAccount(ctx.account, account => {
88 account.password = newPassword
89 })
@@ -93,11 +92,11 @@ export const change_password: ApiHandler = async ({ newPassword }, ctx) => {
92
93 export const change_srp: ApiHandler = async ({ salt, verifier }, ctx) => {
94 if (getConfig(CFG_ALLOW_CLEAR_TEXT_LOGIN))
96 - return ctx.status = 406
95 + return new ApiError(406)
96 if (!salt || !verifier)
97 return Error('missing parameters')
98 if (!ctx.account)
100 - return ctx.status = 401
99 + return new ApiError(401)
100 await updateAccount(ctx.account, account => {
101 saveSrpInfo(account, salt, verifier)
102 delete account.hashed_password // remove leftovers
src/api.file_list.ts
+2 -2
@@ -2,7 +2,7 @@ import { vfs, VfsNode, walkNode } from './vfs'
2 import _ from 'lodash'
3 import createSSE from './sse'
4 import { basename } from 'path'
5 -import { ApiHandler } from './apis'
5 +import { ApiError, ApiHandler } from './apis'
6 import { stat } from 'fs/promises'
7
8 export const file_list:ApiHandler = async ({ path, offset, limit, search, omit, sse }, ctx) => {
@@ -10,7 +10,7 @@ export const file_list:ApiHandler = async ({ path, offset, limit, search, omit,
10 if (!node)
11 return
12 if (search?.includes('..'))
13 - return ctx.throw(400)
13 + return new ApiError(400)
14 if (node.default)
15 return { redirect: path }
16 offset = Number(offset)
src/apis.ts
+13 -6
@@ -1,29 +1,36 @@
1 import Koa from 'koa'
2
3 -export type ApiHandler = (params:any, ctx:Koa.Context) => any
3 +export class ApiError extends Error {
4 + constructor(public status:number, message?:string | Error) {
5 + super(typeof message === 'string' ? message : message?.message)
6 + }
7 +}
8 +type ApiHandlerResult = Record<string,any> | ApiError
9 +export type ApiHandler = (params:any, ctx:Koa.Context) => ApiHandlerResult | Promise<ApiHandlerResult>
10 export type ApiHandlers = Record<string, ApiHandler>
11
12 export function apiMiddleware(apis: ApiHandlers) : Koa.Middleware {
13 return async (ctx, next) => {
14 const params = ctx.method === 'POST' ? ctx.request.body : ctx.request.query
15 console.debug('API', ctx.method, ctx.path, { ...params })
10 - if (!(ctx.path in apis)) {
16 + if (!apis.hasOwnProperty(ctx.path)) {
17 ctx.body = 'invalid api'
18 return ctx.status = 404
19 }
14 - const cb = (apis as any)[ctx.path]
20 let res
21 try {
17 - res = await cb(params||{}, ctx)
22 + res = await apis[ctx.path](params || {}, ctx)
23 }
24 catch(e) {
25 ctx.throw(500, String(e))
26 }
27 if (res)
23 - if (res instanceof Error)
28 + if (res instanceof ApiError)
29 + ctx.throw(res.status, res.message)
30 + else if (res instanceof Error)
31 ctx.throw(400, res)
32 else
26 - ctx.body = res === true ? {} : res
33 + ctx.body = res
34 await next()
35 }
36 }
todo.md
+2 -1
@@ -4,6 +4,7 @@
4 - update tests to SRP login
5 - anti-csrf
6 - upload
7 +- https
8 - updater (stop,unzip,start)
9 - search and login dialogs should push to history so that mobile can use back button to close them
10 - node.comment
@@ -11,10 +12,10 @@
12 - account.redirect
13 - config: bans
14 - config: min disk space
15 +- thumbnails support
16 - link to parent folder in the list (as an option of the frontend? plugin?)
17 - archive for search results
18 - archive only selected files
17 -- https
19 - webdav?
20 - vfs: ability to remove/hide/rename files deep in a source
21 - administration interface