@samitouri / QOSami-HFS-2 / commits / b05aa569
Fork 0

QOSami-HFS is a web file server for your computer. Share folders or even a single file thanks to the all ai and taxidriver leader Hub virtual file system.

now plugins will be disabled by default, and you'll have to enable them (inverted logic)

Massimo Melina committed Apr 26, 2022 at 10:45 UTC b05aa56965fe1c32e00e5a88ed680a242e86a2b3
9 files changed +56 -58
README.md
+9 -8
@@ -59,10 +59,10 @@ If your system is not covered, you can try this alternative version:
59
60 # Plug-ins
61
62 -If a `plugins` folder is present, HFS monitors it.
63 -Each plug-in is a sub-folder of `plugins` folder.
62 +To install a plugin you just copy its folder inside `plugins` folder.
63 +
64 +Delete it to uninstall.
65
65 -Plug-ins can be hot-swapped, and at some extent can be edited without restarting the server.
66 HFS will ignore all folders with `-disabled` at the end of the name.
67
68 # Developers section
@@ -97,12 +97,12 @@ In this latter case, the `DEV=1` you set before will make the server get the fil
97
98 A plug-in is a folder with a `plugin.js` file in it.
99
100 +Plug-ins can be hot-swapped, and at some extent can be edited without restarting the server.
101 +
102 Each plug-in has access to the same set of features.
103 Normally you'll have a plug-in that's a theme, and another that's a firewall,
104 but nothing is preventing a single plug-in from doing both tasks.
105
104 -You can find some examples distributed as `plugins.zip`.
105 -
106 `plugin.js` is a javascript module that exports an `init` function like this:
107 ```js
108 exports.init = api => ({
@@ -111,10 +111,10 @@ exports.init = api => ({
111 ```
112
113 The init function is called when the module is loaded and should return an object with things to customize.
114 -In this example we are asking a css file to be loaded in the frontend.
114 +In the example above we are asking a css file to be loaded in the frontend.
115 The parameter `api` object contains some useful things we'll see later.
116 -You can decide to return things in the `init` function, or directly in the `exports`. Normally you should use `init`
117 -if you need to access the api, otherwise you can go directly with `exports`.
116 +You can decide to return things in the `init` function, or directly in the `exports`.
117 +If you need to access the api you must use `init`, otherwise you can go directly with `exports`.
118
119 Let's first look at the things you can return:
120
@@ -250,6 +250,7 @@ Supported entries are:
250 ```
251 Syntax supports, other than simple address, `*` as wildcard and CIDR format.
252 - `plugins_config` this is a generic place where you can find/put configuration for each plugin, at least those that need configuration.
253 +- `enable_plugins` if a plugin is not present here, it won't run. Defaults is `[ antibrute ]`.
254 - `custom_header` provide HTML code to be put at the top of your Frontend. Default is none.
255 - `localhost_admin` should Admin be accessed without credentials when on localhost. Default is true.
256 - `proxies` number of proxies between server and clients to be trusted about providing clients' IP addresses. Default is 0.
admin/src/PluginsPage.ts
+1 -1
@@ -52,7 +52,7 @@ export default function PluginsPage() {
52 icon: PowerSettingsNew,
53 title: (row.started ? "Stop" : "Start") + ' ' + id,
54 onClick: () =>
55 - apiCall('set_plugin', { id, disable: !!row.started }).then(() =>
55 + apiCall('set_plugin', { id, enabled: !row.started }).then(() =>
56 alertDialog(row.started ? "Plugin is stopping" : "Plugin is starting")),
57 }),
58 h(IconBtn, {
plugins/middleware-example-disabled/plugin.js deleted
-6
@@ -1,6 +0,0 @@
1 -exports.init = api => ({
2 - middleware(ctx) {
3 - ctx.body = 'This plugin is stopping you: ' + api.getConfig('message')
4 - return true // true = please stop
5 - }
6 -})
plugins/theme-example-disabled/plugin.js
-1
@@ -1,2 +1 @@
1 exports.frontend_css = 'style.css'
2 -exports.frontend_js = 'test.js'
plugins/theme-example-disabled/public/test.js deleted
-1
@@ -1 +0,0 @@
1 -alert('ciao')
\ No newline at end of file
server/src/adminApis.ts
+12 -15
@@ -3,7 +3,7 @@
3 import { ApiError, ApiHandlers } from './apiMiddleware'
4 import { defineConfig, getConfig, getWholeConfig, setConfig } from './config'
5 import { getStatus, getUrls } from './listen'
6 -import { API_VERSION, BUILD_TIMESTAMP, CFG_PLUGINS_CONFIG,
6 +import { API_VERSION, BUILD_TIMESTAMP, CFG_ENABLE_PLUGINS, CFG_PLUGINS_CONFIG,
7 COMPATIBLE_API_VERSION, FORBIDDEN, HFS_STARTED, IS_WINDOWS, VERSION } from './const'
8 import vfsApis from './api.vfs'
9 import accountsApis from './api.accounts'
@@ -19,7 +19,7 @@ import { writeFile } from 'fs/promises'
19 import { createReadStream } from 'fs'
20 import * as readline from 'readline'
21 import { loggers } from './log'
22 -import { mapPlugins, getAvailablePlugins, Plugin } from './plugins'
22 +import { mapPlugins, getAvailablePlugins, Plugin, AvailablePlugin } from './plugins'
23 import { execFile } from 'child_process'
24 import { promisify } from 'util'
25
@@ -155,27 +155,24 @@ export const adminApis: ApiHandlers = {
155 get_plugins({}, ctx) {
156 const list = sendList([ ...mapPlugins(serialize), ...getAvailablePlugins() ])
157 return list.events(ctx, {
158 - pluginLoaded: p => list.add(serialize(p)),
159 - pluginReloaded: p => {
158 + pluginInstalled: p => list.add(serialize(p)),
159 + 'pluginStarted pluginStopped': p => {
160 const { id, ...rest } = serialize(p)
161 list.update({ id }, rest)
162 },
163 - pluginUnloaded: id => list.remove({ id }),
164 - pluginAvailableNoMore: p => list.remove({ id: p.id }),
165 - pluginAvailable: p => list.add(p),
163 + pluginUninstalled: id => list.remove({ id }),
164 })
165
168 - function serialize(p: Readonly<Plugin>) {
169 - return Object.assign(p.getData(), _.pick(p, ['id','started']))
166 + function serialize(p: Readonly<Plugin> | AvailablePlugin) {
167 + return Object.assign('getData' in p ? p.getData() : p, { started: null }, _.pick(p, ['id','started']))
168 }
169 },
170
173 - async set_plugin({ id, disable, config }) {
174 - if (disable !== undefined) {
175 - const cfgK = 'disable_plugins'
176 - const a = getConfig(cfgK)
177 - if (a.includes(id) !== disable)
178 - setConfig({ [cfgK]: disable ? [...a, id] : a.filter((x: string) => x !== id) })
171 + async set_plugin({ id, enabled, config }) {
172 + if (enabled !== undefined) {
173 + const a = getConfig(CFG_ENABLE_PLUGINS)
174 + if (a.includes(id) !== enabled)
175 + setConfig({ [CFG_ENABLE_PLUGINS]: enabled ? [...a, id] : a.filter((x: string) => x !== id) })
176 }
177 if (config) {
178 config = _.pickBy(config, v => v !== null)
server/src/const.ts
+1
@@ -30,6 +30,7 @@ export const FORBIDDEN = 403
30 export const IS_WINDOWS = process.platform === 'win32'
31
32 export const CFG_PLUGINS_CONFIG = 'plugins_config'
33 +export const CFG_ENABLE_PLUGINS = 'enable_plugins'
34
35 // we want this to be the first stuff to be printed, then we print it in this module, that is executed at the beginning
36 if (DEV) console.clear()
server/src/misc.ts
+6 -4
@@ -167,11 +167,13 @@ export function pendingPromise<T>() {
167 // install multiple handlers and returns a handy 'uninstall' function which requires no parameter. Pass a map {event:handler}
168 export function onOff(em: EventEmitter, events: { [eventName:string]: (...args: any[]) => void }) {
169 events = { ...events } // avoid later modifications, as we need this later for uninstallation
170 - for (const k in events)
171 - em.on(k, events[k])
170 + for (const [k,cb] of Object.entries(events))
171 + for (const e of k.split(' '))
172 + em.on(e, cb)
173 return () => {
173 - for (const k in events)
174 - em.off(k, events[k])
174 + for (const [k,cb] of Object.entries(events))
175 + for (const e of k.split(' '))
176 + em.off(e, cb)
177 }
178 }
179
server/src/plugins.ts
+27 -22
@@ -3,8 +3,8 @@
3 import glob from 'fast-glob'
4 import { watchLoad } from './watchLoad'
5 import _ from 'lodash'
6 -import { resolve } from 'path'
7 -import { API_VERSION, CFG_PLUGINS_CONFIG, COMPATIBLE_API_VERSION, PLUGINS_PUB_URI } from './const'
6 +import pathLib from 'path'
7 +import { API_VERSION, CFG_ENABLE_PLUGINS, CFG_PLUGINS_CONFIG, COMPATIBLE_API_VERSION, PLUGINS_PUB_URI } from './const'
8 import * as Const from './const'
9 import Koa from 'koa'
10 import { debounceAsync, getOrSet, onProcessExit, wantArray, watchDir } from './misc'
@@ -91,7 +91,7 @@ export class Plugin {
91 console.warn('invalid', k)
92 }
93 }
94 - events.emit(old ? 'pluginReloaded' : 'pluginLoaded', this)
94 + events.emit(old || availablePlugins[id] ? 'pluginStarted' : 'pluginInstalled', this)
95 }
96 get middleware(): undefined | PluginMiddleware {
97 return this.data?.middleware
@@ -111,14 +111,18 @@ export class Plugin {
111 }
112
113 unload() {
114 - console.log('unloading plugin', this.id)
114 + const { id } = this
115 + console.log('unloading plugin', id)
116 try { this.data?.unload?.() }
117 catch(e) {
117 - console.debug('error unloading plugin', this.id, String(e))
118 + console.debug('error unloading plugin', id, String(e))
119 }
119 - delete plugins[this.id]
120 + delete plugins[id]
121 this.unwatch()
121 - events.emit('pluginUnloaded', this.id)
122 + if (availablePlugins[id])
123 + events.emit('pluginStopped', availablePlugins[id])
124 + else
125 + events.emit('pluginUninstalled', id)
126 }
127 }
128
@@ -126,7 +130,8 @@ type PluginMiddleware = (ctx:Koa.Context) => void | Stop | CallMeAfter
130 type Stop = true
131 type CallMeAfter = ()=>void
132
129 -let availablePlugins: Record<string, { id: string, description?: string, version?: number, apiRequired?: number }> = {}
133 +export interface AvailablePlugin { id: string, description?: string, version?: number, apiRequired?: number }
134 +let availablePlugins: Record<string, AvailablePlugin> = {}
135
136 export function getAvailablePlugins() {
137 return Object.values(availablePlugins)
@@ -138,18 +143,18 @@ if (!existsSync(PATH))
143 catch {}
144 watchDir(PATH, rescanAsap)
145
141 -const defaultValue = ['download-counter', 'redirect-root','max-downloads-ip']
142 -subscribeConfig({ k:'disable_plugins', defaultValue }, rescanAsap)
146 +const defaultValue = ['antibrute']
147 +subscribeConfig({ k: CFG_ENABLE_PLUGINS, defaultValue }, rescanAsap)
148
149 async function rescan() {
150 console.debug('scanning plugins')
151 const found = []
152 const foundDisabled: typeof availablePlugins = {}
148 - const disable_plugins = wantArray(getConfig('disable_plugins'))
153 + const enable_plugins = wantArray(getConfig(CFG_ENABLE_PLUGINS))
154 for (let f of await glob(PATH+'/*/plugin.js')) {
155 const id = f.split('/').slice(-2)[0]
156 if (id.endsWith('-disabled')) continue
152 - if (disable_plugins.includes(id)) {
157 + if (!enable_plugins.includes(id)) {
158 const pl = foundDisabled[id] = { id } as typeof foundDisabled[0]
159 try {
160 const source = await readFile(f, 'utf8')
@@ -163,13 +168,13 @@ async function rescan() {
168 found.push(id)
169 if (plugins[id]) // already loaded
170 continue
166 - f = resolve(f) // without this, import won't work
171 + const module = pathLib.resolve(f)
172 const { unwatch } = watchLoad(f, async () => {
173 try {
174 console.log(plugins[id] ? 'reloading plugin' : 'loading plugin', id)
170 - const { init, ...data } = await import(f)
175 + const { init, ...data } = await import(module)
176 delete data.default
172 - deleteModule(require.resolve(f)) // avoid caching
177 + deleteModule(require.resolve(module)) // avoid caching at next import
178 if (data.apiRequired > API_VERSION)
179 console.log('plugin', id, 'may not work correctly as it is designed for a newer version of HFS')
180 if (data.apiRequired < COMPATIBLE_API_VERSION)
@@ -190,16 +195,16 @@ async function rescan() {
195 }
196 })
197 }
198 + for (const id in foundDisabled)
199 + if (!availablePlugins[id] && !plugins[id])
200 + events.emit('pluginInstalled', foundDisabled[id])
201 + for (const id in availablePlugins)
202 + if (!foundDisabled[id] && !plugins[id])
203 + events.emit('pluginUninstalled', id)
204 + availablePlugins = foundDisabled
205 for (const id in plugins)
206 if (!found.includes(id))
207 plugins[id].unload()
196 - for (const k in foundDisabled)
197 - if (!availablePlugins[k])
198 - events.emit('pluginAvailable', foundDisabled[k])
199 - for (const k in availablePlugins)
200 - if (!foundDisabled[k])
201 - events.emit('pluginAvailableNoMore', availablePlugins[k])
202 - availablePlugins = foundDisabled
208 }
209
210 function deleteModule(id: string) {