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
package/src/listen.ts ADDED
@@ -0,0 +1,220 @@
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 * as http from 'http'
4
+ import { defineConfig } from './config'
5
+ import { app } from './index'
6
+ import * as https from 'https'
7
+ import { watchLoad } from './watchLoad'
8
+ import { networkInterfaces } from 'os';
9
+ import { newConnection } from './connections'
10
+ import open from 'open'
11
+ import { debounceAsync, onlyTruthy, wait } from './misc'
12
+ import { ADMIN_URI, DEV } from './const'
13
+ import findProcess from 'find-process'
14
+ import { anyAccountCanLoginAdmin } from './perm'
15
+ import _ from 'lodash'
16
+
17
+ interface ServerExtra { name: string, error?: string, busy?: Promise<string> }
18
+ let httpSrv: http.Server & ServerExtra
19
+ let httpsSrv: http.Server & ServerExtra
20
+
21
+ const openBrowserAtStart = defineConfig('open_browser_at_start', !DEV)
22
+
23
+ export const portCfg = defineConfig<number>('port', 80)
24
+ portCfg.sub(async port => {
25
+ while (!app)
26
+ await wait(100)
27
+ stopServer(httpSrv).then()
28
+ httpSrv = Object.assign(http.createServer(app.callback()), { name: 'http' })
29
+ port = await startServer(httpSrv, { port })
30
+ if (!port) return
31
+ httpSrv.on('connection', newConnection)
32
+ printUrls(port, 'http')
33
+ if (openBrowserAtStart.get())
34
+ openAdmin()
35
+ })
36
+
37
+ export function openAdmin() {
38
+ for (const srv of [httpSrv, httpsSrv]) {
39
+ const a = srv.address()
40
+ if (!a || typeof a === 'string') continue
41
+ const baseUrl = srv.name + '://localhost:' + a.port
42
+ open(baseUrl + ADMIN_URI, { wait: true}).catch(e => {
43
+ console.debug(String(e))
44
+ console.warn("cannot launch browser on this machine >PLEASE< open your browser and reach one of these (you may need a different address)",
45
+ ...Object.values(getUrls()).flat().map(x => '\n - ' + x + ADMIN_URI))
46
+ if (! anyAccountCanLoginAdmin())
47
+ console.log(`HINT: you can enter command: create-admin YOUR_PASSWORD`)
48
+ })
49
+ return true
50
+ }
51
+ console.log("openAdmin failed")
52
+ }
53
+
54
+ const considerHttps = debounceAsync(async () => {
55
+ stopServer(httpsSrv).then()
56
+ let port = httpsPortCfg.get()
57
+ try {
58
+ while (!app)
59
+ await wait(100)
60
+ httpsSrv = Object.assign(
61
+ https.createServer(port < 0 ? {} : { key: httpsOptions.private_key, cert: httpsOptions.cert }, app.callback()),
62
+ { name: 'https', error: undefined }
63
+ )
64
+ if (port >= 0) {
65
+ const namesForOutput: any = { cert: 'certificate', private_key: 'private key' }
66
+ const missing = httpsNeeds.find(x => !x.get())?.key()
67
+ if (missing)
68
+ return httpsSrv.error = "missing " + namesForOutput[missing]
69
+ const cantRead = httpsNeeds.find(x => !httpsOptions[x.key() as HttpsKeys])?.key()
70
+ if (cantRead)
71
+ return httpsSrv.error = "cannot read " + namesForOutput[cantRead]
72
+ }
73
+ }
74
+ catch(e) {
75
+ httpsSrv.error = "bad private key or certificate"
76
+ console.log("failed to create https server: check your private key and certificate", String(e))
77
+ return
78
+ }
79
+ port = await startServer(httpsSrv, { port })
80
+ if (!port) return
81
+ httpsSrv.on('connection', newConnection)
82
+ printUrls(port, 'https')
83
+ })
84
+
85
+
86
+ const cert = defineConfig<string>('cert')
87
+ const privateKey = defineConfig<string>('private_key')
88
+ const httpsNeeds = [cert, privateKey]
89
+ const httpsOptions = { cert: '', private_key: '' }
90
+ type HttpsKeys = keyof typeof httpsOptions
91
+ for (const cfg of httpsNeeds) {
92
+ let unwatch: ReturnType<typeof watchLoad>['unwatch']
93
+ cfg.sub(async v => {
94
+ unwatch?.()
95
+ const k = cfg.key() as HttpsKeys
96
+ httpsOptions[k] = v
97
+ if (!v || v.includes('\n'))
98
+ return considerHttps()
99
+ // v is a path
100
+ httpsOptions[k] = ''
101
+ unwatch = watchLoad(v, data => {
102
+ httpsOptions[k] = data
103
+ considerHttps()
104
+ }).unwatch
105
+ await considerHttps()
106
+ })
107
+ }
108
+
109
+ export const httpsPortCfg = defineConfig('https_port', -1)
110
+ httpsPortCfg.sub(considerHttps)
111
+
112
+ interface StartServer { port: number, host?:string }
113
+ function startServer(srv: typeof httpSrv, { port, host }: StartServer) {
114
+ return new Promise<number>(async resolve => {
115
+ try {
116
+ if (port < 0 || !host && !await testIpV4()) // !host means ipV4+6, and if v4 port alone is busy we won't be notified of the failure, so we'll first test it on its own
117
+ return resolve(0)
118
+ port = await listen(host)
119
+ if (port)
120
+ console.log(srv.name, "serving on", host||"any network", ':', port)
121
+ resolve(port)
122
+ }
123
+ catch(e) {
124
+ srv.error = String(e)
125
+ console.error(srv.name, "couldn't listen on port", port, srv.error)
126
+ resolve(0)
127
+ }
128
+ })
129
+
130
+ async function testIpV4() {
131
+ const res = await listen('0.0.0.0')
132
+ await new Promise(res => srv.close(res))
133
+ return res > 0
134
+ }
135
+
136
+ function listen(host?: string) {
137
+ return new Promise<number>(async (resolve, reject) => {
138
+ srv.listen({ port, host }, () => {
139
+ const ad = srv.address()
140
+ if (!ad)
141
+ return reject('no address')
142
+ if (typeof ad === 'string') {
143
+ srv.close()
144
+ return reject('type of socket not supported')
145
+ }
146
+ resolve(ad.port)
147
+ }).on('error', async e => {
148
+ srv.error = String(e)
149
+ srv.busy = undefined
150
+ const { code } = e as any
151
+ if (code === 'EADDRINUSE') {
152
+ srv.busy = findProcess('port', port).then(res => res?.[0]?.name || '', () => '')
153
+ srv.error = `port ${port} busy: ${await srv.busy || "unknown process"}`
154
+ }
155
+ console.error(srv.name, srv.error)
156
+ const k = (srv === httpSrv? portCfg : httpsPortCfg).key()
157
+ console.log(` >> try specifying a different port, enter this command: config ${k} 8011`)
158
+ resolve(0)
159
+ })
160
+ })
161
+ }
162
+ }
163
+
164
+ function stopServer(srv: http.Server) {
165
+ return new Promise(resolve => {
166
+ if (!srv?.listening)
167
+ return resolve(null)
168
+ const ad = srv.address()
169
+ if (ad && typeof ad !== 'string')
170
+ console.log("stopped port", ad.port)
171
+ srv.close(err => {
172
+ if (err && (err as any).code !== 'ERR_SERVER_NOT_RUNNING')
173
+ console.debug("failed to stop server", String(err))
174
+ resolve(err)
175
+ })
176
+ })
177
+ }
178
+
179
+ export function getStatus() {
180
+ return {
181
+ httpSrv,
182
+ httpsSrv,
183
+ }
184
+ }
185
+
186
+ const ignore = /^(lo|.*loopback.*|virtualbox.*|.*\(wsl\).*|llw\d|awdl\d|utun\d|anpi\d)$/i // avoid giving too much information
187
+
188
+ export function getUrls() {
189
+ return Object.fromEntries(onlyTruthy([httpSrv, httpsSrv].map(srv => {
190
+ if (!srv?.listening)
191
+ return false
192
+ const port = (srv?.address() as any)?.port
193
+ const appendPort = port === (srv.name === 'https' ? 443 : 80) ? '' : ':' + port
194
+ const urls = onlyTruthy(Object.entries(networkInterfaces()).map(([name, nets]) =>
195
+ nets && !ignore.test(name) && nets.map(net => {
196
+ if (net.internal) return
197
+ let { address } = net
198
+ if (address.includes(':'))
199
+ address = '[' + address + ']'
200
+ return srv.name + '://' + address + appendPort
201
+ })
202
+ ).flat())
203
+ return urls.length && [srv.name, urls]
204
+ })))
205
+ }
206
+
207
+ function printUrls(port: number, proto: string) {
208
+ if (!port) return
209
+ for (const [name, nets] of Object.entries(networkInterfaces())) {
210
+ if (!nets || ignore.test(name)) continue
211
+ _.remove(nets, 'internal')
212
+ if (!nets.length) continue
213
+ const best = _.find(nets, { family: 'IPv4' }) || nets[0]
214
+ const appendPort = port === (proto==='https' ? 443 : 80) ? '' : ':' + port
215
+ let { address } = best
216
+ if (address.includes(':'))
217
+ address = '['+address+']'
218
+ console.log('network', name, proto + '://' + address + appendPort)
219
+ }
220
+ }
package/src/log.ts ADDED
@@ -0,0 +1,128 @@
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 Koa from 'koa'
4
+ import { Writable } from 'stream'
5
+ import { defineConfig } from './config'
6
+ import { createWriteStream, renameSync } from 'fs'
7
+ import * as util from 'util'
8
+ import { mkdir, stat } from 'fs/promises'
9
+ import { DAY } from './const'
10
+ import events from './events'
11
+ import _ from 'lodash'
12
+ import { dirname } from 'path'
13
+ import { getCurrentUsername } from './perm'
14
+
15
+ class Logger {
16
+ stream?: Writable
17
+ last?: Date
18
+ path: string = ''
19
+
20
+ constructor(readonly name: string){
21
+ }
22
+
23
+ async setPath(path: string) {
24
+ this.path = path
25
+ this.stream?.end()
26
+ this.last = undefined
27
+ if (!path)
28
+ return this.stream = undefined
29
+ try {
30
+ const stats = await stat(path)
31
+ this.last = stats.mtime || stats.ctime
32
+ }
33
+ catch {
34
+ await mkdir(dirname(path), { recursive: true })
35
+ .catch(() => console.log("cannot create folder for", path))
36
+ }
37
+ this.reopen()
38
+ }
39
+
40
+ reopen() {
41
+ return this.stream = createWriteStream(this.path, { flags: 'a' })
42
+ .on('error', () => this.stream = undefined)
43
+ }
44
+ }
45
+
46
+ // we'll have names same as config keys. These are used also by the get_log api.
47
+ const accessLogger = new Logger('log')
48
+ const accessErrorLog = new Logger('error_log')
49
+ export const loggers = [accessLogger, accessErrorLog]
50
+
51
+ defineConfig('log', 'logs/access.log').sub(path => {
52
+ console.debug('access log file: ' + (path || 'disabled'))
53
+ accessLogger.setPath(path)
54
+ })
55
+
56
+ const errorLogFile = defineConfig(accessErrorLog.name, 'logs/access-error.log')
57
+ errorLogFile.sub(path => {
58
+ console.debug('access error log: ' + (path || 'disabled'))
59
+ accessErrorLog.setPath(path)
60
+ })
61
+
62
+ const logRotation = defineConfig('log_rotation', 'weekly')
63
+
64
+ export function log(): Koa.Middleware {
65
+ const debounce = _.debounce(cb => cb(), 1000)
66
+ return async (ctx, next) => { // wrapping in a function will make it use current 'mw' value
67
+ await next()
68
+ const isError = ctx.status >= 400
69
+ const logger = isError && accessErrorLog || accessLogger
70
+ const rotate = logRotation.get()?.[0]
71
+ let { stream, last, path } = logger
72
+ if (!stream) return
73
+ const now = new Date()
74
+ const a = now.toString().split(' ')
75
+ logger.last = now
76
+ if (rotate && last) { // rotation enabled and a file exists?
77
+ const passed = Number(now) - Number(last)
78
+ - 3600_000 // be pessimistic and count a possible DST change
79
+ if (rotate === 'm' && (passed >= 31*DAY || now.getMonth() !== last.getMonth())
80
+ || rotate === 'd' && (passed >= DAY || now.getDate() !== last.getDate()) // checking passed will solve the case when the day of the month is the same but a month has passed
81
+ || rotate === 'w' && (passed >= 7*DAY || now.getDay() < last.getDay())) {
82
+ stream.end()
83
+ const postfix = last.getFullYear() + '-' + doubleDigit(last.getMonth() + 1) + '-' + doubleDigit(last.getDate())
84
+ try { // other logging requests shouldn't happen while we are renaming. Since this is very infrequent we can tolerate solving this by making it sync.
85
+ renameSync(path, path + '-' + postfix)
86
+ }
87
+ catch(e) { // ok, rename failed, but this doesn't mean we ain't gonna log
88
+ console.error(e)
89
+ }
90
+ stream = logger.reopen() // keep variable updated
91
+ if (!stream) return
92
+ }
93
+ }
94
+ const format = '%s - %s [%s] "%s %s HTTP/%s" %d %s\n' // Apache's Common Log Format
95
+ const date = a[2]+'/'+a[1]+'/'+a[3]+':'+a[4]+' '+a[5].slice(3)
96
+ const user = getCurrentUsername(ctx)
97
+ events.emit(logger.name, Object.assign(_.pick(ctx, ['ip', 'method','status','length']), { user, ts: now, uri: ctx.path }))
98
+ console.debug(ctx.status, ctx.method, ctx.path)
99
+ debounce(() => // once in a while we check if the file is still good (not deleted, etc), or we'll reopen it
100
+ stat(logger.path).catch(() => logger.reopen())) // async = smoother but we may lose some entries
101
+ stream.write(util.format( format,
102
+ ctx.ip,
103
+ user || '-',
104
+ date,
105
+ ctx.method,
106
+ ctx.path,
107
+ ctx.req.httpVersion,
108
+ ctx.status,
109
+ ctx.length ? ctx.length.toString() : '-',
110
+ ))
111
+ }
112
+ }
113
+
114
+ function doubleDigit(n: number) {
115
+ return n > 9 ? n : '0'+n
116
+ }
117
+
118
+ // dump console.error to file
119
+ const debugLogFile = createWriteStream('debug.log', { flags: 'a' })
120
+ debugLogFile.on('open', () => {
121
+ const was = console.error
122
+ console.error = function(...args: any[]) {
123
+ was.apply(this, args)
124
+ const params = args.map(x =>
125
+ typeof x === 'string' ? x : JSON.stringify(x)).join(' ')
126
+ debugLogFile.write(new Date().toLocaleString() + ': ' + params + '\n')
127
+ }
128
+ }).on('error', () => console.log("cannot create debug.log"))
@@ -0,0 +1,176 @@
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 compress from 'koa-compress'
4
+ import Koa from 'koa'
5
+ import session from 'koa-session'
6
+ import { ADMIN_URI, BUILD_TIMESTAMP, DEV, FORBIDDEN, SESSION_DURATION } from './const'
7
+ import Application from 'koa'
8
+ import { FRONTEND_URI } from './const'
9
+ import { cantReadStatusCode, hasPermission, nodeIsDirectory, urlToNode } from './vfs'
10
+ import { dirTraversal, objSameKeys, tryJson } from './misc'
11
+ import { zipStreamFromFolder } from './zip'
12
+ import { serveFileNode } from './serveFile'
13
+ import { serveGuiFiles } from './serveGuiFiles'
14
+ import mount from 'koa-mount'
15
+ import { Readable } from 'stream'
16
+ import { applyBlock } from './block'
17
+ import { getAccount, getCurrentUsername } from './perm'
18
+ import { socket2connection, updateConnection, normalizeIp } from './connections'
19
+ import basicAuth from 'basic-auth'
20
+ import { SRPClientSession, SRPParameters, SRPRoutines } from 'tssrp6a'
21
+ import { srpStep1 } from './api.auth'
22
+ import { IncomingMessage } from 'http'
23
+
24
+ export const gzipper = compress({
25
+ threshold: 2048,
26
+ gzip: { flush: require('zlib').constants.Z_SYNC_FLUSH },
27
+ deflate: { flush: require('zlib').constants.Z_SYNC_FLUSH },
28
+ br: false, // disable brotli
29
+ filter(type) {
30
+ return /text|javascript|style/i.test(type)
31
+ },
32
+ })
33
+
34
+ export const headRequests: Koa.Middleware = async (ctx, next) => {
35
+ const head = ctx.method === 'HEAD'
36
+ if (head)
37
+ ctx.method = 'GET' // let other middlewares work, so we can collect the size at the end
38
+ await next()
39
+ if (!head || ctx.body === undefined) return
40
+ const { length, status } = ctx.response
41
+ if (ctx.body)
42
+ ctx.body = Readable.from('') // empty the body for this is a HEAD request. Using Readable avoids koa from trying to set length to 0
43
+ ctx.status = status
44
+ if (length)
45
+ ctx.response.length = length
46
+ }
47
+
48
+ export const sessions = (app: Application) => session({
49
+ key: 'hfs_$id',
50
+ signed: true,
51
+ rolling: true,
52
+ maxAge: SESSION_DURATION,
53
+ }, app)
54
+
55
+ const serveFrontendFiles = serveGuiFiles(process.env.FRONTEND_PROXY, FRONTEND_URI)
56
+ const serveFrontendPrefixed = mount(FRONTEND_URI.slice(0,-1), serveFrontendFiles)
57
+ const serveAdminPrefixed = mount(ADMIN_URI.slice(0,-1), serveGuiFiles(process.env.ADMIN_PROXY, ADMIN_URI))
58
+
59
+ export const serveGuiAndSharedFiles: Koa.Middleware = async (ctx, next) => {
60
+ const { path } = ctx
61
+ if (ctx.body)
62
+ return next()
63
+ if (path.startsWith(FRONTEND_URI))
64
+ return serveFrontendPrefixed(ctx,next)
65
+ if (path+'/' === ADMIN_URI)
66
+ return ctx.redirect(ADMIN_URI)
67
+ if (path.startsWith(ADMIN_URI))
68
+ return serveAdminPrefixed(ctx,next)
69
+ const node = await urlToNode(path, ctx)
70
+ if (!node)
71
+ return ctx.status = 404
72
+ const canRead = hasPermission(node, 'can_read', ctx)
73
+ const isFolder = await nodeIsDirectory(node)
74
+ if (isFolder && !path.endsWith('/'))
75
+ return ctx.redirect(path + '/')
76
+ if (canRead && !isFolder)
77
+ return node.source ? serveFileNode(node)(ctx,next)
78
+ : next()
79
+ if (!canRead) {
80
+ ctx.status = cantReadStatusCode(node)
81
+ if (ctx.status === FORBIDDEN)
82
+ return
83
+ const browserDetected = ctx.get('Upgrade-Insecure-Requests') || ctx.get('Sec-Fetch-Mode') // ugh, heuristics
84
+ if (!browserDetected) // we don't want to trigger basic authentication on browsers, it's meant for download managers only
85
+ ctx.set('WWW-Authenticate', 'Basic') // we support basic authentication
86
+ ctx.state.serveApp = true
87
+ return serveFrontendFiles(ctx, next)
88
+ }
89
+ ctx.set({ server:'HFS '+BUILD_TIMESTAMP })
90
+ const { get } = ctx.query
91
+ if (get === 'zip')
92
+ return await zipStreamFromFolder(node, ctx)
93
+ if (node.default) {
94
+ const def = await urlToNode(path + node.default, ctx)
95
+ return !def ? next()
96
+ : hasPermission(def, 'can_read', ctx) ? serveFileNode(def)(ctx, next)
97
+ : ctx.status = cantReadStatusCode(def)
98
+ }
99
+ return serveFrontendFiles(ctx, next)
100
+ }
101
+
102
+ let proxyDetected = false
103
+ export const someSecurity: Koa.Middleware = async (ctx, next) => {
104
+ ctx.request.ip = normalizeIp(ctx.ip)
105
+ try {
106
+ let proxy = ctx.get('X-Forwarded-For')
107
+ // we have some dev-proxies to ignore
108
+ if (DEV && proxy && [process.env.FRONTEND_PROXY, process.env.ADMIN_PROXY].includes(ctx.get('X-Forwarded-port')))
109
+ proxy = ''
110
+ if (dirTraversal(decodeURI(ctx.path)))
111
+ return ctx.status = 418
112
+ if (applyBlock(ctx.socket, ctx.ip))
113
+ return
114
+ proxyDetected ||= proxy > ''
115
+ ctx.state.proxiedFor = proxy
116
+ }
117
+ catch {
118
+ return ctx.status = 418
119
+ }
120
+ return next()
121
+ }
122
+
123
+ // this is only about http proxies
124
+ export function getProxyDetected() {
125
+ return proxyDetected
126
+ }
127
+ export const prepareState: Koa.Middleware = async (ctx, next) => {
128
+ // calculate these once and for all
129
+ ctx.state.account = await getHttpAccount(ctx) ?? getAccount(getCurrentUsername(ctx))
130
+ const conn = ctx.state.connection = socket2connection(ctx.socket)
131
+ await next()
132
+ if (conn)
133
+ updateConnection(conn, { ctx })
134
+ }
135
+
136
+ async function getHttpAccount(ctx: Koa.Context) {
137
+ const credentials = basicAuth(ctx.req)
138
+ const account = getAccount(credentials?.name||'')
139
+ if (account && await srpCheck(account.username, credentials!.pass))
140
+ return account
141
+ }
142
+
143
+ async function srpCheck(username: string, password: string) {
144
+ username = username.toLocaleLowerCase()
145
+ const account = getAccount(username)
146
+ if (!account?.srp || !password) return false
147
+ const { step1, salt, pubKey } = await srpStep1(account)
148
+ const client = new SRPClientSession(new SRPRoutines(new SRPParameters()))
149
+ const clientRes1 = await client.step1(username, password)
150
+ const clientRes2 = await clientRes1.step2(BigInt(salt), BigInt(pubKey))
151
+ return await step1.step2(clientRes2.A, clientRes2.M1).then(() => true, () => false)
152
+ }
153
+
154
+ // unify get/post parameters, with JSON decoding to not be limited to strings
155
+ export const paramsDecoder: Koa.Middleware = async (ctx, next) => {
156
+ ctx.params = ctx.method === 'POST' ? tryJson(await getReqData(ctx.req))
157
+ : objSameKeys(ctx.query, x => Array.isArray(x) ? x : tryJson(x))
158
+ await next()
159
+ }
160
+
161
+ async function getReqData(req: IncomingMessage): Promise<any> {
162
+ return new Promise((resolve, reject) => {
163
+ let data = ''
164
+ req.on('data', chunk =>
165
+ data += chunk)
166
+ req.on('error', reject)
167
+ req.on('end', () => {
168
+ try {
169
+ resolve(data)
170
+ }
171
+ catch(e) {
172
+ reject(e)
173
+ }
174
+ })
175
+ })
176
+ }
package/src/misc.ts ADDED
@@ -0,0 +1,149 @@
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 { EventEmitter } from 'events'
4
+ import { basename } from 'path'
5
+ import _ from 'lodash'
6
+ import Koa from 'koa'
7
+ import { Connection } from './connections'
8
+ import assert from 'assert'
9
+ export * from './util-http'
10
+ export * from './util-generators'
11
+ export * from './util-files'
12
+ import debounceAsync from './debounceAsync'
13
+ export { debounceAsync }
14
+
15
+ export type Callback<IN=void, OUT=void> = (x:IN) => OUT
16
+ export type Dict<T = any> = Record<string, T>
17
+
18
+ export function enforceFinal(sub:string, s:string) {
19
+ return s.endsWith(sub) ? s : s+sub
20
+ }
21
+
22
+ export function prefix(pre:string, v:string|number, post:string='') {
23
+ return v ? pre+v+post : ''
24
+ }
25
+
26
+ export function setHidden<T, ADD>(dest: T, src: ADD) {
27
+ return Object.defineProperties(dest, objSameKeys(src as any, value => ({
28
+ enumerable: false,
29
+ writable: true,
30
+ value,
31
+ }))) as T & ADD
32
+ }
33
+
34
+ export function objSameKeys<S extends object,VR=any>(src: S, newValue:(value:Truthy<S[keyof S]>, key:keyof S)=>any) {
35
+ return Object.fromEntries(Object.entries(src).map(([k,v]) => [k, newValue(v,k as keyof S)])) as { [K in keyof S]:VR }
36
+ }
37
+
38
+ export function wait(ms: number) {
39
+ return new Promise(res=> setTimeout(res,ms))
40
+ }
41
+
42
+ export function wantArray<T>(x?: void | T | T[]) {
43
+ return x == null ? [] : Array.isArray(x) ? x : [x]
44
+ }
45
+
46
+ export function getOrSet<T>(o: Record<string,T>, k:string, creator:()=>T): T {
47
+ return k in o ? o[k]
48
+ : (o[k] = creator())
49
+ }
50
+
51
+ // 10 chars is 51+bits, 8 is 41+bits
52
+ export function randomId(len = 10): string {
53
+ if (len > 10)
54
+ return randomId(10) + randomId(len - 10)
55
+ return Math.random()
56
+ .toString(36)
57
+ .substring(2, 2+len)
58
+ .replace(/l/g, 'L'); // avoid confusion reading l1
59
+ }
60
+
61
+ type ProcessExitHandler = (signal:string) => any
62
+ const cbs = new Set<ProcessExitHandler>()
63
+ export function onProcessExit(cb: ProcessExitHandler) {
64
+ cbs.add(cb)
65
+ return () => cbs.delete(cb)
66
+ }
67
+ onFirstEvent(process, ['exit', 'SIGQUIT', 'SIGTERM', 'SIGINT', 'SIGHUP'], signal =>
68
+ Promise.allSettled(Array.from(cbs).map(cb => cb(signal))).then(() =>
69
+ process.exit(0)))
70
+
71
+ export function onFirstEvent(emitter:EventEmitter, events: string[], cb: (...args:any[])=> void) {
72
+ let already = false
73
+ for (const e of events)
74
+ emitter.on(e, (...args) => {
75
+ if (already) return
76
+ already = true
77
+ cb(...args)
78
+ })
79
+ }
80
+
81
+ export function pattern2filter(pattern: string){
82
+ const re = new RegExp(_.escapeRegExp(pattern), 'i')
83
+ return (s?:string) =>
84
+ !s || !pattern || re.test(basename(s))
85
+ }
86
+
87
+ type Truthy<T> = T extends false | '' | 0 | null | undefined ? never : T
88
+
89
+ export function truthy<T>(value: T): value is Truthy<T> {
90
+ return Boolean(value)
91
+ }
92
+
93
+ export function onlyTruthy<T>(arr: T[]) {
94
+ return arr.filter(truthy)
95
+ }
96
+
97
+ type PendingPromise<T> = Promise<T> & { resolve: (value: T) => void, reject: (reason?: any) => void }
98
+ export function pendingPromise<T>() {
99
+ let takeOut
100
+ const ret = new Promise<T>((resolve, reject) =>
101
+ takeOut = { resolve, reject })
102
+ return Object.assign(ret, takeOut) as PendingPromise<T>
103
+ }
104
+
105
+ // install multiple handlers and returns a handy 'uninstall' function which requires no parameter. Pass a map {event:handler}
106
+ export function onOff(em: EventEmitter, events: { [eventName:string]: (...args: any[]) => void }) {
107
+ events = { ...events } // avoid later modifications, as we need this later for uninstallation
108
+ for (const [k,cb] of Object.entries(events))
109
+ for (const e of k.split(' '))
110
+ em.on(e, cb)
111
+ return () => {
112
+ for (const [k,cb] of Object.entries(events))
113
+ for (const e of k.split(' '))
114
+ em.off(e, cb)
115
+ }
116
+ }
117
+
118
+ export function objRenameKey(o: Dict | undefined, from: string, to: string) {
119
+ if (!o || !o.hasOwnProperty(from) || from === to) return
120
+ o[to] = o[from]
121
+ delete o[from]
122
+ return true
123
+ }
124
+
125
+ export function typedKeys<T extends {}>(o: T) {
126
+ return Object.keys(o) as (keyof T)[]
127
+ }
128
+
129
+ export function with_<T,RT>(par:T, cb: (par:T) => RT) {
130
+ return cb(par)
131
+ }
132
+
133
+ export function isLocalHost(c: Connection | Koa.Context) {
134
+ const ip = c.socket.remoteAddress // don't use Context.ip as it is subject to proxied ips, and that's no use for localhost detection
135
+ return ip && (ip === '::1' || ip.endsWith('127.0.0.1'))
136
+ }
137
+
138
+ export function same(a: any, b: any) {
139
+ try {
140
+ assert.deepStrictEqual(a, b)
141
+ return true
142
+ }
143
+ catch { return false }
144
+ }
145
+
146
+ export function tryJson(s?: string) {
147
+ try { return s && JSON.parse(s) }
148
+ catch {}
149
+ }