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/