hfs 0.26.6 → 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.3129dad1.js +0 -282
  118. package/admin/assets/index.dcc78777.css +0 -1
  119. package/admin/assets/sha512.e9b1ee42.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 -226
  161. package/src/watchLoad.js +0 -73
  162. package/src/zip.js +0 -69
@@ -0,0 +1,106 @@
1
+ import _ from 'lodash'
2
+ import { Connection, getConnections } from './connections'
3
+ import { pendingPromise, wait } from './misc'
4
+ import { ApiHandlers, SendListReadable } from './apiMiddleware'
5
+ import Koa from 'koa'
6
+ import { totalGot, totalInSpeed, totalOutSpeed, totalSent } from './throttler'
7
+ import { getCurrentUsername } from './perm'
8
+
9
+ const apis: ApiHandlers = {
10
+
11
+ async disconnect({ ip, port, wait }) {
12
+ const match = _.matches({ ip, port })
13
+ const c = getConnections().find(c => match(getConnAddress(c)))
14
+ const waiter = pendingPromise<void>()
15
+ c?.socket.end(waiter.resolve)
16
+ if (wait)
17
+ await waiter
18
+ return { result: Boolean(c) }
19
+ },
20
+
21
+ get_connections({}, ctx) {
22
+ const list = new SendListReadable({ addAtStart: getConnections().map(c => serializeConnection(c)) })
23
+ type Change = Partial<Omit<Connection,'ip'>>
24
+ const throttledUpdate = _.throttle(update, 1000/20) // try to avoid clogging with updates
25
+ const state = Symbol('state') // undefined=added, Timeout=add-pending, false=removed
26
+ return list.events(ctx, {
27
+ connection(conn: Connection) {
28
+ conn[state] = setTimeout(() => add(conn), 100)
29
+ },
30
+ connectionClosed(conn: Connection) {
31
+ if (cancel(conn)) return
32
+ list.remove(serializeConnection(conn, true))
33
+ conn[state] = false
34
+ },
35
+ connectionUpdated(conn: Connection, change: Change) {
36
+ if (!change.ctx)
37
+ return throttledUpdate(conn, change)
38
+
39
+ Object.assign(change, fromCtx(change.ctx))
40
+ change.ctx = undefined
41
+ if (!add(conn))
42
+ throttledUpdate(conn, change)
43
+ },
44
+ })
45
+
46
+ function add(conn: Connection) {
47
+ if (!cancel(conn)) return
48
+ list.add(serializeConnection(conn))
49
+ return true
50
+ }
51
+
52
+ function cancel(conn: Connection) {
53
+ if (!conn[state]) return
54
+ clearTimeout(conn[state])
55
+ conn[state] = undefined
56
+ return true
57
+ }
58
+
59
+ function update(conn: Connection, change: Change) {
60
+ if (conn[state] === false) return
61
+ list.update(serializeConnection(conn, true), change)
62
+ }
63
+
64
+ function serializeConnection(conn: Connection, minimal?:true) {
65
+ const { socket, started, secure } = conn
66
+ return Object.assign(getConnAddress(conn), !minimal && {
67
+ v: (socket.remoteFamily?.endsWith('6') ? 6 : 4),
68
+ got: socket.bytesRead,
69
+ sent: socket.bytesWritten,
70
+ started,
71
+ secure: (secure || undefined) as boolean|undefined, // undefined will save some space once json-ed
72
+ ...fromCtx(conn.ctx),
73
+ })
74
+ }
75
+
76
+ function fromCtx(ctx?: Koa.Context) {
77
+ return ctx && {
78
+ user: getCurrentUsername(ctx),
79
+ archive: ctx.state.archive,
80
+ path: (ctx.fileSource || ctx.state.archive) && ctx.path // only for downloading files
81
+ }
82
+ }
83
+ },
84
+
85
+ async *get_connection_stats() {
86
+ while (1) {
87
+ yield {
88
+ outSpeed: totalOutSpeed,
89
+ inSpeed: totalInSpeed,
90
+ got: totalGot,
91
+ sent: totalSent,
92
+ connections: getConnections().length
93
+ }
94
+ await wait(1000)
95
+ }
96
+ },
97
+ }
98
+
99
+ export default apis
100
+
101
+ function getConnAddress(conn: Connection) {
102
+ return {
103
+ ip: conn.ip,
104
+ port: conn.socket.remotePort,
105
+ }
106
+ }
@@ -0,0 +1,139 @@
1
+ import {
2
+ AvailablePlugin,
3
+ enablePlugins,
4
+ getAvailablePlugins,
5
+ getPluginConfigFields,
6
+ mapPlugins,
7
+ Plugin, pluginsConfig,
8
+ PATH as PLUGINS_PATH, isPluginRunning, enablePlugin, getPluginInfo, setPluginConfig
9
+ } from './plugins'
10
+ import _ from 'lodash'
11
+ import assert from 'assert'
12
+ import { objSameKeys, onOff, wait } from './misc'
13
+ import { ApiHandlers, SendListReadable } from './apiMiddleware'
14
+ import events from './events'
15
+ import { rm } from 'fs/promises'
16
+ import { downloadPlugin, getFolder2repo, getRepoInfo, readOnlinePlugin, searchPlugins } from './github'
17
+
18
+ const apis: ApiHandlers = {
19
+
20
+ get_plugins({}, ctx) {
21
+ const list = new SendListReadable({ addAtStart: [ ...mapPlugins(serialize), ...getAvailablePlugins() ] })
22
+ return list.events(ctx, {
23
+ pluginInstalled: p => list.add(serialize(p)),
24
+ 'pluginStarted pluginStopped pluginUpdated': p => {
25
+ const { id, ...rest } = serialize(p)
26
+ list.update({ id }, rest)
27
+ },
28
+ pluginUninstalled: id => list.remove({ id }),
29
+ })
30
+
31
+ function serialize(p: Readonly<Plugin> | AvailablePlugin) {
32
+ const o = 'getData' in p ? Object.assign(_.pick(p, ['id','started']), p.getData())
33
+ : { ...p } // _.defaults mutates object, and we don't want that
34
+ return _.defaults(o, { started: null, badApi: null }) // nulls should be used to be sure to overwrite previous values,
35
+ }
36
+ },
37
+
38
+ async get_plugin_updates() {
39
+ const list = new SendListReadable()
40
+ setTimeout(async () => {
41
+ for (const [folder, repo] of Object.entries(getFolder2repo()))
42
+ try {
43
+ if (!repo) continue
44
+ const online = await readOnlinePlugin(await getRepoInfo(repo))
45
+ if (!online.apiRequired || online.badApi) continue
46
+ const disk = getPluginInfo(folder)
47
+ if (online.version! > disk.version)
48
+ list.add(online)
49
+ }
50
+ catch (err:any) {
51
+ list.error(err.code || err.message)
52
+ }
53
+ list.close()
54
+ })
55
+ return list
56
+ },
57
+
58
+ async set_plugin({ id, enabled, config }) {
59
+ assert(id, 'id')
60
+ if (enabled !== undefined)
61
+ enablePlugin(id, enabled)
62
+ if (config)
63
+ setPluginConfig(id, config)
64
+ return {}
65
+ },
66
+
67
+ async get_plugin({ id }) {
68
+ return {
69
+ enabled: enablePlugins.get().includes(id),
70
+ config: {
71
+ ...objSameKeys(getPluginConfigFields(id) ||{}, v => v?.defaultValue),
72
+ ...pluginsConfig.get()[id]
73
+ }
74
+ }
75
+ },
76
+
77
+ search_online_plugins({ text }, ctx) {
78
+ return new SendListReadable({
79
+ async doAtStart(list) {
80
+ try {
81
+ const folder2repo = getFolder2repo()
82
+ for await (const pl of searchPlugins(text)) {
83
+ const repo = pl.id
84
+ const folder = _.findKey(folder2repo, x => x === repo)
85
+ const installed = folder && getPluginInfo(folder)
86
+ Object.assign(pl, {
87
+ installed: _.includes(folder2repo, repo),
88
+ update: installed && installed.version < pl.version!,
89
+ })
90
+ list.add(pl)
91
+ // watch for events about this plugin, until this request is closed
92
+ ctx.req.on('close', onOff(events, {
93
+ pluginInstalled: p => {
94
+ if (p.repo === repo)
95
+ list.update({ id: repo }, { installed: true })
96
+ },
97
+ pluginUninstalled: folder => {
98
+ if (repo === getFolder2repo()[folder])
99
+ list.update({ id: repo }, { installed: false })
100
+ },
101
+ pluginUpdated: p => {
102
+ if (p.repo === repo)
103
+ list.update({ id: repo }, { update: p.version < pl.version! })
104
+ },
105
+ ['pluginDownload_' + repo](status) {
106
+ list.update({ id: repo }, { downloading: status ?? null })
107
+ }
108
+ }))
109
+ }
110
+ } catch (err: any) {
111
+ list.error(err.code || err.message)
112
+ }
113
+ list.ready()
114
+ }
115
+ })
116
+ },
117
+
118
+ async download_plugin(pl) {
119
+ const res = await downloadPlugin(pl.id, pl.branch)
120
+ return typeof res === 'string' ? getPluginInfo(res) : res
121
+ },
122
+
123
+ async update_plugin(pl) {
124
+ await downloadPlugin(pl.id, pl.branch, true)
125
+ return {}
126
+ },
127
+
128
+ async uninstall_plugin({ id }) {
129
+ while (isPluginRunning(id)) {
130
+ enablePlugin(id, false)
131
+ await wait(500)
132
+ }
133
+ await rm(PLUGINS_PATH + '/' + id, { recursive: true, force: true })
134
+ return {}
135
+ }
136
+
137
+ }
138
+
139
+ export default apis
package/src/api.vfs.ts ADDED
@@ -0,0 +1,182 @@
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 { getNodeName, nodeIsDirectory, saveVfs, urlToNode, vfs, VfsNode } from './vfs'
4
+ import _ from 'lodash'
5
+ import { stat } from 'fs/promises'
6
+ import { ApiError, ApiHandlers } from './apiMiddleware'
7
+ import { dirname, join, resolve } from 'path'
8
+ import { dirStream, isWindowsDrive, objSameKeys } from './misc'
9
+ import { exec } from 'child_process'
10
+ import { promisify } from 'util'
11
+ import { FORBIDDEN, IS_WINDOWS } from './const'
12
+ import { isMatch } from 'micromatch'
13
+
14
+ type VfsAdmin = {
15
+ type?: string,
16
+ size?: number,
17
+ ctime?: Date,
18
+ mtime?: Date,
19
+ website?: true,
20
+ children?: VfsAdmin[]
21
+ } & Omit<VfsNode, 'type' | 'children'>
22
+
23
+ // to manipulate the tree we need the original node
24
+ async function urlToNodeOriginal(uri: string) {
25
+ const n = await urlToNode(uri)
26
+ return n?.isTemp ? n.original : n
27
+ }
28
+
29
+ const apis: ApiHandlers = {
30
+
31
+ async get_vfs() {
32
+ return { root: vfs && await recur(vfs) }
33
+
34
+ async function recur(node: VfsNode): Promise<VfsAdmin> {
35
+ const dir = await nodeIsDirectory(node)
36
+ const stats: Pick<VfsAdmin, 'size' | 'ctime' | 'mtime'> = {}
37
+ try {
38
+ if (!dir)
39
+ Object.assign(stats, _.pick(await stat(node.source!), ['size', 'ctime', 'mtime']))
40
+ }
41
+ catch {
42
+ stats.size = -1
43
+ }
44
+ if (stats && Number(stats.mtime) === Number(stats.ctime))
45
+ delete stats.mtime
46
+ const isRoot = node === vfs
47
+ return {
48
+ ...stats,
49
+ ...node,
50
+ website: dir && node.source && await stat(join(node.source, 'index.html')).then(() => true, () => undefined)
51
+ || undefined,
52
+ name: isRoot ? undefined : getNodeName(node),
53
+ type: dir ? 'folder' : undefined,
54
+ children: node.children && await Promise.all(node.children.map(recur)),
55
+ }
56
+ }
57
+ },
58
+
59
+ async set_vfs({ uri, props }) {
60
+ const n = await urlToNodeOriginal(uri)
61
+ if (!n)
62
+ return new ApiError(404, 'path not found')
63
+ props = pickProps(props, ['name','source','can_see','can_read','masks','default'])
64
+ props = objSameKeys(props, v => v === null ? undefined : v) // null is a way to serialize undefined, that will restore default values
65
+ if (props.masks && typeof props.masks !== 'object')
66
+ delete props.masks
67
+ Object.assign(n, props)
68
+ if (getNodeName(_.omit(n, ['name'])) === n.name) // name only if necessary
69
+ n.name = undefined
70
+ await saveVfs()
71
+ return n
72
+ },
73
+
74
+ async add_vfs({ under, source, name }) {
75
+ const n = under ? await urlToNodeOriginal(under) : vfs
76
+ if (!n)
77
+ return new ApiError(404, 'invalid under')
78
+ if (n.isTemp || !await nodeIsDirectory(n))
79
+ return new ApiError(FORBIDDEN, 'invalid under')
80
+ if (isWindowsDrive(source))
81
+ source += '\\' // slash must be included, otherwise it will refer to the cwd of that drive
82
+ const a = n.children || (n.children = [])
83
+ if (source && a.find(x => x.source === source))
84
+ return new ApiError(409, 'already present')
85
+ a.unshift({ source, name })
86
+ await saveVfs()
87
+ return {}
88
+ },
89
+
90
+ async del_vfs({ uris }) {
91
+ if (!uris || !Array.isArray(uris))
92
+ return new ApiError(400, 'invalid uris')
93
+ return {
94
+ errors: await Promise.all(uris.map(async uri => {
95
+ if (typeof uri !== 'string')
96
+ return 400
97
+ const node = await urlToNodeOriginal(uri)
98
+ if (!node)
99
+ return 404
100
+ const parent = dirname(uri)
101
+ const parentNode = await urlToNodeOriginal(parent)
102
+ if (!parentNode)
103
+ return FORBIDDEN
104
+ const { children } = parentNode
105
+ if (!children) // shouldn't happen
106
+ return 500
107
+ const idx = children.indexOf(node)
108
+ children.splice(idx, 1)
109
+ saveVfs()
110
+ return 0 // error code 0 is OK
111
+ }))
112
+ }
113
+ },
114
+
115
+ get_cwd() {
116
+ return { path: process.cwd() }
117
+ },
118
+
119
+ async resolve_path({ path, closestFolder }) {
120
+ path = resolve(path)
121
+ if (closestFolder)
122
+ while (path && !await stat(path).then(x => x.isDirectory(), () => 0))
123
+ path = dirname(path)
124
+ return { path }
125
+ },
126
+
127
+ async *ls({ path, files=true, fileMask }, ctx) {
128
+ if (!path && IS_WINDOWS) {
129
+ try {
130
+ for (const n of await getDrives())
131
+ yield { add: { n, k: 'd' } }
132
+ }
133
+ catch(error) {
134
+ console.debug(error)
135
+ }
136
+ return
137
+ }
138
+ try {
139
+ path = isWindowsDrive(path) ? path + '\\' : resolve(path || '/')
140
+ for await (const name of dirStream(path)) {
141
+ if (ctx.req.aborted)
142
+ return
143
+ try {
144
+ const stats = await stat(join(path, name))
145
+ if (stats.isFile())
146
+ if (!files || fileMask && !isMatch(name, fileMask))
147
+ continue
148
+ yield {
149
+ add: {
150
+ n: name,
151
+ s: stats.size,
152
+ c: stats.ctime,
153
+ m: stats.mtime,
154
+ k: stats.isDirectory() ? 'd' : undefined,
155
+ }
156
+ }
157
+ }
158
+ catch {} // just ignore entries we can't stat
159
+ }
160
+ } catch (e: any) {
161
+ yield { error: e.code || e.message || String(e) }
162
+ }
163
+ }
164
+
165
+ }
166
+
167
+ export default apis
168
+
169
+ // pick only selected props, and consider null and empty string as undefined
170
+ function pickProps(o: any, keys: string[]) {
171
+ const ret: any = {}
172
+ if (o && typeof o === 'object')
173
+ for (const k of keys)
174
+ if (k in o)
175
+ ret[k] = o[k] === null || o[k] === '' ? undefined : o[k]
176
+ return ret
177
+ }
178
+
179
+ async function getDrives() {
180
+ const { stdout } = await promisify(exec)('wmic logicaldisk get name')
181
+ return stdout.split('\n').slice(1).map(x => x.trim()).filter(Boolean)
182
+ }
@@ -0,0 +1,124 @@
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 createSSE from './sse'
5
+ import { Readable } from 'stream'
6
+ import { asyncGeneratorToReadable, onOff } from './misc'
7
+ import events from './events'
8
+ import { UNAUTHORIZED } from './const'
9
+ import _, { DebouncedFunc } from 'lodash'
10
+
11
+ export class ApiError extends Error {
12
+ constructor(public status:number, message?:string | Error) {
13
+ super(typeof message === 'string' ? message : message?.message)
14
+ }
15
+ }
16
+ type ApiHandlerResult = Record<string,any> | ApiError | Readable | AsyncGenerator<any>
17
+ export type ApiHandler = (params:any, ctx:Koa.Context) => ApiHandlerResult | Promise<ApiHandlerResult>
18
+ export type ApiHandlers = Record<string, ApiHandler>
19
+
20
+ export function apiMiddleware(apis: ApiHandlers) : Koa.Middleware {
21
+ return async (ctx) => {
22
+ const { params } = ctx
23
+ console.debug('API', ctx.method, ctx.path, { ...params })
24
+ if (!apis.hasOwnProperty(ctx.path)) {
25
+ ctx.body = 'invalid api'
26
+ return ctx.status = 404
27
+ }
28
+ const csrf = ctx.cookies.get('csrf')
29
+ // we don't rely on SameSite cookie option because it's https-only
30
+ let res = csrf && csrf !== params.csrf ? new ApiError(UNAUTHORIZED, 'csrf')
31
+ : await apis[ctx.path](params || {}, ctx)
32
+ if (isAsyncGenerator(res))
33
+ res = asyncGeneratorToReadable(res)
34
+ if (res instanceof Readable) { // Readable, we'll go SSE-mode
35
+ res.pipe(createSSE(ctx))
36
+ const stillRes = res // satisfy ts
37
+ ctx.req.on('close', () => // by closing the generated stream, creator of the stream will know the request is over without having to access anything else
38
+ stillRes.destroy())
39
+ return
40
+ }
41
+ if (res instanceof ApiError) {
42
+ ctx.body = res.message
43
+ return ctx.status = res.status
44
+ }
45
+ if (res instanceof Error) { // generic exception
46
+ ctx.body = String(res)
47
+ return ctx.status = 400
48
+ }
49
+ ctx.body = res
50
+ }
51
+ }
52
+
53
+ function isAsyncGenerator(x: any): x is AsyncGenerator {
54
+ return typeof (x as AsyncGenerator)?.next === 'function'
55
+ }
56
+
57
+ // offer an api for a generic dynamic list. Suitable to be the result of an api.
58
+ type SendListFunc<T> = (list:SendListReadable<T>) => void
59
+ export class SendListReadable<T> extends Readable {
60
+ protected lastError: string | number | undefined
61
+ protected buffer: any[] = []
62
+ protected processBuffer: DebouncedFunc<any>
63
+ constructor({ addAtStart, doAtStart, bufferTime }:{ bufferTime?: number, addAtStart?: T[], doAtStart?: SendListFunc<T> }={}) {
64
+ super({ objectMode: true, read(){} })
65
+ if (!bufferTime)
66
+ bufferTime = 100
67
+ this.processBuffer = _.debounce(() => {
68
+ this.push(this.buffer)
69
+ this.buffer = []
70
+ }, bufferTime, { maxWait: bufferTime })
71
+ this.on('end', () =>
72
+ this.destroy())
73
+ if (doAtStart)
74
+ setTimeout(() => doAtStart(this)) // work later, when list object has been received by Koa
75
+ if (addAtStart) {
76
+ for (const x of addAtStart)
77
+ this.add(x)
78
+ this.ready()
79
+ }
80
+ }
81
+ protected _push(rec: any) {
82
+ this.buffer.push(rec)
83
+ if (this.buffer.length > 10_000) // hard limit
84
+ this.processBuffer.flush()
85
+ else
86
+ this.processBuffer()
87
+ }
88
+ add(rec: T | T[]) {
89
+ this._push({ add: rec })
90
+ }
91
+ remove(key: Partial<T>) {
92
+ this._push({ remove: [key] })
93
+ }
94
+ update(search: Partial<T>, change: Partial<T>) {
95
+ this._push({ update:[{ search, change }] })
96
+ }
97
+ ready() { // useful to indicate the end of an initial phase, but we leave open for updates
98
+ this._push('ready')
99
+ }
100
+ custom(data: any) {
101
+ this._push(data)
102
+ }
103
+ error(msg: NonNullable<typeof this.lastError>, close=false) {
104
+ this._push({ error: msg })
105
+ this.lastError = msg
106
+ if (close)
107
+ this.close()
108
+ }
109
+ getLastError() {
110
+ return this.lastError
111
+ }
112
+ close() {
113
+ this.processBuffer.flush()
114
+ this.push(null)
115
+ }
116
+ events(ctx: Koa.Context, eventMap: Parameters<typeof onOff>[1]) {
117
+ const off = onOff(events, eventMap)
118
+ ctx.res.once('close', off)
119
+ return this
120
+ }
121
+ isClosed() {
122
+ return this.destroyed
123
+ }
124
+ }
package/src/block.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { defineConfig } from './config'
2
+ import { getConnections, normalizeIp } from './connections'
3
+ import { onlyTruthy, with_ } from './misc'
4
+ import cidr from 'cidr-tools'
5
+ import _ from 'lodash'
6
+ import { Socket } from 'net'
7
+
8
+ defineConfig<string[]>('block', []).sub(rules => {
9
+ compileBlock(rules)
10
+ for (const { socket, ip } of getConnections())
11
+ applyBlock(socket, ip)
12
+ })
13
+
14
+ type BlockFun = (x: string) => boolean
15
+ let blockFunctions: BlockFun[] = [] // "compiled" versions of the rules in config.block
16
+
17
+ function compileBlock(rules: any) {
18
+ blockFunctions = !Array.isArray(rules) ? []
19
+ : onlyTruthy(rules.map(rule => !rule ? null
20
+ : with_(rule.ip, ip => typeof ip !== 'string' ? null
21
+ : ip.includes('/') ? x => cidr.contains(ip, x)
22
+ : ip.includes('*') ? with_(ipMask2regExp(ip), re => x => re.test(x) )
23
+ : x => x === ip
24
+ )
25
+ ))
26
+
27
+ function ipMask2regExp(ipMask: string) {
28
+ return new RegExp(_.escapeRegExp(ipMask).replace(/\\\*/g, '.*'))
29
+ }
30
+ }
31
+
32
+ export function applyBlock(socket: Socket, ip=normalizeIp(socket.remoteAddress||'')) {
33
+ if (ip && blockFunctions.find(rule => rule(ip)))
34
+ return socket.destroy()
35
+ }