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.
- package/admin/.DS_Store +0 -0
- package/admin/.eslintrc +8 -0
- package/admin/.gitignore +23 -0
- package/admin/index.html +1 -3
- package/admin/package.json +67 -0
- package/admin/{logo.svg → public/logo.svg} +0 -0
- package/admin/src/AccountForm.ts +92 -0
- package/admin/src/AccountsPage.ts +143 -0
- package/admin/src/App.ts +83 -0
- package/admin/src/ArrayField.ts +84 -0
- package/admin/src/ConfigPage.ts +279 -0
- package/admin/src/FileField.ts +52 -0
- package/admin/src/FileForm.ts +148 -0
- package/admin/src/FilePicker.ts +166 -0
- package/admin/src/HomePage.ts +96 -0
- package/admin/src/InstalledPlugins.ts +158 -0
- package/admin/src/LoginRequired.ts +75 -0
- package/admin/src/LogoutPage.ts +27 -0
- package/admin/src/LogsPage.ts +75 -0
- package/admin/src/MainMenu.ts +74 -0
- package/admin/src/MenuButton.ts +38 -0
- package/admin/src/MonitorPage.ts +200 -0
- package/admin/src/OnlinePlugins.ts +101 -0
- package/admin/src/PermField.ts +80 -0
- package/admin/src/PluginsPage.ts +27 -0
- package/admin/src/VfsMenuBar.ts +58 -0
- package/admin/src/VfsPage.ts +124 -0
- package/admin/src/VfsTree.ts +95 -0
- package/admin/src/addFiles.ts +59 -0
- package/admin/src/api.ts +246 -0
- package/admin/src/dialog.ts +203 -0
- package/admin/src/index.css +21 -0
- package/admin/src/index.ts +10 -0
- package/admin/src/md.ts +31 -0
- package/admin/src/misc.ts +141 -0
- package/admin/src/react-app-env.d.ts +1 -0
- package/admin/src/reportWebVitals.ts +15 -0
- package/admin/src/setupTests.ts +5 -0
- package/admin/src/state.ts +40 -0
- package/admin/src/theme.ts +37 -0
- package/admin/tsconfig.json +26 -0
- package/admin/vite.config.ts +32 -0
- package/frontend/.DS_Store +0 -0
- package/frontend/.eslintrc +8 -0
- package/frontend/.gitignore +23 -0
- package/frontend/index.html +1 -3
- package/frontend/package.json +51 -0
- package/frontend/{fontello.css → public/fontello.css} +0 -0
- package/frontend/{fontello.woff2 → public/fontello.woff2} +0 -0
- package/frontend/src/App.ts +25 -0
- package/frontend/src/Breadcrumbs.ts +43 -0
- package/frontend/src/BrowseFiles.ts +141 -0
- package/frontend/src/Head.ts +45 -0
- package/frontend/src/UserPanel.ts +52 -0
- package/frontend/src/api.ts +78 -0
- package/frontend/src/components.ts +54 -0
- package/frontend/src/dialog.css +76 -0
- package/frontend/src/dialog.ts +105 -0
- package/frontend/src/icons.ts +46 -0
- package/frontend/src/index.scss +307 -0
- package/frontend/src/index.ts +10 -0
- package/frontend/src/login.ts +50 -0
- package/frontend/src/menu.ts +188 -0
- package/frontend/src/misc.ts +54 -0
- package/frontend/src/options.ts +52 -0
- package/frontend/src/react-app-env.d.ts +1 -0
- package/frontend/src/reportWebVitals.ts +15 -0
- package/frontend/src/setupTests.ts +5 -0
- package/frontend/src/state.ts +82 -0
- package/frontend/src/useAuthorized.ts +17 -0
- package/frontend/src/useFetchList.ts +144 -0
- package/frontend/src/useTheme.ts +23 -0
- package/frontend/tsconfig.json +26 -0
- package/frontend/vite.config.ts +21 -0
- package/package.json +2 -1
- package/plugins/vhosting/plugin.js +1 -1
- package/src/QuickZipStream.ts +279 -0
- package/src/ThrottledStream.ts +98 -0
- package/src/adminApis.ts +161 -0
- package/src/api.accounts.ts +78 -0
- package/src/api.auth.ts +131 -0
- package/src/api.file_list.ts +102 -0
- package/src/api.helpers.ts +30 -0
- package/src/api.monitor.ts +106 -0
- package/src/api.plugins.ts +139 -0
- package/src/api.vfs.ts +182 -0
- package/src/apiMiddleware.ts +124 -0
- package/src/block.ts +35 -0
- package/src/commands.ts +122 -0
- package/src/config.ts +166 -0
- package/src/connections.ts +60 -0
- package/src/const.ts +57 -0
- package/src/crypt.ts +16 -0
- package/src/debounceAsync.ts +51 -0
- package/src/events.ts +6 -0
- package/src/frontEndApis.ts +17 -0
- package/src/github.ts +102 -0
- package/src/index.ts +53 -0
- package/src/listen.ts +220 -0
- package/src/log.ts +128 -0
- package/src/middlewares.ts +176 -0
- package/src/misc.ts +149 -0
- package/src/pbkdf2.ts +83 -0
- package/src/perm.ts +194 -0
- package/src/plugins.ts +342 -0
- package/src/serveFile.ts +104 -0
- package/src/serveGuiFiles.ts +95 -0
- package/src/sse.ts +29 -0
- package/src/throttler.ts +106 -0
- package/src/update.ts +67 -0
- package/src/util-files.ts +137 -0
- package/src/util-generators.ts +29 -0
- package/src/util-http.ts +29 -0
- package/src/vfs.ts +258 -0
- package/src/watchLoad.ts +75 -0
- package/src/zip.ts +69 -0
- package/admin/assets/index.3129dad1.js +0 -282
- package/admin/assets/index.dcc78777.css +0 -1
- package/admin/assets/sha512.e9b1ee42.js +0 -8
- package/frontend/assets/index.1151988f.js +0 -85
- package/frontend/assets/index.93366732.css +0 -1
- package/frontend/assets/sha512.bb881250.js +0 -8
- package/src/QuickZipStream.js +0 -285
- package/src/ThrottledStream.js +0 -93
- package/src/adminApis.js +0 -169
- package/src/api.accounts.js +0 -59
- package/src/api.auth.js +0 -130
- package/src/api.file_list.js +0 -103
- package/src/api.helpers.js +0 -32
- package/src/api.monitor.js +0 -102
- package/src/api.plugins.js +0 -127
- package/src/api.vfs.js +0 -164
- package/src/apiMiddleware.js +0 -136
- package/src/block.js +0 -33
- package/src/commands.js +0 -124
- package/src/config.js +0 -168
- package/src/connections.js +0 -57
- package/src/const.js +0 -83
- package/src/crypt.js +0 -21
- package/src/debounceAsync.js +0 -48
- package/src/events.js +0 -9
- package/src/frontEndApis.js +0 -38
- package/src/github.js +0 -102
- package/src/index.js +0 -55
- package/src/listen.js +0 -235
- package/src/log.js +0 -137
- package/src/middlewares.js +0 -154
- package/src/misc.js +0 -160
- package/src/pbkdf2.js +0 -74
- package/src/perm.js +0 -176
- package/src/plugins.js +0 -343
- package/src/serveFile.js +0 -104
- package/src/serveGuiFiles.js +0 -113
- package/src/sse.js +0 -29
- package/src/throttler.js +0 -91
- package/src/update.js +0 -69
- package/src/util-files.js +0 -148
- package/src/util-generators.js +0 -30
- package/src/util-http.js +0 -30
- package/src/vfs.js +0 -226
- package/src/watchLoad.js +0 -73
- 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
|
+
}
|