hfs 0.26.8 → 0.27.2
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/README.md +15 -2
- package/admin/assets/index-509bb1d6.js +415 -0
- package/admin/assets/index-60a380a7.css +1 -0
- package/admin/assets/sha512-738f0943.js +8 -0
- package/admin/index.html +3 -1
- package/admin/{public/logo.svg → logo.svg} +0 -0
- package/frontend/assets/index-6e178dfd.css +1 -0
- package/frontend/assets/index-aea7654e.js +85 -0
- package/frontend/assets/sha512-bf915587.js +8 -0
- package/frontend/{public/fontello.css → fontello.css} +0 -0
- package/frontend/{public/fontello.woff2 → fontello.woff2} +0 -0
- package/frontend/index.html +4 -2
- package/package.json +2 -6
- package/plugins/vhosting/plugin.js +23 -20
- package/src/QuickZipStream.js +285 -0
- package/src/ThrottledStream.js +93 -0
- package/src/adminApis.js +169 -0
- package/src/api.accounts.js +59 -0
- package/src/api.auth.js +128 -0
- package/src/api.file_list.js +110 -0
- package/src/api.helpers.js +32 -0
- package/src/api.monitor.js +104 -0
- package/src/api.plugins.js +128 -0
- package/src/api.vfs.js +167 -0
- package/src/apiMiddleware.js +123 -0
- package/src/block.js +34 -0
- package/src/commands.js +125 -0
- package/src/config.js +168 -0
- package/src/connections.js +57 -0
- package/src/const.js +94 -0
- package/src/crypt.js +21 -0
- package/src/debounceAsync.js +49 -0
- package/src/events.js +9 -0
- package/src/frontEndApis.js +38 -0
- package/src/github.js +104 -0
- package/src/index.js +57 -0
- package/src/listen.js +235 -0
- package/src/log.js +137 -0
- package/src/middlewares.js +195 -0
- package/src/misc.js +160 -0
- package/src/pbkdf2.js +74 -0
- package/src/perm.js +183 -0
- package/src/plugins.js +343 -0
- package/src/serveFile.js +105 -0
- package/src/serveGuiFiles.js +113 -0
- package/src/sse.js +30 -0
- package/src/throttler.js +91 -0
- package/src/update.js +70 -0
- package/src/util-files.js +163 -0
- package/src/util-generators.js +31 -0
- package/src/util-http.js +32 -0
- package/src/vfs.js +232 -0
- package/src/watchLoad.js +73 -0
- package/src/zip.js +73 -0
- package/admin/.DS_Store +0 -0
- package/admin/.eslintrc +0 -8
- package/admin/.gitignore +0 -23
- package/admin/package.json +0 -67
- package/admin/src/AccountForm.ts +0 -92
- package/admin/src/AccountsPage.ts +0 -143
- package/admin/src/App.ts +0 -83
- package/admin/src/ArrayField.ts +0 -84
- package/admin/src/ConfigPage.ts +0 -279
- package/admin/src/FileField.ts +0 -52
- package/admin/src/FileForm.ts +0 -148
- package/admin/src/FilePicker.ts +0 -166
- package/admin/src/HomePage.ts +0 -96
- package/admin/src/InstalledPlugins.ts +0 -158
- package/admin/src/LoginRequired.ts +0 -75
- package/admin/src/LogoutPage.ts +0 -27
- package/admin/src/LogsPage.ts +0 -75
- package/admin/src/MainMenu.ts +0 -74
- package/admin/src/MenuButton.ts +0 -38
- package/admin/src/MonitorPage.ts +0 -200
- package/admin/src/OnlinePlugins.ts +0 -101
- package/admin/src/PermField.ts +0 -80
- package/admin/src/PluginsPage.ts +0 -27
- package/admin/src/VfsMenuBar.ts +0 -58
- package/admin/src/VfsPage.ts +0 -124
- package/admin/src/VfsTree.ts +0 -95
- package/admin/src/addFiles.ts +0 -59
- package/admin/src/api.ts +0 -246
- package/admin/src/dialog.ts +0 -203
- package/admin/src/index.css +0 -21
- package/admin/src/index.ts +0 -10
- package/admin/src/md.ts +0 -31
- package/admin/src/misc.ts +0 -141
- package/admin/src/react-app-env.d.ts +0 -1
- package/admin/src/reportWebVitals.ts +0 -15
- package/admin/src/setupTests.ts +0 -5
- package/admin/src/state.ts +0 -40
- package/admin/src/theme.ts +0 -37
- package/admin/tsconfig.json +0 -26
- package/admin/vite.config.ts +0 -32
- package/frontend/.DS_Store +0 -0
- package/frontend/.eslintrc +0 -8
- package/frontend/.gitignore +0 -23
- package/frontend/package.json +0 -51
- package/frontend/src/App.ts +0 -25
- package/frontend/src/Breadcrumbs.ts +0 -43
- package/frontend/src/BrowseFiles.ts +0 -141
- package/frontend/src/Head.ts +0 -45
- package/frontend/src/UserPanel.ts +0 -52
- package/frontend/src/api.ts +0 -78
- package/frontend/src/components.ts +0 -54
- package/frontend/src/dialog.css +0 -76
- package/frontend/src/dialog.ts +0 -105
- package/frontend/src/icons.ts +0 -46
- package/frontend/src/index.scss +0 -307
- package/frontend/src/index.ts +0 -10
- package/frontend/src/login.ts +0 -50
- package/frontend/src/menu.ts +0 -188
- package/frontend/src/misc.ts +0 -54
- package/frontend/src/options.ts +0 -52
- package/frontend/src/react-app-env.d.ts +0 -1
- package/frontend/src/reportWebVitals.ts +0 -15
- package/frontend/src/setupTests.ts +0 -5
- package/frontend/src/state.ts +0 -82
- package/frontend/src/useAuthorized.ts +0 -17
- package/frontend/src/useFetchList.ts +0 -144
- package/frontend/src/useTheme.ts +0 -23
- package/frontend/tsconfig.json +0 -26
- package/frontend/vite.config.ts +0 -21
- package/src/QuickZipStream.ts +0 -279
- package/src/ThrottledStream.ts +0 -98
- package/src/adminApis.ts +0 -161
- package/src/api.accounts.ts +0 -78
- package/src/api.auth.ts +0 -131
- package/src/api.file_list.ts +0 -102
- package/src/api.helpers.ts +0 -30
- package/src/api.monitor.ts +0 -106
- package/src/api.plugins.ts +0 -139
- package/src/api.vfs.ts +0 -182
- package/src/apiMiddleware.ts +0 -124
- package/src/block.ts +0 -35
- package/src/commands.ts +0 -122
- package/src/config.ts +0 -166
- package/src/connections.ts +0 -60
- package/src/const.ts +0 -57
- package/src/crypt.ts +0 -16
- package/src/debounceAsync.ts +0 -51
- package/src/events.ts +0 -6
- package/src/frontEndApis.ts +0 -17
- package/src/github.ts +0 -102
- package/src/index.ts +0 -53
- package/src/listen.ts +0 -220
- package/src/log.ts +0 -128
- package/src/middlewares.ts +0 -176
- package/src/misc.ts +0 -149
- package/src/pbkdf2.ts +0 -83
- package/src/perm.ts +0 -194
- package/src/plugins.ts +0 -342
- package/src/serveFile.ts +0 -104
- package/src/serveGuiFiles.ts +0 -95
- package/src/sse.ts +0 -29
- package/src/throttler.ts +0 -106
- package/src/update.ts +0 -67
- package/src/util-files.ts +0 -137
- package/src/util-generators.ts +0 -29
- package/src/util-http.ts +0 -29
- package/src/vfs.ts +0 -258
- package/src/watchLoad.ts +0 -75
- package/src/zip.ts +0 -69
package/src/api.plugins.ts
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,182 +0,0 @@
|
|
|
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
|
-
}
|
package/src/apiMiddleware.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
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
|
-
}
|
package/src/commands.ts
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { addAccount, getAccount, updateAccount } from './perm'
|
|
2
|
-
import { getConfig, getConfigDefinition, setConfig } from './config'
|
|
3
|
-
import _ from 'lodash'
|
|
4
|
-
import { getUpdate, update } from './update'
|
|
5
|
-
import { openAdmin } from './listen'
|
|
6
|
-
import yaml from 'yaml'
|
|
7
|
-
import { BUILD_TIMESTAMP, VERSION } from './const'
|
|
8
|
-
import { createInterface } from 'readline'
|
|
9
|
-
|
|
10
|
-
try {
|
|
11
|
-
/*
|
|
12
|
-
is this try-block useful in case the stdin is unavailable?
|
|
13
|
-
Not sure, but someone reported a problem using nohup https://github.com/rejetto/hfs/issues/74
|
|
14
|
-
and I've found this example try-catching https://github.com/DefinitelyTyped/DefinitelyTyped/blob/dda83a906914489e09ca28afea12948529015d4a/types/node/readline.d.ts#L489
|
|
15
|
-
*/
|
|
16
|
-
createInterface({ input: process.stdin }).on('line', parseCommandLine)
|
|
17
|
-
console.log(`HINT: type "help" for help`)
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
console.log("console commands not available")
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function parseCommandLine(line: string) {
|
|
24
|
-
if (!line) return
|
|
25
|
-
const [name, ...params] = line.trim().split(/ +/)
|
|
26
|
-
const cmd = (commands as any)[name]
|
|
27
|
-
if (!cmd)
|
|
28
|
-
return console.error("cannot understand entered command, try 'help'")
|
|
29
|
-
if (cmd.cb.length > params.length)
|
|
30
|
-
return console.error("insufficient parameters, expected: " + cmd.params)
|
|
31
|
-
cmd.cb(...params).then(() =>console.log("+++ command executed"),
|
|
32
|
-
(err: any) => {
|
|
33
|
-
if (typeof err === 'string')
|
|
34
|
-
console.error("command failed:", err)
|
|
35
|
-
else
|
|
36
|
-
throw err
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const commands = {
|
|
41
|
-
help: {
|
|
42
|
-
params: '',
|
|
43
|
-
async cb() {
|
|
44
|
-
console.log("supported commands:",
|
|
45
|
-
..._.map(commands, ({ params }, name) =>
|
|
46
|
-
'\n - ' + name + ' ' + params))
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
'show-admin': {
|
|
50
|
-
params: '',
|
|
51
|
-
cb(){
|
|
52
|
-
openAdmin()
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
'create-admin': {
|
|
56
|
-
params: '<password> [<username>=admin]',
|
|
57
|
-
async cb(password: string, username='admin') {
|
|
58
|
-
if (getAccount(username))
|
|
59
|
-
throw `user ${username} already exists`
|
|
60
|
-
const acc = addAccount(username, { admin: true })
|
|
61
|
-
await updateAccount(acc!, acc => {
|
|
62
|
-
acc.password = password
|
|
63
|
-
})
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
|
-
'change-password': {
|
|
67
|
-
params: '<user> <password>',
|
|
68
|
-
async cb(user: string, password: string) {
|
|
69
|
-
const acc = getAccount(user)
|
|
70
|
-
if (!acc)
|
|
71
|
-
throw "user doesn't exist"
|
|
72
|
-
await updateAccount(acc!, acc => {
|
|
73
|
-
acc.password = password
|
|
74
|
-
})
|
|
75
|
-
}
|
|
76
|
-
},
|
|
77
|
-
config: {
|
|
78
|
-
params: '<key> <value>',
|
|
79
|
-
async cb(key: string, value: string) {
|
|
80
|
-
const conf = getConfigDefinition(key)
|
|
81
|
-
if (!conf)
|
|
82
|
-
throw "specified key doesn't exist"
|
|
83
|
-
let v: any = value
|
|
84
|
-
try { v = JSON.parse(v) }
|
|
85
|
-
catch {}
|
|
86
|
-
setConfig({ [key]: v })
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
'show-config': {
|
|
90
|
-
params: '<key>',
|
|
91
|
-
async cb(key: string) {
|
|
92
|
-
const conf = getConfigDefinition(key)
|
|
93
|
-
if (!conf)
|
|
94
|
-
throw "specified key doesn't exist"
|
|
95
|
-
console.log(yaml.stringify(getConfig(key), { lineWidth:1000 }).trim())
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
quit: {
|
|
99
|
-
params: '',
|
|
100
|
-
async cb() {
|
|
101
|
-
process.exit(0)
|
|
102
|
-
}
|
|
103
|
-
},
|
|
104
|
-
update: {
|
|
105
|
-
params: '',
|
|
106
|
-
cb: update
|
|
107
|
-
},
|
|
108
|
-
'check-update': {
|
|
109
|
-
params: '',
|
|
110
|
-
async cb() {
|
|
111
|
-
const update = await getUpdate()
|
|
112
|
-
console.log("new version available", update.name)
|
|
113
|
-
}
|
|
114
|
-
},
|
|
115
|
-
version: {
|
|
116
|
-
params: '',
|
|
117
|
-
async cb() {
|
|
118
|
-
console.log(VERSION)
|
|
119
|
-
console.log(BUILD_TIMESTAMP)
|
|
120
|
-
}
|
|
121
|
-
},
|
|
122
|
-
}
|