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) {