better code: typing

Massimo Melina committed Dec 14, 2023 at 16:57 UTC fbef1a9771d749ee33c684b8c7f1fb51420945c8
11 files changed +71 -27
src/api.auth.ts
+4 -4
@@ -43,8 +43,8 @@ export const loginSrp1: ApiHandler = async ({ username }, ctx) => {
43 if (!ctx.session)
44 return new ApiError(HTTP_SERVER_ERROR)
45 if (!account || !accountCanLogin(account)) { // TODO simulate fake account to prevent knowing valid usernames
46 - ctx.state.logExtra = { u: username }
47 - ctx.state.dont_log = false // log even if log_api is false
46 + ctx.logExtra({ u: username })
47 + ctx.state.dontLog = false // log even if log_api is false
48 return new ApiError(HTTP_UNAUTHORIZED)
49 }
50 try {
@@ -80,8 +80,8 @@ export const loginSrp2: ApiHandler = async ({ pubKey, proof }, ctx) => {
80 }
81 }
82 catch(e) {
83 - ctx.state.dont_log = false // log even if log_api is false
84 - ctx.state.logExtra = { u: username }
83 + ctx.logExtra({ u: username })
84 + ctx.state.dontLog = false // log even if log_api is false
85 return new ApiError(HTTP_UNAUTHORIZED, String(e))
86 }
87 finally {
src/apiMiddleware.ts
+1 -1
@@ -23,7 +23,7 @@ const logApi = defineConfig('log_api', true)
23 export function apiMiddleware(apis: ApiHandlers) : Koa.Middleware {
24 return async (ctx) => {
25 if (!logApi.get())
26 - ctx.state.dont_log = true
26 + ctx.state.dontLog = true
27 const isPost = ctx.params
28 const params = isPost ? ctx.params || {} : ctx.query
29 const apiName = ctx.path
src/cross.ts
+4 -4
@@ -129,6 +129,10 @@ export function enforceFinal(sub:string, s:string, evenEmpty=false) {
129 return !evenEmpty && !s || s.endsWith(sub) ? s : s+sub
130 }
131
132 +export function removeStarting(sub: string, s: string) {
133 + return s.startsWith(sub) ? s.slice(sub.length) : s
134 +}
135 +
136 export function splitAt(sub: string | number, all: string): [string, string] {
137 if (typeof sub === 'number')
138 return [all.slice(0, sub), all.slice(sub + 1)]
@@ -231,10 +235,6 @@ export function findDefined<I, O>(a: I[] | Record<string, I>, cb:(v:I, k: string
235 }
236 }
237
234 -export function removeStarting(sub: string, s: string) {
235 - return s.startsWith(sub) ? s.slice(sub.length) : s
236 -}
237 -
238 export function newObj<S extends (object | undefined | null),VR=any>(
239 src: S,
240 returnNewValue: (value:Truthy<S[keyof S]>, key: Exclude<keyof S, symbol>, setK:(newK?: string)=>true, depth: number) => any,
src/listen.ts
+1 -2
@@ -85,8 +85,7 @@ const considerHttps = debounceAsync(async () => {
85 defaultBaseUrl.port = getCurrentPort(httpSrv) ?? 0
86 let port = httpsPortCfg.get()
87 try {
88 - while (!app)
89 - await wait(100)
88 + await waitFor(() => app)
89 httpsSrv = Object.assign(
90 https.createServer(port === PORT_DISABLED ? {} : { ...commonOptions, key: httpsOptions.private_key, cert: httpsOptions.cert }, app.callback()),
91 { name: 'https' }
src/log.ts
+27 -8
@@ -9,9 +9,10 @@ import { stat } from 'fs/promises'
9 import _ from 'lodash'
10 import { createFileWithPath, prepareFolder } from './util-files'
11 import { getCurrentUsername } from './auth'
12 -import { DAY, makeNetMatcher, tryJson } from './misc'
12 +import { DAY, makeNetMatcher, tryJson, Dict, Falsy } from './misc'
13 import events from './events'
14 import { getConnection } from './connections'
15 +import { app } from './index'
16
17 class Logger {
18 stream?: Writable
@@ -73,7 +74,7 @@ export const logMw: Koa.Middleware = async (ctx, next) => {
74 // don't await, as we don't want to hold the middlewares chain
75 ctx.state.completed = Promise.race([ once(ctx.res, 'finish'), once(ctx.res, 'close') ])
76 ctx.state.completed.then(() => {
76 - if (ctx.state.dont_log) return
77 + if (ctx.state.dontLog) return
78 if (dontLogNet.compiled()(ctx.ip)) return
79 const isError = ctx.status >= 400
80 const logger = isError && accessErrorLog || accessLogger
@@ -105,14 +106,14 @@ export const logMw: Koa.Middleware = async (ctx, next) => {
106 const user = getCurrentUsername(ctx)
107 const length = ctx.state.length ?? ctx.length
108 const uri = ctx.originalUrl
108 - let extra = ctx.state.includesLastByte && ctx.vfsNode && ctx.res.finished && { dl: 1 }
109 - || ctx.state.uploadPath && { ul: ctx.state.uploadPath, size: ctx.state.uploadSize }
109 + ctx.logExtra(ctx.state.includesLastByte && ctx.vfsNode && ctx.res.finished && { dl: 1 }
110 + || ctx.state.uploadPath && { ul: ctx.state.uploadPath, size: ctx.state.uploadSize })
111 const conn = getConnection(ctx)
112 if (conn?.country)
112 - Object.assign(extra ||= {}, { country: conn.country })
113 - extra = extra ? Object.assign(extra, ctx.state.logExtra) : ctx.state.logExtra
113 + ctx.logExtra({ country: conn.country })
114 if (logUA.get())
115 - extra = Object.assign({ ua: ctx.get('user-agent') }, extra)
115 + ctx.logExtra({ ua: ctx.get('user-agent') })
116 + const extra = ctx.state.logExtra
117 events.emit(logger.name, Object.assign(_.pick(ctx, ['ip', 'method','status']), { length, user, ts: now, uri, extra }))
118 debounce(() => // once in a while we check if the file is still good (not deleted, etc), or we'll reopen it
119 stat(logger.path).catch(() => logger.reopen())) // async = smoother but we may lose some entries
@@ -125,11 +126,29 @@ export const logMw: Koa.Middleware = async (ctx, next) => {
126 ctx.req.httpVersion,
127 ctx.status,
128 length?.toString() ?? '-',
128 - extra ? JSON.stringify(JSON.stringify(extra)) : '',
129 + _.isEmpty(extra) ? '' : JSON.stringify(JSON.stringify(extra)),
130 ))
131 })
132 }
133
134 +declare module "koa" {
135 + interface BaseContext {
136 + logExtra(o: Falsy | Dict<any>): void
137 + }
138 + interface DefaultState {
139 + dontLog?: boolean // don't log this request
140 + logExtra?: object
141 + completed?: Promise<unknown>
142 + }
143 +}
144 +
145 +events.on('app', () => { // wait for app to be set
146 + app.context.logExtra = function(o) { // no => as we need 'this'
147 + if (o)
148 + Object.assign((this as any).state.logExtra ||= {}, o)
149 + }
150 +})
151 +
152 function doubleDigit(n: number) {
153 return n > 9 ? n : '0'+n
154 }
src/middlewares.ts
+5 -5
@@ -192,7 +192,7 @@ export const someSecurity: Koa.Middleware = async (ctx, next) => {
192 // we have some dev-proxies to ignore
193 && !(DEV && [process.env.FRONTEND_PROXY, process.env.ADMIN_PROXY].includes(ctx.get('X-Forwarded-port')))) {
194 proxyDetected = ctx
195 - ctx.state.when = new Date()
195 + ctx.state.whenProxyDetected = new Date()
196 }
197 }
198 catch {
@@ -205,7 +205,7 @@ export const someSecurity: Koa.Middleware = async (ctx, next) => {
205
206 // limited to http proxies
207 export function getProxyDetected() {
208 - if (proxyDetected?.state.when < Date.now() - DAY)
208 + if (proxyDetected?.state.whenProxyDetected < Date.now() - DAY)
209 proxyDetected = undefined
210 return !ignoreProxies.get() && proxyDetected
211 && { from: proxyDetected.ip, for: proxyDetected.get('X-Forwarded-For') }
@@ -251,11 +251,11 @@ declare module "koa" {
251 params: Record<string, any>
252 }
253 interface DefaultState {
254 - account?: Account
254 + account?: Account // user logged in
255 revProxyPath: string
256 connection: Connection
257 - serveApp?: boolean
258 - browsing?: string
257 + serveApp?: boolean // please, serve the frontend app
258 + browsing?: string // for admin/monitoring
259 }
260 }
261 export const paramsDecoder: Koa.Middleware = async (ctx, next) => {
src/serveFile.ts
+6
@@ -119,3 +119,9 @@ export function getRange(ctx: Koa.Context, totalSize: number) {
119 ctx.response.length = end - start + 1
120 return { start, end }
121 }
122 +
123 +declare module "koa" {
124 + interface DefaultState {
125 + includesLastByte?: boolean
126 + }
127 +}
\ No newline at end of file
src/serveGuiFiles.ts
+2 -2
@@ -29,7 +29,7 @@ function serveStatic(uri: string): Koa.Middleware {
29 subscribe(customHtmlState, () => cache = {}) // reset cache at every change
30 return async (ctx) => {
31 if (!logGui.get())
32 - ctx.state.dont_log = true
32 + ctx.state.dontLog = true
33 if(ctx.method === 'OPTIONS') {
34 ctx.status = HTTP_NO_CONTENT
35 ctx.set({ Allow: 'OPTIONS, GET' })
@@ -149,7 +149,7 @@ function serveProxied(port: string | undefined, uri: string) { // used for devel
149 }) )
150 return function (ctx, next) {
151 if (!logGui.get())
152 - ctx.state.dont_log = true
152 + ctx.state.dontLog = true
153 return proxy(ctx, next)
154 } as Koa.Middleware
155 }
src/throttler.ts
+8 -1
@@ -39,7 +39,7 @@ export const throttler: Koa.Middleware = async (ctx, next) => {
39 group.updateLimit(v))
40 return { group, count:0, destroy: unsub }
41 })
42 - const conn = ctx.state.connection as Connection | undefined
42 + const conn = ctx.state.connection
43 if (!conn) throw 'assert throttler connection'
44
45 const ts = conn[SymThrStr] = new ThrottledStream(ipGroup.group, conn[SymThrStr])
@@ -85,6 +85,13 @@ export const throttler: Koa.Middleware = async (ctx, next) => {
85 ctx.state.length = ts.getBytesSent() - offset)
86 }
87
88 +declare module "koa" {
89 + interface DefaultState {
90 + length?: number
91 + originalStream?: Parameters<Koa.Middleware>[0]['body']
92 + }
93 +}
94 +
95 export function roundSpeed(n: number) {
96 return _.round(n, 1) || _.round(n, 3) // further precision if necessary
97 }
src/upload.ts
+7
@@ -159,3 +159,10 @@ export function uploadWriter(base: VfsNode, path: string, ctx: Koa.Context) {
159 notifyClient(ctx, 'upload.status', { [path]: ctx.status }) // allow browsers to detect failure while still sending body
160 }
161 }
162 +
163 +declare module "koa" {
164 + interface DefaultState {
165 + uploadSize?: number
166 + uploadPath?: string
167 + }
168 +}
\ No newline at end of file
src/zip.ts
+6
@@ -78,3 +78,9 @@ export async function zipStreamFromFolder(node: VfsNode, ctx: Koa.Context) {
78 }
79
80 const zipSeconds = defineConfig('zip_calculate_size_for_seconds', 1)
81 +
82 +declare module "koa" {
83 + interface DefaultState {
84 + archive?: string
85 + }
86 +}
\ No newline at end of file