better code: split files
Massimo Melina committed
Oct 29, 2023 at 19:18 UTC
59acc36da896d03a23dc06db27825a81fd26cb17
9 files changed
+76
-81
admin/src/LoginRequired.ts
+2
-2
@@ -5,7 +5,7 @@ import { createElement as h, Fragment, useEffect, useRef, useState } from 'react
5
import { Center, getHFS, makeSessionRefresher } from './misc'
6
import { Form } from '@hfs/mui-grid-form'
7
import { apiCall } from './api'
8
-import { srpSequence } from '@hfs/shared'
8
+import { srpClientSequence } from '@hfs/shared'
9
import { Alert, Box } from '@mui/material'
10
11
export function LoginRequired({ children }: any) {
@@ -57,7 +57,7 @@ function LoginForm() {
57
}
58
59
async function login(username: string, password: string) {
60
- const res = await srpSequence(username, password, apiCall).catch(err => {
60
+ const res = await srpClientSequence(username, password, apiCall).catch(err => {
61
throw err?.code === 401 ? "Wrong username or password"
62
: err === 'trust' ? "Login aborted: server identity cannot be trusted"
63
: err?.name === 'AbortError' ? "Server didn't respond"
frontend/src/login.ts
+2
-2
@@ -3,7 +3,7 @@
3
import { apiCall } from '@hfs/shared/api'
4
import { state, useSnapState } from './state'
5
import { alertDialog, newDialog } from './dialog'
6
-import { getHFS, getPrefixUrl, hIcon, makeSessionRefresher, srpSequence, working } from './misc'
6
+import { getHFS, getPrefixUrl, hIcon, makeSessionRefresher, srpClientSequence, working } from './misc'
7
import { useNavigate } from 'react-router-dom'
8
import { createElement as h, Fragment, useEffect, useRef } from 'react'
9
import { t, useI18N } from './i18n'
@@ -12,7 +12,7 @@ import { CustomCode } from './components'
12
13
async function login(username:string, password:string) {
14
const stopWorking = working()
15
- return srpSequence(username, password, apiCall).then(res => {
15
+ return srpClientSequence(username, password, apiCall).then(res => {
16
stopWorking()
17
sessionRefresher(res)
18
state.loginRequired = false
shared/index.ts
+1
-1
@@ -4,7 +4,7 @@ import _ from 'lodash'
4
import { apiCall } from './api'
5
export * from './react'
6
export * from './dialogs'
7
-export * from './srp'
7
+export * from '../src/srp'
8
export * from '../src/cross'
9
10
(window as any)._ = _
shared/srp.ts
deleted
-16
@@ -1,16 +0,0 @@
1
-// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
-
3
-import { SRPClientSession, SRPParameters, SRPRoutines } from 'tssrp6a'
4
-
5
-export async function srpSequence(username:string, password:string, apiCall: (cmd:string, params:any) => any) {
6
- const { pubKey, salt } = await apiCall('loginSrp1', { username })
7
- if (!salt) throw Error('salt')
8
- const srp6aNimbusRoutines = new SRPRoutines(new SRPParameters())
9
- const srp = new SRPClientSession(srp6aNimbusRoutines);
10
- const resStep1 = await srp.step1(username, password)
11
- const resStep2 = await resStep1.step2(BigInt(salt), BigInt(pubKey))
12
- const res = await apiCall('loginSrp2', { pubKey: String(resStep2.A), proof: String(resStep2.M1) }) // bigint-s must be cast to string to be json-ed
13
- await resStep2.step3(BigInt(res.proof)).catch(() => Promise.reject('trust'))
14
- return res
15
-}
16
-
src/api.auth.ts
+6
-33
@@ -1,36 +1,20 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
-import { Account, accountCanLogin, getAccount, getCurrentUsername, getFromAccount, normalizeUsername } from './perm'
3
+import { Account, accountCanLogin, getAccount, getCurrentUsername, getFromAccount } from './perm'
4
import { verifyPassword } from './crypt'
5
import { ApiError, ApiHandler } from './apiMiddleware'
6
-import { SRPParameters, SRPRoutines, SRPServerSession, SRPServerSessionStep1 } from 'tssrp6a'
7
-import {
8
- ADMIN_URI,
9
- HTTP_UNAUTHORIZED, HTTP_BAD_REQUEST, HTTP_SERVER_ERROR, HTTP_NOT_ACCEPTABLE, HTTP_CONFLICT, HTTP_NOT_FOUND
10
-} from './const'
11
-import Koa from 'koa'
6
+import { SRPServerSessionStep1 } from 'tssrp6a'
7
+import { ADMIN_URI, HTTP_UNAUTHORIZED, HTTP_BAD_REQUEST, HTTP_SERVER_ERROR, HTTP_NOT_ACCEPTABLE, HTTP_CONFLICT,
8
+ HTTP_NOT_FOUND } from './const'
9
import { changeSrpHelper, changePasswordHelper } from './api.helpers'
10
import { ctxAdminAccess } from './adminApis'
14
-import { prepareState, sessionDuration } from './middlewares'
11
+import { sessionDuration } from './middlewares'
12
+import { loggedIn, srpStep1 } from './auth'
13
import { defineConfig } from './config'
14
17
-const srp6aNimbusRoutines = new SRPRoutines(new SRPParameters())
15
const ongoingLogins:Record<string,SRPServerSessionStep1> = {} // store data that doesn't fit session object
16
const keepSessionAlive = defineConfig('keep_session_alive', true)
17
21
-// centralized log-in state
22
-async function loggedIn(ctx:Koa.Context, username: string | false) {
23
- const s = ctx.session
24
- if (!s)
25
- return ctx.throw(HTTP_SERVER_ERROR,'session')
26
- if (username === false) {
27
- delete s.username
28
- return
29
- }
30
- s.username = normalizeUsername(username)
31
- await prepareState(ctx, async ()=>{}) // updating the state is necessary to send complete session data so that frontend shows admin button
32
-}
33
-
18
function makeExp() {
19
return !keepSessionAlive.get() ? undefined
20
: { exp: new Date(Date.now() + sessionDuration.compiled()) }
@@ -73,17 +57,6 @@ export const loginSrp1: ApiHandler = async ({ username }, ctx) => {
57
}
58
}
59
76
-export async function srpStep1(account: Account) {
77
- if (!account.srp)
78
- throw HTTP_NOT_ACCEPTABLE
79
- const [salt, verifier] = account.srp.split('|')
80
- if (!salt || !verifier)
81
- throw Error("malformed account")
82
- const srpSession = new SRPServerSession(srp6aNimbusRoutines)
83
- const step1 = await srpSession.step1(account.username, BigInt(salt), BigInt(verifier))
84
- return { step1, salt, pubKey: String(step1.B) } // cast to string cause bigint can't be jsonized
85
-}
86
-
60
export const loginSrp2: ApiHandler = async ({ pubKey, proof }, ctx) => {
61
if (!ctx.session)
62
return new ApiError(HTTP_SERVER_ERROR)
src/auth.ts
new
+40
@@ -0,0 +1,40 @@
1
+import { Account, getAccount, normalizeUsername } from './perm'
2
+import { HTTP_NOT_ACCEPTABLE, HTTP_SERVER_ERROR } from './cross-const'
3
+import { SRPParameters, SRPRoutines, SRPServerSession } from 'tssrp6a'
4
+import { Context } from 'koa'
5
+import { prepareState } from './middlewares'
6
+import { srpClientPart } from './srp'
7
+
8
+const srp6aNimbusRoutines = new SRPRoutines(new SRPParameters())
9
+
10
+export async function srpStep1(account: Account) {
11
+ if (!account.srp)
12
+ throw HTTP_NOT_ACCEPTABLE
13
+ const [salt, verifier] = account.srp.split('|')
14
+ if (!salt || !verifier)
15
+ throw Error("malformed account")
16
+ const srpSession = new SRPServerSession(srp6aNimbusRoutines)
17
+ const step1 = await srpSession.step1(account.username, BigInt(salt), BigInt(verifier))
18
+ return { step1, salt, pubKey: String(step1.B) } // cast to string cause bigint can't be jsonized
19
+}
20
+
21
+export async function srpCheck(username: string, password: string) {
22
+ const account = getAccount(username)
23
+ if (!account?.srp || !password) return false
24
+ const { step1, salt, pubKey } = await srpStep1(account)
25
+ const client = await srpClientPart(username, password, salt, pubKey)
26
+ return await step1.step2(client.A, client.M1).then(() => true, () => false)
27
+}
28
+
29
+// centralized log-in state
30
+export async function loggedIn(ctx: Context, username: string | false) {
31
+ const s = ctx.session
32
+ if (!s)
33
+ return ctx.throw(HTTP_SERVER_ERROR,'session')
34
+ if (username === false) {
35
+ delete s.username
36
+ return
37
+ }
38
+ s.username = normalizeUsername(username)
39
+ await prepareState(ctx, async ()=>{}) // updating the state is necessary to send complete session data so that frontend shows admin button
40
+}
src/middlewares.ts
+4
-25
@@ -2,21 +2,12 @@
2
3
import compress from 'koa-compress'
4
import Koa from 'koa'
5
-import {
6
- ADMIN_URI, API_URI, BUILD_TIMESTAMP, DEV,
5
+import { ADMIN_URI, API_URI, BUILD_TIMESTAMP, DEV,
6
HTTP_FORBIDDEN, HTTP_NOT_FOUND, HTTP_FOOL, HTTP_UNAUTHORIZED, HTTP_BAD_REQUEST, HTTP_METHOD_NOT_ALLOWED,
7
} from './const'
8
import { FRONTEND_URI } from './const'
9
import { statusCodeForMissingPerm, nodeIsDirectory, urlToNode, vfs, walkNode, VfsNode, getNodeName } from './vfs'
11
-import {
12
- DAY,
13
- asyncGeneratorToReadable,
14
- dirTraversal,
15
- filterMapGenerator,
16
- isLocalHost,
17
- stream2string,
18
- tryJson, Dict
19
-} from './misc'
10
+import { DAY, asyncGeneratorToReadable, dirTraversal, filterMapGenerator, isLocalHost, stream2string, tryJson } from './misc'
11
import { zipStreamFromFolder } from './zip'
12
import { serveFile, serveFileNode } from './serveFile'
13
import { serveGuiFiles } from './serveGuiFiles'
@@ -26,8 +17,7 @@ import { applyBlock } from './block'
17
import { accountCanLogin, getAccount } from './perm'
18
import { socket2connection, updateConnection, normalizeIp } from './connections'
19
import basicAuth from 'basic-auth'
29
-import { SRPClientSession, SRPParameters, SRPRoutines } from 'tssrp6a'
30
-import { srpStep1 } from './api.auth'
20
+import { loggedIn, srpCheck } from './auth'
21
import { basename, dirname } from 'path'
22
import { pipeline } from 'stream/promises'
23
import formidable from 'formidable'
@@ -36,8 +26,6 @@ import { allowAdmin, favicon } from './adminApis'
26
import { constants } from 'zlib'
27
import { baseUrl, getHttpsWorkingPort } from './listen'
28
import { defineConfig } from './config'
39
-import { getLangData } from './lang'
40
-import { getSection } from './customHtml'
29
import { sendErrorPage } from './errorPages'
30
31
const forceHttps = defineConfig('force_https', true)
@@ -215,6 +203,7 @@ export function getProxyDetected() {
203
return !ignoreProxies.get() && proxyDetected
204
&& { from: proxyDetected.ip, for: proxyDetected.get('X-Forwarded-For') }
205
}
206
+
207
export const prepareState: Koa.Middleware = async (ctx, next) => {
208
if (ctx.session)
209
ctx.session.maxAge = sessionDuration.compiled()
@@ -236,16 +225,6 @@ async function getHttpAccount(ctx: Koa.Context) {
225
return account
226
}
227
239
-async function srpCheck(username: string, password: string) {
240
- const account = getAccount(username)
241
- if (!account?.srp || !password) return false
242
- const { step1, salt, pubKey } = await srpStep1(account)
243
- const client = new SRPClientSession(new SRPRoutines(new SRPParameters()))
244
- const clientRes1 = await client.step1(username, password)
245
- const clientRes2 = await clientRes1.step2(BigInt(salt), BigInt(pubKey))
246
- return await step1.step2(clientRes2.A, clientRes2.M1).then(() => true, () => false)
247
-}
248
-
228
export const paramsDecoder: Koa.Middleware = async (ctx, next) => {
229
ctx.params = ctx.method === 'POST' && ctx.originalUrl.startsWith(API_URI)
230
&& (tryJson(await stream2string(ctx.req)) || {})
src/srp.ts
new
+19
@@ -0,0 +1,19 @@
1
+// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
+
3
+import { SRPClientSession, SRPParameters, SRPRoutines } from 'tssrp6a'
4
+
5
+export async function srpClientSequence(username:string, password:string, apiCall: (cmd:string, params:any) => any) {
6
+ const { pubKey, salt } = await apiCall('loginSrp1', { username })
7
+ if (!salt) throw Error('salt')
8
+ const client = await srpClientPart(username, password, salt, pubKey)
9
+ const res = await apiCall('loginSrp2', { pubKey: String(client.A), proof: String(client.M1) }) // bigint-s must be cast to string to be json-ed
10
+ await client.step3(BigInt(res.proof)).catch(() => Promise.reject('trust'))
11
+ return res
12
+}
13
+
14
+export async function srpClientPart(username: string, password: string, salt: string, pubKey: string) {
15
+ const srp6aNimbusRoutines = new SRPRoutines(new SRPParameters())
16
+ const srp = new SRPClientSession(srp6aNimbusRoutines);
17
+ const res = await srp.step1(username, password)
18
+ return await res.step2(BigInt(salt), BigInt(pubKey))
19
+}
\ No newline at end of file
tests/test.ts
+2
-2
@@ -1,7 +1,7 @@
1
import axios, { AxiosRequestConfig } from 'axios'
2
import { wrapper } from 'axios-cookiejar-support'
3
import { CookieJar } from 'tough-cookie'
4
-import { srpSequence } from '@hfs/shared/srp'
4
+import { srpClientSequence } from '../src/srp'
5
import { createReadStream, rmSync } from 'fs'
6
import { dirname, join } from 'path'
7
import _ from 'lodash'
@@ -129,7 +129,7 @@ describe('after-login', () => {
129
})
130
131
function login(usr: string, pwd=password) {
132
- return srpSequence(usr, pwd, (cmd: string, params: any) =>
132
+ return srpClientSequence(usr, pwd, (cmd: string, params: any) =>
133
reqApi(cmd, params, (x,res)=> !res.isAxiosError)())
134
}
135