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