better code: split files

Massimo Melina committed Jan 7, 2022 at 21:29 UTC 27cdc73a58d1c967c852c703d72cbc1997e98b01
6 files changed +193 -186
package.json
+1
@@ -17,6 +17,7 @@
17 "build-prune": "cd dist/node_modules/yaml/dist && mv doc do_c && cd ../.. && mkdir lodash2 && cp lodash/lodash.min.js lodash2/lodash.js && cp lodash/package.json lodash2 && rm -rf lodash && mv lodash2 lodash && cd .. && nm-prune --force && cd node_modules/yaml/dist && mv do_c doc",
18 "build-all": "rm -rf dist && npm run build && cd frontend && npm install && npm run build && echo COMPLETED",
19 "test": "mocha -r ts-node/register 'tests/**/*.ts'",
20 + "zip-dist": "zip hfs.zip -r *.yaml READ* dist/*",
21 "make-exe-after-build": "pkg . -C brotli"
22 },
23 "bin": "dist/index.js",
src/api.auth.ts new
+99
@@ -0,0 +1,99 @@
1 +import { getAccount, getCurrentUsername, saveSrpInfo, updateAccount } from './perm'
2 +import { verifyPassword } from './crypt'
3 +import { CFG_ALLOW_CLEAR_TEXT_LOGIN, getConfig } from './config'
4 +import { ApiHandler } from './apis'
5 +import { SRPParameters, SRPRoutines, SRPServerSession, SRPServerSessionStep1 } from 'tssrp6a'
6 +import { SESSION_DURATION } from './index'
7 +
8 +const srp6aNimbusRoutines = new SRPRoutines(new SRPParameters())
9 +const srpSession = new SRPServerSession(srp6aNimbusRoutines)
10 +const ongoingLogins:Record<string,SRPServerSessionStep1> = {}
11 +
12 +function makeExp() {
13 + return { exp: new Date(Date.now() + SESSION_DURATION) }
14 +}
15 +
16 +export const login: ApiHandler = async ({ user, password }, ctx) => {
17 + if (!user)
18 + return ctx.status = 400
19 + if (!password)
20 + return ctx.status = 400
21 + const acc = getAccount(user)
22 + if (!acc)
23 + return ctx.status = 401
24 + if (!acc.hashedPassword)
25 + return ctx.status = 406
26 + if (!await verifyPassword(acc.hashedPassword, password))
27 + return ctx.status = 401
28 + if (ctx.session)
29 + ctx.session.user = user
30 + return makeExp()
31 +}
32 +
33 +export const loginSrp1: ApiHandler = async ({ user }, ctx) => {
34 + const account = getAccount(user)
35 + if (!ctx.session)
36 + return ctx.throw(500)
37 + if (!account) // TODO simulate fake account to prevent knowing valid usernames
38 + return ctx.status = 401
39 + if (!account.srp)
40 + return ctx.status = 406 // unacceptable
41 +
42 + const [salt, verifier] = account.srp.split('|')
43 + const step1 = await srpSession.step1(account.user, BigInt(salt), BigInt(verifier))
44 + const sid = Math.random()
45 + ongoingLogins[sid] = step1
46 + setTimeout(()=> delete ongoingLogins[sid], 60_000)
47 +
48 + ctx.session.login = { user, sid }
49 + return { salt, pubKey: String(step1.B) } // cast to string cause bigint can't be jsonized
50 +}
51 +
52 +export const loginSrp2: ApiHandler = async ({ pubKey, proof }, ctx) => {
53 + if (!ctx.session)
54 + return ctx.throw(500)
55 + const { user, sid } = ctx.session.login
56 + const step1 = ongoingLogins[sid]
57 + try {
58 + const M2 = await step1.step2(BigInt(pubKey), BigInt(proof))
59 + ctx.session.user = user
60 + return { proof: String(M2), ...makeExp() }
61 + }
62 + catch(e) {
63 + ctx.body = String(e)
64 + ctx.status = 401
65 + }
66 +}
67 +
68 +export const logout: ApiHandler = async ({}, ctx) => {
69 + if (ctx.session)
70 + ctx.session.user = undefined
71 + ctx.status = 200
72 + return true
73 +}
74 +
75 +export const refresh_session: ApiHandler = async ({}, ctx) => {
76 + return { user: ctx.session?.user, ...makeExp() }
77 +}
78 +
79 +export const change_password: ApiHandler = async ({ newPassword }, ctx) => {
80 + if (!newPassword) // clear text version
81 + return Error('missing parameters')
82 + await updateAccount(await getCurrentUsername(ctx), account => {
83 + account.password = newPassword
84 + })
85 + return true
86 +}
87 +
88 +export const change_srp: ApiHandler = async ({ salt, verifier }, ctx) => {
89 + if (getConfig(CFG_ALLOW_CLEAR_TEXT_LOGIN))
90 + return ctx.status = 406
91 + if (!salt || !verifier)
92 + return Error('missing parameters')
93 + await updateAccount(await getCurrentUsername(ctx), account => {
94 + saveSrpInfo(account, salt, verifier)
95 + delete account.hashedPassword // remove leftovers
96 + })
97 + return true
98 +}
99 +
src/api.file_list.ts new
+88
@@ -0,0 +1,88 @@
1 +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'
6 +import { stat } from 'fs/promises'
7 +
8 +export const file_list:ApiHandler = async ({ path, offset, limit, search, omit, sse }, ctx) => {
9 + let node = await vfs.urlToNode(path || '/', ctx)
10 + if (!node)
11 + return
12 + if (search?.includes('..'))
13 + return ctx.throw(400)
14 + if (node.default)
15 + return { redirect: path }
16 + offset = Number(offset)
17 + limit = Number(limit)
18 + const re = new RegExp(_.escapeRegExp(search),'i')
19 + const match = (s?:string) => !s || !search || re.test(s)
20 + const walker = walkNode(node, ctx, search ? Infinity : 0)
21 + const sseSrv = sse ? createSSE(ctx) : null
22 + const res = produceEntries()
23 + return !sseSrv && { list: await res }
24 +
25 + async function produceEntries() {
26 + const list = []
27 + const h = sseSrv && setInterval(()=> console.debug('walking'), 500)
28 + for await (const sub of walker) {
29 + if (sseSrv?.stopped || ctx.aborted) break
30 + const filename = basename(sub.name||'')
31 + if (!match(filename))
32 + continue
33 + const entry = await nodeToDirEntry(sub)
34 + if (!entry)
35 + continue
36 + if (offset) {
37 + --offset
38 + continue
39 + }
40 + if (omit) {
41 + if (omit !== 'c')
42 + ctx.throw(400, 'omit')
43 + if (!entry.m)
44 + entry.m = entry.c
45 + delete entry.c
46 + }
47 + if (sseSrv)
48 + sseSrv.send({ entry })
49 + else
50 + list.push(entry)
51 + if (limit && !--limit)
52 + break
53 + }
54 + if (h) clearInterval(h)
55 + sseSrv?.close()
56 + return list
57 + }
58 +}
59 +
60 +interface DirEntry { n:string, s?:number, m?:Date, c?:Date }
61 +
62 +async function nodeToDirEntry(node: VfsNode): Promise<DirEntry | null> {
63 + try {
64 + let { name, source, default:def } = node
65 + if (source?.includes('//'))
66 + return { n: name || source }
67 + if (source) {
68 + if (!name)
69 + name = basename(source)
70 + if (def)
71 + return { n: name }
72 + const st = await stat(source)
73 + const folder = st.isDirectory()
74 + const { ctime, mtime } = st
75 + return {
76 + n: name + (folder ? '/' : ''),
77 + c: ctime,
78 + m: Math.abs(+mtime-+ctime) < 1000 ? undefined : mtime,
79 + s: folder ? undefined : st.size,
80 + }
81 + }
82 + return name ? { n: name + '/' } : null
83 + }
84 + catch (err:any) {
85 + console.error(String(err))
86 + return null
87 + }
88 +}
src/apis.ts
+1 -1
@@ -1,6 +1,6 @@
1 import Koa from 'koa'
2
3 -type ApiHandler = (params:any, ctx:Koa.Context) => any
3 +export type ApiHandler = (params:any, ctx:Koa.Context) => any
4 export type ApiHandlers = Record<string, ApiHandler>
5
6 export function apiMiddleware(apis: ApiHandlers) : Koa.Middleware {
src/frontEndApis.ts
+4 -179
@@ -1,156 +1,14 @@
1 -import { vfs, VfsNode, walkNode } from './vfs'
2 -import _ from 'lodash'
3 -import createSSE from './sse'
4 -import { basename } from 'path'
5 -import { getAccount, getCurrentUsername, saveSrpInfo, updateAccount } from './perm'
6 -import { stat } from 'fs/promises'
1 import { ApiHandlers } from './apis'
2 import { plugins } from './plugins'
3 import { PLUGINS_PUB_URI } from './const'
10 -import { SRPParameters, SRPRoutines, SRPServerSession, SRPServerSessionStep1 } from 'tssrp6a'
11 -import { SESSION_DURATION } from './index'
12 -import { verifyPassword } from './crypt'
13 -import { CFG_ALLOW_CLEAR_TEXT_LOGIN, getConfig } from './config'
14 -
15 -const srp6aNimbusRoutines = new SRPRoutines(new SRPParameters())
16 -const srpSession = new SRPServerSession(srp6aNimbusRoutines)
17 -const ongoingLogins:Record<string,SRPServerSessionStep1> = {}
4 +import * as api_file_list from './api.file_list'
5 +import * as api_auth from './api.auth'
6
7 export const frontEndApis: ApiHandlers = {
8
21 - async file_list({ path, offset, limit, search, omit, sse }, ctx) {
22 - let node = await vfs.urlToNode(path || '/', ctx)
23 - if (!node)
24 - return
25 - if (search?.includes('..'))
26 - return ctx.throw(400)
27 - if (node.default)
28 - return { redirect: path }
29 - offset = Number(offset)
30 - limit = Number(limit)
31 - const re = new RegExp(_.escapeRegExp(search),'i')
32 - const match = (s?:string) => !s || !search || re.test(s)
33 - const walker = walkNode(node, ctx, search ? Infinity : 0)
34 - const sseSrv = sse ? createSSE(ctx) : null
35 - const res = produceEntries()
36 - return !sseSrv && { list: await res }
37 -
38 - async function produceEntries() {
39 - const list = []
40 - for await (const sub of walker) {
41 - if (sseSrv?.stopped || ctx.aborted) break
42 - const filename = basename(sub.name||'')
43 - if (!match(filename))
44 - continue
45 - const entry = await nodeToDirEntry(sub)
46 - if (!entry)
47 - continue
48 - if (offset) {
49 - --offset
50 - continue
51 - }
52 - if (omit) {
53 - if (omit !== 'c')
54 - ctx.throw(400, 'omit')
55 - if (!entry.m)
56 - entry.m = entry.c
57 - delete entry.c
58 - }
59 - if (sseSrv)
60 - sseSrv.send({ entry })
61 - else
62 - list.push(entry)
63 - if (limit && !--limit)
64 - break
65 - }
66 - sseSrv?.close()
67 - return list
68 - }
69 - },
70 -
71 - async login({ user, password }, ctx) {
72 - if (!user)
73 - return ctx.status = 400
74 - if (!password)
75 - return ctx.status = 400
76 - const acc = getAccount(user)
77 - if (!acc)
78 - return ctx.status = 401
79 - if (!acc.hashedPassword)
80 - return ctx.status = 406
81 - if (!await verifyPassword(acc.hashedPassword, password))
82 - return ctx.status = 401
83 - if (ctx.session)
84 - ctx.session.user = user
85 - return makeExp()
86 - },
87 -
88 - async loginSrp1({ user }, ctx) {
89 - const account = getAccount(user)
90 - if (!ctx.session)
91 - return ctx.throw(500)
92 - if (!account) // TODO simulate fake account to prevent knowing valid usernames
93 - return ctx.status = 401
94 - if (!account.srp)
95 - return ctx.status = 406 // unacceptable
96 -
97 - const [salt, verifier] = account.srp.split('|')
98 - const step1 = await srpSession.step1(account.user, BigInt(salt), BigInt(verifier))
99 - const sid = Math.random()
100 - ongoingLogins[sid] = step1
101 - setTimeout(()=> delete ongoingLogins[sid], 60_000)
102 -
103 - ctx.session.login = { user, sid }
104 - return { salt, pubKey: String(step1.B) } // cast to string cause bigint can't be jsonized
105 - },
106 -
107 - async loginSrp2({ pubKey, proof }, ctx) {
108 - if (!ctx.session)
109 - return ctx.throw(500)
110 - const { user, sid } = ctx.session.login
111 - const step1 = ongoingLogins[sid]
112 - try {
113 - const M2 = await step1.step2(BigInt(pubKey), BigInt(proof))
114 - ctx.session.user = user
115 - return { proof: String(M2), ...makeExp() }
116 - }
117 - catch(e) {
118 - ctx.body = String(e)
119 - ctx.status = 401
120 - }
121 - },
122 -
123 - async logout({}, ctx) {
124 - if (ctx.session)
125 - ctx.session.user = undefined
126 - ctx.status = 200
127 - return true
128 - },
129 -
130 - async refresh_session({}, ctx) {
131 - return { user: ctx.session?.user, ...makeExp() }
132 - },
9 + ...api_file_list,
10
134 - async change_password({ newPassword }, ctx) {
135 - if (!newPassword) // clear text version
136 - return Error('missing parameters')
137 - await updateAccount(await getCurrentUsername(ctx), account => {
138 - account.password = newPassword
139 - })
140 - return true
141 - },
142 -
143 - async change_srp({ salt, verifier }, ctx) {
144 - if (getConfig(CFG_ALLOW_CLEAR_TEXT_LOGIN))
145 - return ctx.status = 406
146 - if (!salt || !verifier)
147 - return Error('missing parameters')
148 - await updateAccount(await getCurrentUsername(ctx), account => {
149 - saveSrpInfo(account, salt, verifier)
150 - delete account.hashedPassword // remove leftovers
151 - })
152 - return true
153 - },
11 + ...api_auth,
12
13 async extras_to_load() {
14 const css = []
@@ -159,38 +17,5 @@ export const frontEndApis: ApiHandlers = {
17 css.push( ...plug.frontend_css.map(f => PLUGINS_PUB_URI + k + '/' + f) )
18 return { css }
19 },
162 -}
163 -
164 -interface DirEntry { n:string, s?:number, m?:Date, c?:Date }
165 -
166 -async function nodeToDirEntry(node: VfsNode): Promise<DirEntry | null> {
167 - try {
168 - let { name, source, default:def } = node
169 - if (source?.includes('//'))
170 - return { n: name || source }
171 - if (source) {
172 - if (!name)
173 - name = basename(source)
174 - if (def)
175 - return { n: name }
176 - const st = await stat(source)
177 - const folder = st.isDirectory()
178 - const { ctime, mtime } = st
179 - return {
180 - n: name + (folder ? '/' : ''),
181 - c: ctime,
182 - m: Math.abs(+mtime-+ctime) < 1000 ? undefined : mtime,
183 - s: folder ? undefined : st.size,
184 - }
185 - }
186 - return name ? { n: name + '/' } : null
187 - }
188 - catch (err:any) {
189 - console.error(String(err))
190 - return null
191 - }
192 -}
20
194 -function makeExp() {
195 - return { exp: new Date(Date.now() + SESSION_DURATION) }
21 }
todo.md
-6
@@ -21,9 +21,3 @@
21 - log: ip2name
22 - apis in separated log file with parameters?
23 - errors in separated log file
24 -- login without passing clear text password?
25 - we could use asymmetric encryption, possibly on a hashed password, that means
26 - we should store a hash2(hash1(password+salt1)), where hash1 is applied on both client
27 - and server, which grants that even if the encryption is broken only the salt-hashed
28 - is revealed, which compromises only this server and not others.
29 - http://qnimate.com/asymmetric-encryption-using-web-cryptography-api/