generic socket plugin, control uses it, simplecommands rate limit implementation
This commit is contained in:
parent
ffee7103e0
commit
ecb8c2a69c
@ -3,7 +3,7 @@
|
|||||||
"name": "control",
|
"name": "control",
|
||||||
"description": "Squeebot Plugin Management API and sockets",
|
"description": "Squeebot Plugin Management API and sockets",
|
||||||
"tags": ["api", "control", "management"],
|
"tags": ["api", "control", "management"],
|
||||||
"version": "0.1.3",
|
"version": "0.2.0",
|
||||||
"dependencies": [],
|
"dependencies": ["socket"],
|
||||||
"npmDependencies": []
|
"npmDependencies": []
|
||||||
}
|
}
|
||||||
|
@ -5,16 +5,18 @@ import {
|
|||||||
Configurable,
|
Configurable,
|
||||||
IPluginManifest,
|
IPluginManifest,
|
||||||
IPlugin,
|
IPlugin,
|
||||||
|
DependencyLoad,
|
||||||
} from '@squeebot/core/lib/plugin';
|
} from '@squeebot/core/lib/plugin';
|
||||||
|
|
||||||
import { IChannel } from '@squeebot/core/lib/channel';
|
import { IChannel } from '@squeebot/core/lib/channel';
|
||||||
import { IRepoPluginDef, IRepository } from '@squeebot/core/lib/plugin/repository';
|
import {
|
||||||
|
IRepoPluginDef,
|
||||||
|
IRepository,
|
||||||
|
} from '@squeebot/core/lib/plugin/repository';
|
||||||
import { ISqueebotCore, logger } from '@squeebot/core/lib/core';
|
import { ISqueebotCore, logger } from '@squeebot/core/lib/core';
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import tls, { TLSSocket } from 'tls';
|
|
||||||
import net, { Server, Socket } from 'net';
|
|
||||||
|
|
||||||
interface ControlCommand {
|
interface ControlCommand {
|
||||||
execute: (p: ControlPlugin, ...args: any[]) => Promise<any>;
|
execute: (p: ControlPlugin, ...args: any[]) => Promise<any>;
|
||||||
@ -22,6 +24,18 @@ interface ControlCommand {
|
|||||||
plugin: string;
|
plugin: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SocketMessage {
|
||||||
|
[x: string]: unknown;
|
||||||
|
command?: string;
|
||||||
|
id?: string;
|
||||||
|
status?: string;
|
||||||
|
arguments?: unknown[];
|
||||||
|
list?: unknown[];
|
||||||
|
data?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReplyFn = (message: SocketMessage) => void;
|
||||||
|
|
||||||
let controlCommands: ControlCommand[] = [
|
let controlCommands: ControlCommand[] = [
|
||||||
{
|
{
|
||||||
name: 'loadPlugin',
|
name: 'loadPlugin',
|
||||||
@ -60,7 +74,10 @@ let controlCommands: ControlCommand[] = [
|
|||||||
{
|
{
|
||||||
name: 'installPlugin',
|
name: 'installPlugin',
|
||||||
plugin: 'control',
|
plugin: 'control',
|
||||||
execute: async (p: ControlPlugin, plugin: string): Promise<IPluginManifest> => {
|
execute: async (
|
||||||
|
p: ControlPlugin,
|
||||||
|
plugin: string
|
||||||
|
): Promise<IPluginManifest> => {
|
||||||
if (!plugin) {
|
if (!plugin) {
|
||||||
throw new Error('This function takes 1 argument.');
|
throw new Error('This function takes 1 argument.');
|
||||||
}
|
}
|
||||||
@ -142,7 +159,10 @@ let controlCommands: ControlCommand[] = [
|
|||||||
{
|
{
|
||||||
name: 'listRepositoryPlugins',
|
name: 'listRepositoryPlugins',
|
||||||
plugin: 'control',
|
plugin: 'control',
|
||||||
execute: async (p: ControlPlugin, repo: string): Promise<IRepoPluginDef[]> => {
|
execute: async (
|
||||||
|
p: ControlPlugin,
|
||||||
|
repo: string
|
||||||
|
): Promise<IRepoPluginDef[]> => {
|
||||||
if (!repo) {
|
if (!repo) {
|
||||||
throw new Error('This function takes 1 argument.');
|
throw new Error('This function takes 1 argument.');
|
||||||
}
|
}
|
||||||
@ -163,7 +183,10 @@ let controlCommands: ControlCommand[] = [
|
|||||||
{
|
{
|
||||||
name: 'updateRepository',
|
name: 'updateRepository',
|
||||||
plugin: 'control',
|
plugin: 'control',
|
||||||
execute: async (p: ControlPlugin, repo: string): Promise<IPluginManifest[]> => {
|
execute: async (
|
||||||
|
p: ControlPlugin,
|
||||||
|
repo: string
|
||||||
|
): Promise<IPluginManifest[]> => {
|
||||||
if (!repo) {
|
if (!repo) {
|
||||||
throw new Error('This function takes 1 argument.');
|
throw new Error('This function takes 1 argument.');
|
||||||
}
|
}
|
||||||
@ -177,7 +200,11 @@ let controlCommands: ControlCommand[] = [
|
|||||||
{
|
{
|
||||||
name: 'newChannel',
|
name: 'newChannel',
|
||||||
plugin: 'control',
|
plugin: 'control',
|
||||||
execute: async (p: ControlPlugin, name: string, plugins?: string[]): Promise<void> => {
|
execute: async (
|
||||||
|
p: ControlPlugin,
|
||||||
|
name: string,
|
||||||
|
plugins?: string[]
|
||||||
|
): Promise<void> => {
|
||||||
if (!name) {
|
if (!name) {
|
||||||
throw new Error('This function takes 1 argument.');
|
throw new Error('This function takes 1 argument.');
|
||||||
}
|
}
|
||||||
@ -244,7 +271,11 @@ let controlCommands: ControlCommand[] = [
|
|||||||
{
|
{
|
||||||
name: 'addChannelPlugin',
|
name: 'addChannelPlugin',
|
||||||
plugin: 'control',
|
plugin: 'control',
|
||||||
execute: async (p: ControlPlugin, name: string, plugins: string | string[]): Promise<void> => {
|
execute: async (
|
||||||
|
p: ControlPlugin,
|
||||||
|
name: string,
|
||||||
|
plugins: string | string[]
|
||||||
|
): Promise<void> => {
|
||||||
if (!name || !plugins) {
|
if (!name || !plugins) {
|
||||||
throw new Error('This function takes 2 arguments.');
|
throw new Error('This function takes 2 arguments.');
|
||||||
}
|
}
|
||||||
@ -265,7 +296,11 @@ let controlCommands: ControlCommand[] = [
|
|||||||
{
|
{
|
||||||
name: 'removeChannelPlugin',
|
name: 'removeChannelPlugin',
|
||||||
plugin: 'control',
|
plugin: 'control',
|
||||||
execute: async (p: ControlPlugin, name: string, plugins: string | string[]): Promise<void> => {
|
execute: async (
|
||||||
|
p: ControlPlugin,
|
||||||
|
name: string,
|
||||||
|
plugins: string | string[]
|
||||||
|
): Promise<void> => {
|
||||||
if (!name || !plugins) {
|
if (!name || !plugins) {
|
||||||
throw new Error('This function takes 2 arguments.');
|
throw new Error('This function takes 2 arguments.');
|
||||||
}
|
}
|
||||||
@ -308,7 +343,11 @@ let controlCommands: ControlCommand[] = [
|
|||||||
{
|
{
|
||||||
name: 'getPluginConfigValue',
|
name: 'getPluginConfigValue',
|
||||||
plugin: 'control',
|
plugin: 'control',
|
||||||
execute: async (p: ControlPlugin, name: string, key: string): Promise<any> => {
|
execute: async (
|
||||||
|
p: ControlPlugin,
|
||||||
|
name: string,
|
||||||
|
key: string
|
||||||
|
): Promise<any> => {
|
||||||
if (!name || !key) {
|
if (!name || !key) {
|
||||||
throw new Error('This function takes 2 arguments.');
|
throw new Error('This function takes 2 arguments.');
|
||||||
}
|
}
|
||||||
@ -332,7 +371,11 @@ let controlCommands: ControlCommand[] = [
|
|||||||
{
|
{
|
||||||
name: 'setPluginConfig',
|
name: 'setPluginConfig',
|
||||||
plugin: 'control',
|
plugin: 'control',
|
||||||
execute: async (p: ControlPlugin, name: string, config: any): Promise<any> => {
|
execute: async (
|
||||||
|
p: ControlPlugin,
|
||||||
|
name: string,
|
||||||
|
config: any
|
||||||
|
): Promise<any> => {
|
||||||
if (!name || !config) {
|
if (!name || !config) {
|
||||||
throw new Error('This function takes 2 arguments.');
|
throw new Error('This function takes 2 arguments.');
|
||||||
}
|
}
|
||||||
@ -347,7 +390,12 @@ let controlCommands: ControlCommand[] = [
|
|||||||
{
|
{
|
||||||
name: 'setPluginConfigValue',
|
name: 'setPluginConfigValue',
|
||||||
plugin: 'control',
|
plugin: 'control',
|
||||||
execute: async (p: ControlPlugin, name: string, key: string, value: string): Promise<any> => {
|
execute: async (
|
||||||
|
p: ControlPlugin,
|
||||||
|
name: string,
|
||||||
|
key: string,
|
||||||
|
value: string
|
||||||
|
): Promise<any> => {
|
||||||
if (!name || !key) {
|
if (!name || !key) {
|
||||||
throw new Error('This function takes 3 arguments.');
|
throw new Error('This function takes 3 arguments.');
|
||||||
}
|
}
|
||||||
@ -361,26 +409,6 @@ let controlCommands: ControlCommand[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
declare type StringAny = {[key: string]: any};
|
|
||||||
const match = ['key', 'cert', 'ca', 'dhparam', 'crl', 'pfx'];
|
|
||||||
async function parseTLSConfig(tlsconfig: StringAny): Promise<StringAny> {
|
|
||||||
const result: StringAny = {};
|
|
||||||
for (const key in tlsconfig) {
|
|
||||||
if (key === 'enabled') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (match.indexOf(key) === -1) {
|
|
||||||
result[key] = tlsconfig[key];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = path.resolve(tlsconfig[key]);
|
|
||||||
const bf = await fs.readFile(value);
|
|
||||||
result[key] = bf;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Configurable({
|
@Configurable({
|
||||||
authorizedIPs: [],
|
authorizedIPs: [],
|
||||||
tls: {
|
tls: {
|
||||||
@ -394,16 +422,15 @@ async function parseTLSConfig(tlsconfig: StringAny): Promise<StringAny> {
|
|||||||
})
|
})
|
||||||
class ControlPlugin extends Plugin {
|
class ControlPlugin extends Plugin {
|
||||||
public core: ISqueebotCore | null = null;
|
public core: ISqueebotCore | null = null;
|
||||||
public plugins = new Map<string, any>();
|
public plugins = new Map<string, unknown>();
|
||||||
private server: Server | null = null;
|
|
||||||
private sockets = new Set<Socket>();
|
|
||||||
|
|
||||||
public initialize(): void {
|
public initialize(): void {
|
||||||
this.addEventListener('core', (core: ISqueebotCore) => this.core = core);
|
this.addEventListener('core', (core: ISqueebotCore) => (this.core = core));
|
||||||
this.emitTo('core', 'request-core', this.name);
|
this.emitTo('core', 'request-core', this.name);
|
||||||
this.createSocket();
|
this.getPluginSchema(this.name).catch(() =>
|
||||||
this.getPluginSchema(this.name).catch(
|
logger.error(
|
||||||
() => logger.error('[control] How embarrasing! control could not load it\'s own schema!')
|
'[control] How embarrasing! control could not load it\'s own schema!'
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -412,7 +439,7 @@ class ControlPlugin extends Plugin {
|
|||||||
* @param name Plugin name
|
* @param name Plugin name
|
||||||
* @returns Plugin schema
|
* @returns Plugin schema
|
||||||
*/
|
*/
|
||||||
public async loadPluginSchema(name: string): Promise<any> {
|
public async loadPluginSchema(name: string): Promise<unknown> {
|
||||||
if (!this.core) {
|
if (!this.core) {
|
||||||
throw new Error('control could not access the core.');
|
throw new Error('control could not access the core.');
|
||||||
}
|
}
|
||||||
@ -425,11 +452,15 @@ class ControlPlugin extends Plugin {
|
|||||||
const fileRead = await fs.readFile(schemaPath, { encoding: 'utf8' });
|
const fileRead = await fs.readFile(schemaPath, { encoding: 'utf8' });
|
||||||
schema = JSON.parse(fileRead);
|
schema = JSON.parse(fileRead);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
throw new Error('No schema file found, it is not accessible or is not valid JSON.');
|
throw new Error(
|
||||||
|
'No schema file found, it is not accessible or is not valid JSON.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!schema.type) {
|
if (!schema.type) {
|
||||||
throw new Error('Schema does not specify what type of object it is referencing.');
|
throw new Error(
|
||||||
|
'Schema does not specify what type of object it is referencing.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.plugins.set(name, schema);
|
this.plugins.set(name, schema);
|
||||||
@ -441,7 +472,7 @@ class ControlPlugin extends Plugin {
|
|||||||
* @param name Plugin name
|
* @param name Plugin name
|
||||||
* @param confspec Static schema
|
* @param confspec Static schema
|
||||||
*/
|
*/
|
||||||
public registerPluginConfigSchema(name: string, confspec?: any): void {
|
public registerPluginConfigSchema(name: string, confspec?: unknown): void {
|
||||||
this.plugins.set(name, confspec);
|
this.plugins.set(name, confspec);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -451,7 +482,7 @@ class ControlPlugin extends Plugin {
|
|||||||
* @returns Schema
|
* @returns Schema
|
||||||
* @throws Error if schema is not found or is invalid
|
* @throws Error if schema is not found or is invalid
|
||||||
*/
|
*/
|
||||||
public async getPluginSchema(name: string): Promise<any> {
|
public async getPluginSchema(name: string): Promise<unknown> {
|
||||||
if (this.plugins.has(name)) {
|
if (this.plugins.has(name)) {
|
||||||
return this.plugins.get(name);
|
return this.plugins.get(name);
|
||||||
}
|
}
|
||||||
@ -465,11 +496,14 @@ class ControlPlugin extends Plugin {
|
|||||||
* @param args Control command arguments
|
* @param args Control command arguments
|
||||||
* @returns Control command response
|
* @returns Control command response
|
||||||
*/
|
*/
|
||||||
public async executeControlCommand(command: string, args: string[]): Promise<any> {
|
public async executeControlCommand(
|
||||||
|
command: string,
|
||||||
|
args: string[]
|
||||||
|
): Promise<unknown> {
|
||||||
if (!this.core) {
|
if (!this.core) {
|
||||||
throw new Error('The control plugin cannot control the bot right now.');
|
throw new Error('The control plugin cannot control the bot right now.');
|
||||||
}
|
}
|
||||||
const cmdobj = controlCommands.find(k => k.name === command);
|
const cmdobj = controlCommands.find((k) => k.name === command);
|
||||||
if (!cmdobj || !cmdobj.execute) {
|
if (!cmdobj || !cmdobj.execute) {
|
||||||
throw new Error('No such command');
|
throw new Error('No such command');
|
||||||
}
|
}
|
||||||
@ -484,11 +518,14 @@ class ControlPlugin extends Plugin {
|
|||||||
if (!obj.execute || !obj.name || !obj.plugin) {
|
if (!obj.execute || !obj.name || !obj.plugin) {
|
||||||
throw new Error('Invalid command object.');
|
throw new Error('Invalid command object.');
|
||||||
}
|
}
|
||||||
const exists = controlCommands.find(k => k.name === obj.name);
|
|
||||||
|
const exists = controlCommands.find((k) => k.name === obj.name);
|
||||||
if (exists) {
|
if (exists) {
|
||||||
throw new Error('Control commands should not be overwritten.');
|
throw new Error('Control commands should not be overwritten.');
|
||||||
}
|
}
|
||||||
|
|
||||||
controlCommands.push(obj);
|
controlCommands.push(obj);
|
||||||
|
|
||||||
logger.log('[%s] registered control command', this.name, obj.name);
|
logger.log('[%s] registered control command', this.name, obj.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -501,15 +538,6 @@ class ControlPlugin extends Plugin {
|
|||||||
if (plugin === this.name || plugin === this) {
|
if (plugin === this.name || plugin === this) {
|
||||||
logger.debug('[%s]', this.name, 'shutting down..');
|
logger.debug('[%s]', this.name, 'shutting down..');
|
||||||
|
|
||||||
if (this.server) {
|
|
||||||
logger.log('[%s] Stopping socket server..', this.name);
|
|
||||||
this.server.close();
|
|
||||||
for (const sock of this.sockets) {
|
|
||||||
sock.destroy();
|
|
||||||
}
|
|
||||||
this.sockets.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.plugins.clear();
|
this.plugins.clear();
|
||||||
this.emit('pluginUnloaded', this);
|
this.emit('pluginUnloaded', this);
|
||||||
}
|
}
|
||||||
@ -521,152 +549,70 @@ class ControlPlugin extends Plugin {
|
|||||||
plugin = plugin.manifest.name;
|
plugin = plugin.manifest.name;
|
||||||
}
|
}
|
||||||
this.plugins.delete(plugin);
|
this.plugins.delete(plugin);
|
||||||
controlCommands = controlCommands.filter(k => k.plugin !== plugin);
|
controlCommands = controlCommands.filter((k) => k.plugin !== plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
private errorToClient(socket: TLSSocket | Socket, error: Error): void {
|
@DependencyLoad('socket')
|
||||||
socket.write(JSON.stringify({
|
public socketDepLoaded(socketAPI: any) {
|
||||||
status: 'ERROR',
|
const config = this.config.config;
|
||||||
message: error.message,
|
socketAPI
|
||||||
}) + '\r\n');
|
.createServer({
|
||||||
|
name: 'control',
|
||||||
|
plugin: this.name,
|
||||||
|
...config,
|
||||||
|
})
|
||||||
|
.then((server: any) => {
|
||||||
|
server.on('message', (...args: [SocketMessage, never, ReplyFn]) =>
|
||||||
|
this.handleClientLine(...args)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((err: Error) =>
|
||||||
|
logger.error('[%s] Failed to initialize: ', this.name, err.message)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleClientLine(socket: TLSSocket | Socket, req: any): void {
|
private handleClientLine(
|
||||||
if (!req.command || req.command === 'status') {
|
message: SocketMessage,
|
||||||
socket.write(JSON.stringify({
|
sender: never,
|
||||||
status: 'OK'
|
reply: ReplyFn
|
||||||
}) + '\r\n');
|
): void {
|
||||||
return;
|
if (!message.command) return;
|
||||||
}
|
this.executeControlCommand(
|
||||||
|
message.command,
|
||||||
|
(message.arguments as string[]) || []
|
||||||
|
).then(
|
||||||
|
(cmdData) => {
|
||||||
|
try {
|
||||||
|
const response: SocketMessage = {
|
||||||
|
status: 'OK',
|
||||||
|
command: message.command,
|
||||||
|
id: message.id
|
||||||
|
};
|
||||||
|
|
||||||
if (req.command === 'quit') {
|
if (cmdData != null) {
|
||||||
socket.end();
|
if (Array.isArray(cmdData)) {
|
||||||
return;
|
response.list = cmdData;
|
||||||
}
|
} else {
|
||||||
|
response.data = cmdData;
|
||||||
let args = [];
|
}
|
||||||
const argField = req.args || req.argument || req.arguments;
|
|
||||||
if (argField) {
|
|
||||||
if (!Array.isArray(argField)) {
|
|
||||||
args = [argField];
|
|
||||||
} else {
|
|
||||||
args = argField;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.executeControlCommand(req.command, args).then((cmdData) => {
|
|
||||||
try {
|
|
||||||
const response: any = { status: 'OK', command: req.command };
|
|
||||||
if (cmdData != null) {
|
|
||||||
if (Array.isArray(cmdData)) {
|
|
||||||
response.list = cmdData;
|
|
||||||
} else {
|
|
||||||
response.data = cmdData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
socket.write(JSON.stringify(response) + '\r\n');
|
|
||||||
} catch (e: any) {
|
|
||||||
this.errorToClient(socket, e);
|
|
||||||
}
|
|
||||||
}, (e) => this.errorToClient(socket, e));
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleIncoming(socket: TLSSocket | Socket): void {
|
|
||||||
const c = this.config.config;
|
|
||||||
let addr = socket.remoteAddress;
|
|
||||||
if (addr?.indexOf('::ffff:') === 0) {
|
|
||||||
addr = addr.substr(7);
|
|
||||||
}
|
|
||||||
if (c.authorizedIPs &&
|
|
||||||
c.authorizedIPs.length &&
|
|
||||||
c.authorizedIPs.indexOf(addr) === -1) {
|
|
||||||
if (!(c.authorizedIPs.indexOf('localhost') !== -1 &&
|
|
||||||
(addr === '::1' || addr === '127.0.0.1'))) {
|
|
||||||
logger.warn('[%s] Unauthorized connection made from %s',
|
|
||||||
this.name, addr);
|
|
||||||
socket.destroy();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sockets.add(socket);
|
|
||||||
|
|
||||||
socket.once('end', () => {
|
|
||||||
logger.log('[%s] Client from %s disconnected.', this.name, addr);
|
|
||||||
this.sockets.delete(socket);
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.log('[%s] Client from %s connected.', this.name, addr);
|
|
||||||
socket.setEncoding('utf8');
|
|
||||||
socket.write(JSON.stringify({
|
|
||||||
status: 'OK',
|
|
||||||
commands: controlCommands.map(k => k.name),
|
|
||||||
}) + '\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
try {
|
|
||||||
const split = data.split('\r\n');
|
|
||||||
for (const chunk of split) {
|
|
||||||
if (chunk === '') {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const req = JSON.parse(chunk);
|
reply(response);
|
||||||
this.handleClientLine(socket, req);
|
} catch (error) {
|
||||||
|
reply({
|
||||||
|
status: 'ERROR',
|
||||||
|
arguments: [(error as Error).message],
|
||||||
|
id: message.id
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
},
|
||||||
this.errorToClient(socket, e);
|
(error) =>
|
||||||
}
|
reply({
|
||||||
});
|
status: 'ERROR',
|
||||||
}
|
arguments: [(error as Error).message],
|
||||||
|
id: message.id
|
||||||
private createSocket(): void {
|
})
|
||||||
const c = this.config.config;
|
);
|
||||||
if (c.bind == null ||
|
|
||||||
c.bind === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (c.tls && c.tls.enabled) {
|
|
||||||
if (!c.tls.rejectUnauthorized && (!c.authorizedIPs || !c.authorizedIPs.length)) {
|
|
||||||
logger.warn('[%s] [SECURITY WARNING] !!! YOUR CONTROL SOCKET IS (STILL) INSECURE !!!', this.name);
|
|
||||||
logger.warn('[%s] [SECURITY WARNING] You have enabled TLS, ' +
|
|
||||||
'but you do not have any form of access control configured.', this.name);
|
|
||||||
logger.warn('[%s] [SECURITY WARNING] In order to secure your control socket, ' +
|
|
||||||
'either enable invalid certificate rejection (rejectUnauthorized) or set a ' +
|
|
||||||
'list of authorized IPs.', this.name);
|
|
||||||
logger.warn('[%s] [SECURITY WARNING] !!! YOUR CONTROL SOCKET IS (STILL) INSECURE !!!', this.name);
|
|
||||||
}
|
|
||||||
parseTLSConfig(c.tls).then((options) => {
|
|
||||||
this.server = tls.createServer(options,
|
|
||||||
(socket) => this.handleIncoming(socket));
|
|
||||||
this.server.on('error',
|
|
||||||
(e) => logger.error('[%s] Secure socket error:', e.message));
|
|
||||||
this.server.listen(c.bind, () => {
|
|
||||||
logger.log('[%s] Secure socket listening on %s',
|
|
||||||
this.name, c.bind.toString());
|
|
||||||
});
|
|
||||||
}, (err) => {
|
|
||||||
logger.error('[%s] Secure socket listen failed: %s',
|
|
||||||
this.name, err.message);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!c.authorizedIPs || !c.authorizedIPs.length) {
|
|
||||||
logger.warn('[%s] [SECURITY WARNING] !!! YOUR CONTROL SOCKET IS INSECURE !!!', this.name);
|
|
||||||
logger.warn('[%s] [SECURITY WARNING] You do not have any form of access control configured.', this.name);
|
|
||||||
logger.warn('[%s] [SECURITY WARNING] In order to secure your control socket, ' +
|
|
||||||
'either enable TLS with certificate verification or set a list of authorized IPs.', this.name);
|
|
||||||
logger.warn('[%s] [SECURITY WARNING] !!! YOUR CONTROL SOCKET IS INSECURE !!!', this.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.server = net.createServer((socket) => this.handleIncoming(socket));
|
|
||||||
this.server.on('error', (e) => logger.error('[%s] Socket error:', e.message));
|
|
||||||
this.server.listen(c.bind, () => {
|
|
||||||
logger.log('[%s] Socket listening on %s',
|
|
||||||
this.name, c.bind.toString());
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ interface CommandSpec {
|
|||||||
|
|
||||||
interface RateLimit {
|
interface RateLimit {
|
||||||
rooms: string[];
|
rooms: string[];
|
||||||
perSecond: number;
|
rate: number;
|
||||||
cooldown: number;
|
cooldown: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,7 +61,7 @@ class SqueebotCommandsAPIPlugin extends Plugin {
|
|||||||
private permissions: any = null;
|
private permissions: any = null;
|
||||||
|
|
||||||
public getRateLimit(room: string): RateLimit | null {
|
public getRateLimit(room: string): RateLimit | null {
|
||||||
for (const rm of this.config.get('rateLimits', [])) {
|
for (const rm of this.config.get<RateLimit[]>('rateLimits', [])) {
|
||||||
if (rm.rooms && (rm.rooms.indexOf(room) !== -1 || rm.rooms.indexOf('*') !== -1)) {
|
if (rm.rooms && (rm.rooms.indexOf(room) !== -1 || rm.rooms.indexOf('*') !== -1)) {
|
||||||
return rm;
|
return rm;
|
||||||
}
|
}
|
||||||
@ -86,7 +86,7 @@ class SqueebotCommandsAPIPlugin extends Plugin {
|
|||||||
|
|
||||||
const r = rates[sender];
|
const r = rates[sender];
|
||||||
if (r.lastMessage > Date.now() - rl.cooldown) {
|
if (r.lastMessage > Date.now() - rl.cooldown) {
|
||||||
if (r.messages >= rl.perSecond) {
|
if (r.messages >= rl.rate) {
|
||||||
// Rate limited
|
// Rate limited
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -101,6 +101,56 @@ class SqueebotCommandsAPIPlugin extends Plugin {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setRateLimit(room: string, rate: number, cooldown: number): void {
|
||||||
|
const basis = this.config.get('rateLimits', []) as RateLimit[];
|
||||||
|
const found = basis.find((item) => item.rooms.includes(room));
|
||||||
|
const resave = () => {
|
||||||
|
this.config.set('rateLimits', basis);
|
||||||
|
this.config.save();
|
||||||
|
};
|
||||||
|
|
||||||
|
const addNew = () => {
|
||||||
|
const newRate: RateLimit = {
|
||||||
|
rate, cooldown, rooms: [room]
|
||||||
|
};
|
||||||
|
|
||||||
|
basis.push(newRate);
|
||||||
|
resave();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (found) {
|
||||||
|
if (rate === 0 || (rate !== found.rate || cooldown !== found.cooldown)) {
|
||||||
|
if (rate === 0 || found.rooms.length > 1) {
|
||||||
|
found.rooms.splice(found.rooms.indexOf(room), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rate === 0) {
|
||||||
|
if (found.rooms.length === 0) {
|
||||||
|
basis.splice(basis.indexOf(found), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
resave();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addNew();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found.rooms.length > 1) {
|
||||||
|
addNew();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
found.rate = rate;
|
||||||
|
found.cooldown = cooldown;
|
||||||
|
resave();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addNew();
|
||||||
|
}
|
||||||
|
|
||||||
private roomMatcher(msg: IMessage, specList: CommandSpec[]): CommandSpec[] {
|
private roomMatcher(msg: IMessage, specList: CommandSpec[]): CommandSpec[] {
|
||||||
const roomMatches = [];
|
const roomMatches = [];
|
||||||
|
|
||||||
@ -248,7 +298,7 @@ class SqueebotCommandsAPIPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let allowedPlugins: string[] = [];
|
let allowedPlugins: string[] = [];
|
||||||
if (chan && this.config.get('channelMatching', false) === true) {
|
if (chan && this.config.get<boolean>('channelMatching', false) === true) {
|
||||||
allowedPlugins = chan.plugins;
|
allowedPlugins = chan.plugins;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -572,6 +622,48 @@ class SqueebotCommandsAPIPlugin extends Plugin {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.registerCommand({
|
||||||
|
plugin: this.name,
|
||||||
|
name: 'ratelimit',
|
||||||
|
usage: '<commands per cooldown> <cooldown in seconds>',
|
||||||
|
description: 'Set command rate limits in the current room',
|
||||||
|
hidden: true,
|
||||||
|
permissions: ['room.rate-limit'],
|
||||||
|
execute: async (
|
||||||
|
msg: IMessage,
|
||||||
|
msr: MessageResolver,
|
||||||
|
spec: CommandSpec,
|
||||||
|
prefix: string,
|
||||||
|
...args: any[]
|
||||||
|
): Promise<boolean> => {
|
||||||
|
if (!args.length) {
|
||||||
|
const rl = this.getRateLimit(msg.target?.id as string);
|
||||||
|
if (!rl) {
|
||||||
|
msg.resolve(`There are no rate limits enabled for ${msg.target?.name}.`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isGlobal = rl.rooms.includes('*');
|
||||||
|
msg.resolve(
|
||||||
|
`Active rate limit in ${msg.target?.name}${isGlobal ? ' (global)' : ''}: ${rl.rate} messages in ${rl.cooldown}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [count, cooldown] = args;
|
||||||
|
try {
|
||||||
|
this.setRateLimit(msg.target?.id as string, parseInt(count, 10), parseInt(cooldown, 10));
|
||||||
|
} catch (e: any) {
|
||||||
|
msg.resolve('Setting rate limit failed:', e.message);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.resolve('Rate limit has been configured.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@DependencyLoad('permissions')
|
@DependencyLoad('permissions')
|
||||||
|
9
socket/plugin.json
Normal file
9
socket/plugin.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"main": "plugin.js",
|
||||||
|
"name": "socket",
|
||||||
|
"description": "General-purpose socket server API and service",
|
||||||
|
"tags": ["api"],
|
||||||
|
"version": "0.0.1",
|
||||||
|
"dependencies": [],
|
||||||
|
"npmDependencies": []
|
||||||
|
}
|
438
socket/plugin.ts
Normal file
438
socket/plugin.ts
Normal file
@ -0,0 +1,438 @@
|
|||||||
|
import {
|
||||||
|
Plugin,
|
||||||
|
EventListener,
|
||||||
|
} from '@squeebot/core/lib/plugin';
|
||||||
|
|
||||||
|
import { logger } from '@squeebot/core/lib/core';
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import tls, { TLSSocket } from 'tls';
|
||||||
|
import net, { Server, Socket } from 'net';
|
||||||
|
import { randomBytes } from 'crypto';
|
||||||
|
import EventEmitter from 'events';
|
||||||
|
|
||||||
|
interface TLSConfiguration {
|
||||||
|
[x: string]: string | boolean | undefined;
|
||||||
|
enabled: boolean;
|
||||||
|
key?: string;
|
||||||
|
cert?: string;
|
||||||
|
ca?: string;
|
||||||
|
dhparam?: string;
|
||||||
|
crl?: string;
|
||||||
|
pfx?: string;
|
||||||
|
rejectUnauthorized: boolean;
|
||||||
|
requestCert: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SocketMessage {
|
||||||
|
[x: string]: unknown;
|
||||||
|
command?: string;
|
||||||
|
id?: string;
|
||||||
|
status?: string;
|
||||||
|
arguments?: unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SocketConfiguration {
|
||||||
|
name: string;
|
||||||
|
plugin?: string;
|
||||||
|
authorizedIPs: string[];
|
||||||
|
tls?: TLSConfiguration;
|
||||||
|
bind: number | string | null | false;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PluginConfiguration {
|
||||||
|
sockets: SocketConfiguration[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type SocketWithID = (Socket | TLSSocket) & { id: string };
|
||||||
|
|
||||||
|
const makeID = () => randomBytes(8).toString('hex');
|
||||||
|
|
||||||
|
class SocketServer extends EventEmitter {
|
||||||
|
private server: Server | null = null;
|
||||||
|
private sockets = new Set<SocketWithID>();
|
||||||
|
private intents = new Map<string, string>();
|
||||||
|
public alive = false;
|
||||||
|
|
||||||
|
constructor(public config: SocketConfiguration) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
get logName() {
|
||||||
|
return `socket:${this.config.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public stop() {
|
||||||
|
if (this.server) {
|
||||||
|
logger.log('[%s] Stopping socket server..', this.logName);
|
||||||
|
this.server.close();
|
||||||
|
for (const sock of this.sockets) {
|
||||||
|
sock.destroy();
|
||||||
|
}
|
||||||
|
this.sockets.clear();
|
||||||
|
this.intents.clear();
|
||||||
|
}
|
||||||
|
this.emit('close');
|
||||||
|
this.alive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async start() {
|
||||||
|
if (this.alive) {
|
||||||
|
this.stop();
|
||||||
|
}
|
||||||
|
await this.createSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
public send(message: SocketMessage): void {
|
||||||
|
this.sockets.forEach((socket) => this.sendToClient(socket, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendToID(id: string, message: SocketMessage): void {
|
||||||
|
const findClient = Array.from(this.sockets.values()).find(
|
||||||
|
(client) => client.id === id
|
||||||
|
);
|
||||||
|
if (!findClient) return;
|
||||||
|
this.sendToClient(findClient, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendIntent(intent: string, message: SocketMessage): void {
|
||||||
|
for (const [id, clientintent] of this.intents) {
|
||||||
|
if (clientintent !== intent) continue;
|
||||||
|
this.sendToID(id, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendToClient(socket: SocketWithID, message: SocketMessage): void {
|
||||||
|
socket.write(JSON.stringify(message) + '\r\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleClientLine(socket: SocketWithID, req: SocketMessage): void {
|
||||||
|
if (!req.command || req.command === 'status') {
|
||||||
|
socket.write(
|
||||||
|
JSON.stringify({
|
||||||
|
id: socket.id,
|
||||||
|
status: 'OK',
|
||||||
|
}) + '\r\n'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client requests graceful close
|
||||||
|
if (req.command === 'quit') {
|
||||||
|
socket.end();
|
||||||
|
this.intents.delete(socket.id);
|
||||||
|
this.emit('disconnect', socket.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse argument list
|
||||||
|
let args = [];
|
||||||
|
const argField = req.args || req.argument || req.arguments;
|
||||||
|
if (argField) {
|
||||||
|
if (!Array.isArray(argField)) {
|
||||||
|
args = [argField];
|
||||||
|
} else {
|
||||||
|
args = argField;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set or return the intent
|
||||||
|
if (req.command === 'intent') {
|
||||||
|
if (args[0]) {
|
||||||
|
this.intents.set(socket.id, args[0]);
|
||||||
|
this.emit('intent', socket.id, args[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sendToClient(socket, {
|
||||||
|
id: socket.id,
|
||||||
|
command: 'intent',
|
||||||
|
arguments: [this.intents.get(socket.id)],
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const intent = this.intents.get(socket.id);
|
||||||
|
this.emit(
|
||||||
|
'message',
|
||||||
|
{ ...req, arguments: args },
|
||||||
|
{ id: socket.id, intent },
|
||||||
|
(response: SocketMessage) => {
|
||||||
|
socket.write(JSON.stringify(response) + '\r\n');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
`[%s] Error in message handler: ${(err as Error).stack}`,
|
||||||
|
this.logName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleIncoming(incoming: Socket | TLSSocket): void {
|
||||||
|
const socket = incoming as SocketWithID;
|
||||||
|
socket.id = makeID();
|
||||||
|
const c = this.config;
|
||||||
|
let addr = socket.remoteAddress;
|
||||||
|
if (addr?.indexOf('::ffff:') === 0) {
|
||||||
|
addr = addr.substring(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
addr &&
|
||||||
|
c.authorizedIPs?.length &&
|
||||||
|
c.authorizedIPs.indexOf(addr) === -1
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
c.authorizedIPs.indexOf('localhost') !== -1 &&
|
||||||
|
(addr === '::1' || addr === '127.0.0.1')
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
logger.warn(
|
||||||
|
'[%s] Unauthorized connection made from %s',
|
||||||
|
this.logName,
|
||||||
|
addr
|
||||||
|
);
|
||||||
|
socket.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sockets.add(socket);
|
||||||
|
|
||||||
|
socket.once('end', () => {
|
||||||
|
logger.log('[%s] Client from %s (%s) disconnected.', this.logName, addr, socket.id);
|
||||||
|
this.sockets.delete(socket);
|
||||||
|
this.intents.delete(socket.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.log('[%s] Client from %s (%s) connected.', this.logName, addr, socket.id);
|
||||||
|
socket.setEncoding('utf8');
|
||||||
|
socket.write(
|
||||||
|
JSON.stringify({
|
||||||
|
id: socket.id,
|
||||||
|
status: 'OK',
|
||||||
|
}) + '\r\n'
|
||||||
|
);
|
||||||
|
|
||||||
|
socket.on('data', (data) => {
|
||||||
|
try {
|
||||||
|
const split = data.split('\r\n');
|
||||||
|
for (const chunk of split) {
|
||||||
|
if (chunk === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const req = JSON.parse(chunk);
|
||||||
|
this.handleClientLine(socket, req);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.sendToClient(socket, {
|
||||||
|
status: 'ERROR',
|
||||||
|
arguments: [(error as Error).message],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.emit('connect', socket.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private createSocket(): Promise<void> {
|
||||||
|
const c = this.config;
|
||||||
|
if (c.bind == null || c.bind === false) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (c.tls && c.tls.enabled) {
|
||||||
|
if (
|
||||||
|
!c.tls.rejectUnauthorized &&
|
||||||
|
(!c.authorizedIPs || !c.authorizedIPs.length)
|
||||||
|
) {
|
||||||
|
logger.warn(
|
||||||
|
'[%s] [SECURITY WARNING] !!! YOUR CONTROL SOCKET IS (STILL) INSECURE !!!',
|
||||||
|
this.logName
|
||||||
|
);
|
||||||
|
logger.warn(
|
||||||
|
'[%s] [SECURITY WARNING] You have enabled TLS, ' +
|
||||||
|
'but you do not have any form of access control configured.',
|
||||||
|
this.logName
|
||||||
|
);
|
||||||
|
logger.warn(
|
||||||
|
'[%s] [SECURITY WARNING] In order to secure your control socket, ' +
|
||||||
|
'either enable invalid certificate rejection (rejectUnauthorized) or set a ' +
|
||||||
|
'list of authorized IPs.',
|
||||||
|
this.logName
|
||||||
|
);
|
||||||
|
logger.warn(
|
||||||
|
'[%s] [SECURITY WARNING] !!! YOUR CONTROL SOCKET IS (STILL) INSECURE !!!',
|
||||||
|
this.logName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
parseTLSConfig(c.tls).then(
|
||||||
|
(options) => {
|
||||||
|
this.server = tls.createServer(options, (socket) =>
|
||||||
|
this.handleIncoming(socket)
|
||||||
|
);
|
||||||
|
this.server.on('error', (e) => {
|
||||||
|
logger.error('[%s] Secure socket error:', e.message);
|
||||||
|
if (!this.alive) reject(e);
|
||||||
|
});
|
||||||
|
this.server.listen(c.bind, () => {
|
||||||
|
logger.log(
|
||||||
|
'[%s] Secure socket listening on %s',
|
||||||
|
this.logName,
|
||||||
|
c.bind?.toString()
|
||||||
|
);
|
||||||
|
this.alive = true;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
logger.error(
|
||||||
|
'[%s] Secure socket listen failed: %s',
|
||||||
|
this.logName,
|
||||||
|
err.message
|
||||||
|
);
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!c.authorizedIPs || !c.authorizedIPs.length) {
|
||||||
|
logger.warn(
|
||||||
|
'[%s] [SECURITY WARNING] !!! YOUR SOCKET IS INSECURE !!!',
|
||||||
|
this.logName
|
||||||
|
);
|
||||||
|
logger.warn(
|
||||||
|
'[%s] [SECURITY WARNING] You do not have any form of access control configured.',
|
||||||
|
this.logName
|
||||||
|
);
|
||||||
|
logger.warn(
|
||||||
|
'[%s] [SECURITY WARNING] In order to secure your control socket, ' +
|
||||||
|
'either enable TLS with certificate verification or set a list of authorized IPs.',
|
||||||
|
this.logName
|
||||||
|
);
|
||||||
|
logger.warn(
|
||||||
|
'[%s] [SECURITY WARNING] !!! YOUR SOCKET IS INSECURE !!!',
|
||||||
|
this.logName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.server = net.createServer((socket) => this.handleIncoming(socket));
|
||||||
|
this.server.on('error', (e) => {
|
||||||
|
logger.error('[%s] Socket error:', e.message);
|
||||||
|
if (!this.alive) reject(e);
|
||||||
|
});
|
||||||
|
this.server.listen(c.bind, () => {
|
||||||
|
logger.log(
|
||||||
|
'[%s] Socket listening on %s',
|
||||||
|
this.logName,
|
||||||
|
c.bind?.toString()
|
||||||
|
);
|
||||||
|
this.alive = true;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = ['key', 'cert', 'ca', 'dhparam', 'crl', 'pfx'];
|
||||||
|
async function parseTLSConfig(
|
||||||
|
tlsconfig: TLSConfiguration
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
|
const result: Record<string, unknown> = {};
|
||||||
|
for (const key in tlsconfig) {
|
||||||
|
if (key === 'enabled') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match.indexOf(key) === -1) {
|
||||||
|
result[key] = tlsconfig[key];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = path.resolve(tlsconfig[key] as string);
|
||||||
|
const bf = await fs.readFile(value);
|
||||||
|
result[key] = bf;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SocketPlugin extends Plugin {
|
||||||
|
private plugins = new Map<string, any>();
|
||||||
|
private servers = new Map<string, SocketServer>();
|
||||||
|
|
||||||
|
/** Create an unique 8-bit ID */
|
||||||
|
public makeID = makeID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kill a socket server by name
|
||||||
|
* @param name Socket server name
|
||||||
|
*/
|
||||||
|
public killServer(name: string) {
|
||||||
|
if (!this.servers.has(name)) return;
|
||||||
|
this.servers.get(name)?.stop();
|
||||||
|
this.servers.delete(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a socket server
|
||||||
|
* @param config Socket Server configuration
|
||||||
|
* @param start Start the socket automatically on creation
|
||||||
|
* @returns Socket Server
|
||||||
|
*/
|
||||||
|
public async createServer(config: SocketConfiguration, start = true) {
|
||||||
|
if (this.servers.has(config.name)) return;
|
||||||
|
const server = new SocketServer(config);
|
||||||
|
this.servers.set(config.name, server);
|
||||||
|
if (start) await server.start();
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an existing socket server by name
|
||||||
|
* @param name Socket Server name
|
||||||
|
* @returns Existing Socket Server or undefined
|
||||||
|
*/
|
||||||
|
public getServer(name: string) {
|
||||||
|
return this.servers.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener('pluginUnload')
|
||||||
|
public unloadEventHandler(plugin: string | Plugin): void {
|
||||||
|
if (plugin === this.name || plugin === this) {
|
||||||
|
logger.debug('[%s]', this.name, 'shutting down..');
|
||||||
|
|
||||||
|
for (const [key] of this.servers) {
|
||||||
|
this.killServer(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.plugins.clear();
|
||||||
|
this.emit('pluginUnloaded', this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener('pluginUnloaded')
|
||||||
|
public unloadedEventHandler(plugin: string | Plugin): void {
|
||||||
|
if (typeof plugin !== 'string') {
|
||||||
|
plugin = plugin.manifest.name;
|
||||||
|
}
|
||||||
|
this.plugins.delete(plugin);
|
||||||
|
this.killPluginServers(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
private killPluginServers(plugin: string) {
|
||||||
|
for (const [name, server] of this.servers) {
|
||||||
|
if (!server.config.plugin || server.config.plugin !== plugin) continue;
|
||||||
|
this.killServer(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SocketPlugin;
|
101
socket/proto.ts.bak
Normal file
101
socket/proto.ts.bak
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { EMessageType, Formatter, IMessage, Protocol } from '@squeebot/core';
|
||||||
|
|
||||||
|
class VirtualChatProtocol extends Protocol {
|
||||||
|
public format = new Formatter(false, false);
|
||||||
|
public type = 'VirtualChatProtocol';
|
||||||
|
|
||||||
|
public me = {
|
||||||
|
name: 'Squeebot',
|
||||||
|
id: `socket/${this.serverName}/Squeebot`
|
||||||
|
};
|
||||||
|
|
||||||
|
get serverName() {
|
||||||
|
return this.config.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
get server() {
|
||||||
|
const plugin = this.plugin;
|
||||||
|
return plugin.getServer(this.serverName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public start(...args: any[]): void {
|
||||||
|
this.server?.addListener('message', this.boundMessageHandler);
|
||||||
|
this.server?.addListener('close', this.stopHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public stop(force?: boolean | undefined): void {
|
||||||
|
this.server?.removeListener('message', this.boundMessageHandler);
|
||||||
|
this.server?.removeListener('close', this.stopHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private messageHandler(
|
||||||
|
message: SocketMessage,
|
||||||
|
sender: { id: string; intent?: string },
|
||||||
|
reply: replyFn
|
||||||
|
) {
|
||||||
|
if (message.command !== 'event') return;
|
||||||
|
const [eventName, ...argv] = message.arguments as string[];
|
||||||
|
const newMessage = {
|
||||||
|
id: message.id || makeID(),
|
||||||
|
type: eventName as EMessageType,
|
||||||
|
data: message,
|
||||||
|
text: argv.join(' '),
|
||||||
|
source: this,
|
||||||
|
time: new Date(),
|
||||||
|
target: { id: sender.id, name: sender.intent || sender.id },
|
||||||
|
sender: { id: sender.id, name: sender.intent || sender.id },
|
||||||
|
fullSenderID: `socket/${this.serverName}/${sender.id}`,
|
||||||
|
fullRoomID: `socket/${this.serverName}/${sender.id}`,
|
||||||
|
resolved: false,
|
||||||
|
direct: true,
|
||||||
|
resolve: (...data: any[]) => {
|
||||||
|
newMessage.resolved = true;
|
||||||
|
const response = this.parseMessage(...data);
|
||||||
|
if (!response || !this.server?.alive) return;
|
||||||
|
reply({
|
||||||
|
id: newMessage.id,
|
||||||
|
command: 'message',
|
||||||
|
arguments: ['message', response],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
reject: (error) =>
|
||||||
|
reply({
|
||||||
|
id: newMessage.id,
|
||||||
|
status: 'ERROR',
|
||||||
|
arguments: [error],
|
||||||
|
}),
|
||||||
|
mention: (target) => `${target.name}, `,
|
||||||
|
} as IMessage;
|
||||||
|
|
||||||
|
this.plugin.stream.emitTo(
|
||||||
|
'channel',
|
||||||
|
eventName !== 'message' ? 'event' : 'message',
|
||||||
|
newMessage
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseMessage(...data: any[]): string | null {
|
||||||
|
let response = util.format(data[0], ...data.slice(1));
|
||||||
|
if (!response) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(data[0])) {
|
||||||
|
try {
|
||||||
|
response = this.format.compose(data[0]);
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.error(
|
||||||
|
'[%s] Failed to compose message:',
|
||||||
|
this.fullName,
|
||||||
|
e.message
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boundMessageHandler = this.messageHandler.bind(this);
|
||||||
|
private stopHandler = this.stop.bind(this, true);
|
||||||
|
}
|
@ -17,6 +17,10 @@
|
|||||||
"name": "simplecommands",
|
"name": "simplecommands",
|
||||||
"version": "1.1.4"
|
"version": "1.1.4"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "socket",
|
||||||
|
"version": "0.0.1"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "xprotocol",
|
"name": "xprotocol",
|
||||||
"version": "1.0.1"
|
"version": "1.0.1"
|
||||||
|
Loading…
Reference in New Issue
Block a user