hfs 0.26.7 → 0.26.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/admin/.DS_Store +0 -0
  2. package/admin/.eslintrc +8 -0
  3. package/admin/.gitignore +23 -0
  4. package/admin/index.html +1 -3
  5. package/admin/package.json +67 -0
  6. package/admin/{logo.svg → public/logo.svg} +0 -0
  7. package/admin/src/AccountForm.ts +92 -0
  8. package/admin/src/AccountsPage.ts +143 -0
  9. package/admin/src/App.ts +83 -0
  10. package/admin/src/ArrayField.ts +84 -0
  11. package/admin/src/ConfigPage.ts +279 -0
  12. package/admin/src/FileField.ts +52 -0
  13. package/admin/src/FileForm.ts +148 -0
  14. package/admin/src/FilePicker.ts +166 -0
  15. package/admin/src/HomePage.ts +96 -0
  16. package/admin/src/InstalledPlugins.ts +158 -0
  17. package/admin/src/LoginRequired.ts +75 -0
  18. package/admin/src/LogoutPage.ts +27 -0
  19. package/admin/src/LogsPage.ts +75 -0
  20. package/admin/src/MainMenu.ts +74 -0
  21. package/admin/src/MenuButton.ts +38 -0
  22. package/admin/src/MonitorPage.ts +200 -0
  23. package/admin/src/OnlinePlugins.ts +101 -0
  24. package/admin/src/PermField.ts +80 -0
  25. package/admin/src/PluginsPage.ts +27 -0
  26. package/admin/src/VfsMenuBar.ts +58 -0
  27. package/admin/src/VfsPage.ts +124 -0
  28. package/admin/src/VfsTree.ts +95 -0
  29. package/admin/src/addFiles.ts +59 -0
  30. package/admin/src/api.ts +246 -0
  31. package/admin/src/dialog.ts +203 -0
  32. package/admin/src/index.css +21 -0
  33. package/admin/src/index.ts +10 -0
  34. package/admin/src/md.ts +31 -0
  35. package/admin/src/misc.ts +141 -0
  36. package/admin/src/react-app-env.d.ts +1 -0
  37. package/admin/src/reportWebVitals.ts +15 -0
  38. package/admin/src/setupTests.ts +5 -0
  39. package/admin/src/state.ts +40 -0
  40. package/admin/src/theme.ts +37 -0
  41. package/admin/tsconfig.json +26 -0
  42. package/admin/vite.config.ts +32 -0
  43. package/frontend/.DS_Store +0 -0
  44. package/frontend/.eslintrc +8 -0
  45. package/frontend/.gitignore +23 -0
  46. package/frontend/index.html +1 -3
  47. package/frontend/package.json +51 -0
  48. package/frontend/{fontello.css → public/fontello.css} +0 -0
  49. package/frontend/{fontello.woff2 → public/fontello.woff2} +0 -0
  50. package/frontend/src/App.ts +25 -0
  51. package/frontend/src/Breadcrumbs.ts +43 -0
  52. package/frontend/src/BrowseFiles.ts +141 -0
  53. package/frontend/src/Head.ts +45 -0
  54. package/frontend/src/UserPanel.ts +52 -0
  55. package/frontend/src/api.ts +78 -0
  56. package/frontend/src/components.ts +54 -0
  57. package/frontend/src/dialog.css +76 -0
  58. package/frontend/src/dialog.ts +105 -0
  59. package/frontend/src/icons.ts +46 -0
  60. package/frontend/src/index.scss +307 -0
  61. package/frontend/src/index.ts +10 -0
  62. package/frontend/src/login.ts +50 -0
  63. package/frontend/src/menu.ts +188 -0
  64. package/frontend/src/misc.ts +54 -0
  65. package/frontend/src/options.ts +52 -0
  66. package/frontend/src/react-app-env.d.ts +1 -0
  67. package/frontend/src/reportWebVitals.ts +15 -0
  68. package/frontend/src/setupTests.ts +5 -0
  69. package/frontend/src/state.ts +82 -0
  70. package/frontend/src/useAuthorized.ts +17 -0
  71. package/frontend/src/useFetchList.ts +144 -0
  72. package/frontend/src/useTheme.ts +23 -0
  73. package/frontend/tsconfig.json +26 -0
  74. package/frontend/vite.config.ts +21 -0
  75. package/package.json +2 -1
  76. package/plugins/vhosting/plugin.js +1 -1
  77. package/src/QuickZipStream.ts +279 -0
  78. package/src/ThrottledStream.ts +98 -0
  79. package/src/adminApis.ts +161 -0
  80. package/src/api.accounts.ts +78 -0
  81. package/src/api.auth.ts +131 -0
  82. package/src/api.file_list.ts +102 -0
  83. package/src/api.helpers.ts +30 -0
  84. package/src/api.monitor.ts +106 -0
  85. package/src/api.plugins.ts +139 -0
  86. package/src/api.vfs.ts +182 -0
  87. package/src/apiMiddleware.ts +124 -0
  88. package/src/block.ts +35 -0
  89. package/src/commands.ts +122 -0
  90. package/src/config.ts +166 -0
  91. package/src/connections.ts +60 -0
  92. package/src/const.ts +57 -0
  93. package/src/crypt.ts +16 -0
  94. package/src/debounceAsync.ts +51 -0
  95. package/src/events.ts +6 -0
  96. package/src/frontEndApis.ts +17 -0
  97. package/src/github.ts +102 -0
  98. package/src/index.ts +53 -0
  99. package/src/listen.ts +220 -0
  100. package/src/log.ts +128 -0
  101. package/src/middlewares.ts +176 -0
  102. package/src/misc.ts +149 -0
  103. package/src/pbkdf2.ts +83 -0
  104. package/src/perm.ts +194 -0
  105. package/src/plugins.ts +342 -0
  106. package/src/serveFile.ts +104 -0
  107. package/src/serveGuiFiles.ts +95 -0
  108. package/src/sse.ts +29 -0
  109. package/src/throttler.ts +106 -0
  110. package/src/update.ts +67 -0
  111. package/src/util-files.ts +137 -0
  112. package/src/util-generators.ts +29 -0
  113. package/src/util-http.ts +29 -0
  114. package/src/vfs.ts +258 -0
  115. package/src/watchLoad.ts +75 -0
  116. package/src/zip.ts +69 -0
  117. package/admin/assets/index.0f549e00.js +0 -281
  118. package/admin/assets/index.dcc78777.css +0 -1
  119. package/admin/assets/sha512.ea1121b3.js +0 -8
  120. package/frontend/assets/index.1151988f.js +0 -85
  121. package/frontend/assets/index.93366732.css +0 -1
  122. package/frontend/assets/sha512.bb881250.js +0 -8
  123. package/src/QuickZipStream.js +0 -285
  124. package/src/ThrottledStream.js +0 -93
  125. package/src/adminApis.js +0 -169
  126. package/src/api.accounts.js +0 -59
  127. package/src/api.auth.js +0 -130
  128. package/src/api.file_list.js +0 -103
  129. package/src/api.helpers.js +0 -32
  130. package/src/api.monitor.js +0 -102
  131. package/src/api.plugins.js +0 -127
  132. package/src/api.vfs.js +0 -164
  133. package/src/apiMiddleware.js +0 -136
  134. package/src/block.js +0 -33
  135. package/src/commands.js +0 -124
  136. package/src/config.js +0 -168
  137. package/src/connections.js +0 -57
  138. package/src/const.js +0 -83
  139. package/src/crypt.js +0 -21
  140. package/src/debounceAsync.js +0 -48
  141. package/src/events.js +0 -9
  142. package/src/frontEndApis.js +0 -38
  143. package/src/github.js +0 -102
  144. package/src/index.js +0 -55
  145. package/src/listen.js +0 -235
  146. package/src/log.js +0 -137
  147. package/src/middlewares.js +0 -154
  148. package/src/misc.js +0 -160
  149. package/src/pbkdf2.js +0 -74
  150. package/src/perm.js +0 -176
  151. package/src/plugins.js +0 -343
  152. package/src/serveFile.js +0 -104
  153. package/src/serveGuiFiles.js +0 -113
  154. package/src/sse.js +0 -29
  155. package/src/throttler.js +0 -91
  156. package/src/update.js +0 -69
  157. package/src/util-files.js +0 -148
  158. package/src/util-generators.js +0 -30
  159. package/src/util-http.js +0 -30
  160. package/src/vfs.js +0 -227
  161. package/src/watchLoad.js +0 -73
  162. package/src/zip.js +0 -69
@@ -0,0 +1,98 @@
1
+ // This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
+
3
+ import { Transform, TransformCallback } from 'stream'
4
+ import { TokenBucket } from 'limiter'
5
+
6
+ // throttled stream
7
+ export class ThrottledStream extends Transform {
8
+
9
+ private sent: number = 0
10
+ private lastSpeed: number = 0
11
+ private lastSpeedTime = Date.now()
12
+ private totalSent: number = 0 // total sent over connection, since connection can be re-used for multiple requests
13
+
14
+ constructor(private group: ThrottleGroup, copyStats?: ThrottledStream) {
15
+ super()
16
+ if (!copyStats) return
17
+ this.sent = copyStats.sent
18
+ this.totalSent = copyStats.totalSent
19
+ this.lastSpeedTime = copyStats.lastSpeedTime
20
+ this.lastSpeed = copyStats.lastSpeed
21
+ }
22
+
23
+ async _transform(chunk: any, encoding: BufferEncoding, done: TransformCallback) {
24
+ let pos = 0
25
+ while (1) {
26
+ let n = this.group.suggestChunkSize()
27
+ const slice = chunk.slice(pos, pos + n)
28
+ n = slice.length
29
+ if (!n) // we're done here
30
+ return done()
31
+ try {
32
+ await this.group.consume(n)
33
+ this.push(slice)
34
+ this.sent += n
35
+ this.totalSent += n
36
+ pos += n
37
+ this.emit('sent', n)
38
+ } catch (e) {
39
+ done(e as Error)
40
+ return
41
+ }
42
+ }
43
+ }
44
+
45
+ // @return kBs
46
+ getSpeed(): number {
47
+ const now = Date.now()
48
+ const past = now - this.lastSpeedTime
49
+ if (past >= 1000) { // recalculate?
50
+ this.lastSpeedTime = now
51
+ this.lastSpeed = this.sent / past
52
+ this.sent = 0
53
+ }
54
+ return this.lastSpeed
55
+ }
56
+
57
+ getBytesSent() {
58
+ return this.totalSent
59
+ }
60
+ }
61
+
62
+ export class ThrottleGroup {
63
+
64
+ private bucket: TokenBucket
65
+
66
+ constructor(kBs: number, private parent?: ThrottleGroup) {
67
+ this.bucket = this.updateLimit(kBs) // assignment is redundant and yet the best way I've found to shut up typescript
68
+ }
69
+
70
+ // @return kBs
71
+ getLimit() {
72
+ return this.bucket.bucketSize / 1000
73
+ }
74
+
75
+ updateLimit(kBs: number) {
76
+ if (kBs < 0)
77
+ throw new Error('invalid bytesPerSecond')
78
+ kBs *= 1000
79
+ return this.bucket = new TokenBucket({
80
+ bucketSize: kBs,
81
+ tokensPerInterval: kBs,
82
+ interval: 'second',
83
+ })
84
+ }
85
+
86
+ suggestChunkSize() {
87
+ let b: TokenBucket | undefined = this.bucket
88
+ b.parentBucket = this.parent?.bucket
89
+ let min = b.bucketSize
90
+ while (b = b.parentBucket)
91
+ min = Math.min(min, b.bucketSize)
92
+ return min / 10
93
+ }
94
+
95
+ consume(n: number) {
96
+ return this.bucket.removeTokens(n)
97
+ }
98
+ }
@@ -0,0 +1,161 @@
1
+ // This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
+
3
+ import { ApiError, ApiHandlers, SendListReadable } from './apiMiddleware'
4
+ import { defineConfig, getWholeConfig, setConfig } from './config'
5
+ import { getStatus, getUrls, httpsPortCfg, portCfg } from './listen'
6
+ import {
7
+ API_VERSION,
8
+ BUILD_TIMESTAMP,
9
+ COMPATIBLE_API_VERSION,
10
+ FORBIDDEN,
11
+ HFS_STARTED,
12
+ IS_WINDOWS,
13
+ UNAUTHORIZED,
14
+ VERSION
15
+ } from './const'
16
+ import vfsApis from './api.vfs'
17
+ import accountsApis from './api.accounts'
18
+ import pluginsApis from './api.plugins'
19
+ import monitorApis from './api.monitor'
20
+ import { getConnections } from './connections'
21
+ import { debounceAsync, isLocalHost, onOff, wait } from './misc'
22
+ import _ from 'lodash'
23
+ import events from './events'
24
+ import { getFromAccount } from './perm'
25
+ import Koa from 'koa'
26
+ import { getProxyDetected } from './middlewares'
27
+ import { writeFile } from 'fs/promises'
28
+ import { createReadStream } from 'fs'
29
+ import * as readline from 'readline'
30
+ import { loggers } from './log'
31
+ import { execFile } from 'child_process'
32
+ import { promisify } from 'util'
33
+
34
+ export const adminApis: ApiHandlers = {
35
+
36
+ ...vfsApis,
37
+ ...accountsApis,
38
+ ...pluginsApis,
39
+ ...monitorApis,
40
+
41
+ async set_config({ values: v }) {
42
+ if (v) {
43
+ const st = getStatus()
44
+ const noHttp = (v.port ?? portCfg.get()) < 0 || !st.httpSrv.listening
45
+ const noHttps = (v.https_port ?? httpsPortCfg.get()) < 0 || !st.httpsSrv.listening
46
+ if (noHttp && noHttps)
47
+ return new ApiError(FORBIDDEN, "You cannot switch off both http and https ports")
48
+ await setConfig(v)
49
+ }
50
+ return {}
51
+ },
52
+
53
+ get_config: getWholeConfig,
54
+
55
+ async get_status() {
56
+ const st = getStatus()
57
+ return {
58
+ started: HFS_STARTED,
59
+ build: BUILD_TIMESTAMP,
60
+ version: VERSION,
61
+ apiVersion: API_VERSION,
62
+ compatibleApiVersion: COMPATIBLE_API_VERSION,
63
+ http: await serverStatus(st.httpSrv, portCfg.get()),
64
+ https: await serverStatus(st.httpsSrv, httpsPortCfg.get()),
65
+ urls: getUrls(),
66
+ proxyDetected: getProxyDetected(),
67
+ frpDetected: localhostAdmin.get() && !getProxyDetected()
68
+ && getConnections().every(isLocalHost)
69
+ && await frpDebounced(),
70
+ }
71
+
72
+ async function serverStatus(h: typeof st.httpSrv, configuredPort?: number) {
73
+ const busy = await h.busy
74
+ await wait(0) // simple trick to wait for also .error to be updated. If this trickery becomes necessary elsewhere, then we should make also error a Promise.
75
+ return {
76
+ ..._.pick(h, ['listening', 'error']),
77
+ busy,
78
+ port: (h?.address() as any)?.port || configuredPort,
79
+ }
80
+ }
81
+ },
82
+
83
+ async save_pem({ cert, private_key, name='self' }) {
84
+ if (!cert || !private_key)
85
+ return new ApiError(400)
86
+ const files = { cert: name + '.cert', private_key: name + '.key' }
87
+ await writeFile(files.private_key, private_key)
88
+ await writeFile(files.cert, cert)
89
+ return files
90
+ },
91
+
92
+ async get_log({ file='log' }, ctx) {
93
+ return new SendListReadable({
94
+ bufferTime: 10,
95
+ doAtStart(list) {
96
+ const logger = loggers.find(l => l.name === file)
97
+ if (!logger)
98
+ return list.error(404, true)
99
+ const input = createReadStream(logger.path)
100
+ input.on('error', async (e: any) => {
101
+ if (e.code === 'ENOENT') // ignore ENOENT, consider it an empty log
102
+ return list.ready()
103
+ list.error(e.code || e.message)
104
+ })
105
+ input.on('end', () =>
106
+ list.ready())
107
+ input.on('ready', () => {
108
+ readline.createInterface({ input }).on('line', line => {
109
+ if (ctx.aborted)
110
+ return input.close()
111
+ const obj = parse(line)
112
+ if (obj)
113
+ list.add(obj)
114
+ }).on('close', () => { // file is automatically closed, so we continue by events
115
+ ctx.res.once('close', onOff(events, { // unsubscribe when connection is interrupted
116
+ [logger.name](entry) {
117
+ list.add(entry)
118
+ }
119
+ }))
120
+ })
121
+ })
122
+ }
123
+ })
124
+
125
+ function parse(line: string) {
126
+ const m = /^(.+?) (.+?) (.+?) \[(.{11}):(.{14})] "(\w+) ([^"]+) HTTP\/\d.\d" (\d+) (-|\d+)/.exec(line)
127
+ if (!m) return
128
+ const [, ip, , user, date, time, method, uri, status, length] = m
129
+ return { // keep object format same as events emitted by the log module
130
+ ip,
131
+ user: user === '-' ? undefined : user,
132
+ ts: new Date(date + ' ' + time),
133
+ method,
134
+ uri,
135
+ status: Number(status),
136
+ length: length === '-' ? undefined : Number(length),
137
+ }
138
+ }
139
+ },
140
+ }
141
+
142
+ for (const k in adminApis) {
143
+ const was = adminApis[k]
144
+ adminApis[k] = (params, ctx) =>
145
+ ctxAdminAccess(ctx) ? was(params, ctx)
146
+ : new ApiError(UNAUTHORIZED)
147
+ }
148
+
149
+ export const localhostAdmin = defineConfig('localhost_admin', true)
150
+
151
+ export function ctxAdminAccess(ctx: Koa.Context) {
152
+ return !ctx.state.proxiedFor // we consider localhost_admin only if no proxy is detected
153
+ && localhostAdmin.get() && isLocalHost(ctx)
154
+ || getFromAccount(ctx.state.account, a => a.admin)
155
+ }
156
+
157
+ const frpDebounced = debounceAsync(async () => {
158
+ if (!IS_WINDOWS) return false
159
+ const { stdout } = await promisify(execFile)('tasklist', ['/fi','imagename eq frpc.exe','/nh'])
160
+ return stdout.includes('frpc')
161
+ })
@@ -0,0 +1,78 @@
1
+ // This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
+
3
+ import { changePasswordHelper, changeSrpHelper } from './api.helpers'
4
+ import { ApiError, ApiHandlers } from './apiMiddleware'
5
+ import {
6
+ Account,
7
+ accountCanLoginAdmin,
8
+ accountHasPassword,
9
+ addAccount,
10
+ delAccount,
11
+ getAccount,
12
+ getAccounts,
13
+ getCurrentUsername,
14
+ setAccount
15
+ } from './perm'
16
+ import _ from 'lodash'
17
+ import { FORBIDDEN } from './const'
18
+
19
+ function prepareAccount(ac: Account | undefined) {
20
+ return ac && {
21
+ ..._.omit(ac, ['password','hashed_password','srp']),
22
+ username: ac.username, // omit won't copy it because it's a hidden prop
23
+ hasPassword: accountHasPassword(ac),
24
+ adminActualAccess: accountCanLoginAdmin(ac),
25
+ }
26
+ }
27
+
28
+ const apis: ApiHandlers = {
29
+
30
+ get_usernames() {
31
+ return { list: Object.keys(getAccounts()) }
32
+ },
33
+
34
+ get_account({ username }, ctx) {
35
+ return prepareAccount(getAccount(username || getCurrentUsername(ctx)))
36
+ || new ApiError(404)
37
+ },
38
+
39
+ get_accounts() {
40
+ return { list: Object.values(getAccounts()).map(prepareAccount) }
41
+ },
42
+
43
+ get_admins() {
44
+ return { list: Object.values(getAccounts()).map(prepareAccount).filter(ac => ac?.adminActualAccess).map(ac => ac!.username) }
45
+ },
46
+
47
+ set_account({ username, changes }) {
48
+ const { admin } = changes
49
+ if (admin === null)
50
+ changes.admin = undefined
51
+ else if (admin !== undefined && typeof admin !== 'boolean')
52
+ return new ApiError(400, "invalid admin")
53
+ return setAccount(username, changes) ? {} : new ApiError(400)
54
+ },
55
+
56
+ add_account({ username, ...rest }) {
57
+ if (getAccount(username))
58
+ return new ApiError(FORBIDDEN)
59
+ if (!addAccount(username, rest))
60
+ return new ApiError(400)
61
+ return {}
62
+ },
63
+
64
+ del_account({ username }) {
65
+ return delAccount(username) ? {} : new ApiError(400)
66
+ },
67
+
68
+ async change_password_others({ username, newPassword }) {
69
+ return changePasswordHelper(getAccount(username), newPassword)
70
+ },
71
+
72
+ async change_srp_others({ username, salt, verifier }) {
73
+ return changeSrpHelper(getAccount(username), salt, verifier)
74
+ }
75
+
76
+ }
77
+
78
+ export default apis
@@ -0,0 +1,131 @@
1
+ // This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
+
3
+ import { Account, getAccount, getCurrentUsername } from './perm'
4
+ import { verifyPassword } from './crypt'
5
+ import { ApiError, ApiHandler } from './apiMiddleware'
6
+ import { SRPParameters, SRPRoutines, SRPServerSession, SRPServerSessionStep1 } from 'tssrp6a'
7
+ import { ADMIN_URI, SESSION_DURATION, UNAUTHORIZED } from './const'
8
+ import { randomId } from './misc'
9
+ import Koa from 'koa'
10
+ import { changeSrpHelper, changePasswordHelper } from './api.helpers'
11
+ import { ctxAdminAccess } from './adminApis'
12
+ import { prepareState } from './middlewares'
13
+
14
+ const srp6aNimbusRoutines = new SRPRoutines(new SRPParameters())
15
+ const ongoingLogins:Record<string,SRPServerSessionStep1> = {} // store data that doesn't fit session object
16
+
17
+ // centralized log-in state
18
+ async function loggedIn(ctx:Koa.Context, username: string | false) {
19
+ const s = ctx.session
20
+ if (!s)
21
+ return ctx.throw(500,'session')
22
+ if (username === false) {
23
+ delete s.username
24
+ ctx.cookies.set('csrf', '')
25
+ return
26
+ }
27
+ s.username = username
28
+ await prepareState(ctx, async ()=>{}) // updating the state is necessary to send complete session data so that frontend shows admin button
29
+ delete s.login
30
+ ctx.cookies.set('csrf', randomId(), { signed:false, httpOnly: false })
31
+ }
32
+
33
+ function makeExp() {
34
+ return { exp: new Date(Date.now() + SESSION_DURATION) }
35
+ }
36
+
37
+ export const login: ApiHandler = async ({ username, password }, ctx) => {
38
+ if (!username || !password) // some validation
39
+ return new ApiError(400)
40
+ username = username.toLocaleLowerCase() // normalize username, to be case-insensitive
41
+ const acc = getAccount(username)
42
+ if (!acc)
43
+ return new ApiError(UNAUTHORIZED)
44
+ if (!acc.hashed_password)
45
+ return new ApiError(406)
46
+ if (!await verifyPassword(acc.hashed_password, password))
47
+ return new ApiError(UNAUTHORIZED)
48
+ if (!ctx.session)
49
+ return new ApiError(500)
50
+ await loggedIn(ctx, username)
51
+ return { ...makeExp(), redirect: acc.redirect }
52
+ }
53
+
54
+ export const loginSrp1: ApiHandler = async ({ username }, ctx) => {
55
+ if (!username)
56
+ return new ApiError(400)
57
+ username = username.toLocaleLowerCase()
58
+ const account = getAccount(username)
59
+ if (!ctx.session)
60
+ return new ApiError(500)
61
+ if (!account) // TODO simulate fake account to prevent knowing valid usernames
62
+ return new ApiError(UNAUTHORIZED)
63
+ try {
64
+ const { step1, ...rest } = await srpStep1(account)
65
+ const sid = Math.random()
66
+ ongoingLogins[sid] = step1
67
+ setTimeout(()=> delete ongoingLogins[sid], 60_000)
68
+ ctx.session.login = { username, sid }
69
+ return rest
70
+ }
71
+ catch (code: any) {
72
+ return new ApiError(code)
73
+ }
74
+ }
75
+
76
+ export async function srpStep1(account: Account) {
77
+ if (!account.srp)
78
+ throw 406 // unacceptable
79
+ const [salt, verifier] = account.srp.split('|')
80
+ const srpSession = new SRPServerSession(srp6aNimbusRoutines)
81
+ const step1 = await srpSession.step1(account.username, BigInt(salt), BigInt(verifier))
82
+ return { step1, salt, pubKey: String(step1.B) } // cast to string cause bigint can't be jsonized
83
+ }
84
+
85
+ export const loginSrp2: ApiHandler = async ({ pubKey, proof }, ctx) => {
86
+ if (!ctx.session)
87
+ return new ApiError(500)
88
+ if (!ctx.session.login)
89
+ return new ApiError(409)
90
+ const { username, sid } = ctx.session.login
91
+ const step1 = ongoingLogins[sid]
92
+ try {
93
+ const M2 = await step1.step2(BigInt(pubKey), BigInt(proof))
94
+ await loggedIn(ctx, username)
95
+ return {
96
+ proof: String(M2),
97
+ redirect: ctx.state.account?.redirect,
98
+ ...await refresh_session({},ctx)
99
+ }
100
+ }
101
+ catch(e) {
102
+ return new ApiError(UNAUTHORIZED, String(e))
103
+ }
104
+ finally {
105
+ delete ongoingLogins[sid]
106
+ }
107
+ }
108
+
109
+ export const logout: ApiHandler = async ({}, ctx) => {
110
+ if (!ctx.session)
111
+ return new ApiError(500)
112
+ loggedIn(ctx, false)
113
+ // 401 is a convenient code for OK: the browser clears a possible http authentication (hopefully), and Admin automatically triggers login dialog
114
+ return new ApiError(401)
115
+ }
116
+
117
+ export const refresh_session: ApiHandler = async ({}, ctx) => {
118
+ return !ctx.session ? new ApiError(500) : {
119
+ username: getCurrentUsername(ctx),
120
+ adminUrl: ctxAdminAccess(ctx) ? ADMIN_URI : undefined,
121
+ ...makeExp(),
122
+ }
123
+ }
124
+
125
+ export const change_password: ApiHandler = async ({ newPassword }, ctx) => {
126
+ return changePasswordHelper(ctx.state.account, newPassword)
127
+ }
128
+
129
+ export const change_srp: ApiHandler = async ({ salt, verifier }, ctx) => {
130
+ return changeSrpHelper(ctx.state.account, salt, verifier)
131
+ }
@@ -0,0 +1,102 @@
1
+ // This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
+
3
+ import { cantReadStatusCode, getNodeName, hasPermission, nodeIsDirectory, urlToNode, VfsNode, walkNode } from './vfs'
4
+ import { ApiError, ApiHandler, SendListReadable } from './apiMiddleware'
5
+ import { stat } from 'fs/promises'
6
+ import { mapPlugins } from './plugins'
7
+ import { asyncGeneratorToArray, dirTraversal, pattern2filter } from './misc'
8
+ import _ from 'lodash'
9
+
10
+ export const file_list: ApiHandler = async ({ path, offset, limit, search, omit, sse }, ctx) => {
11
+ let node = await urlToNode(path || '/', ctx)
12
+ const list = new SendListReadable()
13
+ if (!node)
14
+ return fail(404)
15
+ if (!hasPermission(node,'can_read',ctx))
16
+ return fail(cantReadStatusCode(node))
17
+ if (dirTraversal(search))
18
+ return fail(418)
19
+ if (node.default)
20
+ return (sse ? list.custom : _.identity)({ redirect: path }) // sse will wrap the object in a 'custom' message, otherwise we plainly return the object
21
+ if (!await nodeIsDirectory(node))
22
+ return fail(405) // method not allowed on target
23
+ offset = Number(offset)
24
+ limit = Number(limit)
25
+ const filter = pattern2filter(search)
26
+ const walker = walkNode(node, ctx, search ? Infinity : 0)
27
+ const onDirEntryHandlers = mapPlugins(plug => plug.onDirEntry)
28
+ if (!sse)
29
+ return { list: await asyncGeneratorToArray(produceEntries()) }
30
+ setTimeout(async () => {
31
+ for await (const entry of produceEntries())
32
+ list.add(entry)
33
+ list.close()
34
+ })
35
+ return list
36
+
37
+ function fail(code: any) {
38
+ if (!sse)
39
+ return new ApiError(code)
40
+ list.error(code)
41
+ list.close()
42
+ return list
43
+ }
44
+
45
+ async function* produceEntries() {
46
+ for await (const sub of walker) {
47
+ if (ctx.aborted) break
48
+ if (!filter(getNodeName(sub)))
49
+ continue
50
+ const entry = await nodeToDirEntry(sub)
51
+ if (!entry)
52
+ continue
53
+ const cbParams = { entry, ctx, listPath:path, node:sub }
54
+ try {
55
+ if (onDirEntryHandlers.some(cb => cb(cbParams) === false))
56
+ continue
57
+ }
58
+ catch(e) {
59
+ console.log("a plugin with onDirEntry is causing problems:", e)
60
+ }
61
+ if (offset) {
62
+ --offset
63
+ continue
64
+ }
65
+ if (omit) {
66
+ if (omit !== 'c')
67
+ ctx.throw(400, 'omit')
68
+ if (!entry.m)
69
+ entry.m = entry.c
70
+ delete entry.c
71
+ }
72
+ yield entry
73
+ if (limit && !--limit)
74
+ break
75
+ }
76
+ }
77
+ }
78
+
79
+ export interface DirEntry { n:string, s?:number, m?:Date, c?:Date }
80
+
81
+ async function nodeToDirEntry(node: VfsNode): Promise<DirEntry | null> {
82
+ let { source, default:def } = node
83
+ const name = getNodeName(node)
84
+ if (!source)
85
+ return name ? { n: name + '/' } : null
86
+ if (def)
87
+ return { n: name }
88
+ try {
89
+ const st = await stat(source)
90
+ const folder = st.isDirectory()
91
+ const { ctime, mtime } = st
92
+ return {
93
+ n: name + (folder ? '/' : ''),
94
+ c: ctime,
95
+ m: Math.abs(+mtime-+ctime) < 1000 ? undefined : mtime,
96
+ s: folder ? undefined : st.size,
97
+ }
98
+ }
99
+ catch {
100
+ return null
101
+ }
102
+ }
@@ -0,0 +1,30 @@
1
+ // This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
+
3
+ import { Account, allowClearTextLogin, saveSrpInfo, updateAccount } from './perm'
4
+ import { ApiError } from './apiMiddleware'
5
+ import { UNAUTHORIZED } from './const'
6
+
7
+ export async function changePasswordHelper(account: Account | undefined, newPassword: string) {
8
+ if (!newPassword) // clear text version
9
+ return Error('missing parameters')
10
+ if (!account)
11
+ return new ApiError(UNAUTHORIZED)
12
+ await updateAccount(account, account => {
13
+ account.password = newPassword
14
+ })
15
+ return {}
16
+ }
17
+
18
+ export async function changeSrpHelper(account: Account | undefined, salt: string, verifier: string) {
19
+ if (allowClearTextLogin.get())
20
+ return new ApiError(406)
21
+ if (!salt || !verifier)
22
+ return Error('missing parameters')
23
+ if (!account)
24
+ return new ApiError(UNAUTHORIZED)
25
+ await updateAccount(account, account => {
26
+ saveSrpInfo(account, salt, verifier)
27
+ delete account.hashed_password // remove leftovers
28
+ })
29
+ return {}
30
+ }