From 445c6919f1218da9d753fb525ac713f812d590b3 Mon Sep 17 00:00:00 2001 From: Rudy Marchandise Date: Sat, 29 May 2021 22:59:36 +0200 Subject: [PATCH] feat(threshold): security command threshold per second --- .dockerignore | 132 +++++++++++++++++++++++++++ README.md | 4 +- src/Configuration.ts | 4 +- src/services/TwitchService.ts | 29 +++++- src/services/YamlService.ts | 1 + tests/Configuration.spec.ts | 8 +- tests/services/TwitchService.spec.ts | 45 ++++++++- 7 files changed, 211 insertions(+), 12 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2db0656 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,132 @@ +### Markdown ### +*.md +coverage/* +www/* +tests/* + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test +.env*.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Storybook build outputs +.out +.storybook-out +storybook-static + +# rollup.js default build output +dist/ + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# Temporary folders +tmp/ +temp/ + diff --git a/README.md b/README.md index a56842a..5acd5fb 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,10 @@ The versioning scheme is [SemVer](http://semver.org/). LARBIN_FILE=/tmp # Debug mode DEBUG=true +# Single command threshold per second +LARBIN_THRESHOLD=5 -# Twitch Credentials +# Twitch Credentials (mandatory) LARBIN_TWITCH_USERNAME: Larbin LARBIN_TWITCH_PASSWORD: oic:password LARBIN_TWITCH_CHANNEL: example diff --git a/src/Configuration.ts b/src/Configuration.ts index 6826c0e..1cc985d 100644 --- a/src/Configuration.ts +++ b/src/Configuration.ts @@ -7,6 +7,7 @@ export class AppConfiguration { public Debug: boolean; public ConfigurationPath: string; public ConfigurationFile: string; + public ThresholdInSeconds: number; } /** @@ -36,7 +37,8 @@ export class Configuration implements IConfiguration { this.App = { Debug: process.env.DEBUG?.toLocaleLowerCase() == 'true' ?? false, ConfigurationPath: process.env.LARBIN_FILE as string || __dirname, - ConfigurationFile: 'larbin.yml' + ConfigurationFile: 'larbin.yml', + ThresholdInSeconds: Number.parseInt(process.env.LARBIN_THRESHOLD as string || '5') }; // Twitch diff --git a/src/services/TwitchService.ts b/src/services/TwitchService.ts index e2601a4..4388883 100644 --- a/src/services/TwitchService.ts +++ b/src/services/TwitchService.ts @@ -25,6 +25,7 @@ export interface ITwitchService { @singleton() export class TwitchService implements ITwitchService { + private _thresholdKeyDate: Map = new Map(); private _commands: Array = new Array(); private _schedulers: Array = new Array(); private _schedulersIntervals: Array = new Array(); @@ -45,14 +46,32 @@ export class TwitchService implements ITwitchService { public Listen(): void { this.StartSchedulers(); - this._client.on('message', (channels, userstate, message) => { - const command = this._commands.find((x) => message.startsWith(x.Trigger)); - if (command != undefined) { - if (command.CanAction(userstate, this._configuration)) { + this._client.on('message', this._processTwitchMessage); + } + + private _processTwitchMessage(channels: string, userstate: any, message: string) { + const command = this._commands.find((x) => message.startsWith(x.Trigger)); + if (command != undefined) { + if (command.CanAction(userstate, this._configuration)) { + if (this._thresholdValidation(command.Trigger)) { command.Action(this, message, userstate); } } - }); + } + } + + private _thresholdValidation(key: string): boolean { + const lastTrigger = this._thresholdKeyDate.get(key) ?? new Date(1970, 1); + const epochNow = new Date().getTime(); + const epochLastTrigger = lastTrigger.getTime(); + const thresholdInMiliseconds = this._configuration.App.ThresholdInSeconds * 1000; + + if (!(epochLastTrigger + thresholdInMiliseconds < epochNow)) { + return false; + } + + this._thresholdKeyDate.set(key, new Date()); + return true; } public Write(message: string): void { diff --git a/src/services/YamlService.ts b/src/services/YamlService.ts index 67932ba..f5ea737 100644 --- a/src/services/YamlService.ts +++ b/src/services/YamlService.ts @@ -68,6 +68,7 @@ export class YamlService implements IYamlService { Others: policies.others ?? defaultPolicies.Others } as CommandPolicies; } + public getCommands(): Array { const yamlContent = this.getYamlContent(); const commands = new Array(); diff --git a/tests/Configuration.spec.ts b/tests/Configuration.spec.ts index f1a1a20..a4319fc 100644 --- a/tests/Configuration.spec.ts +++ b/tests/Configuration.spec.ts @@ -7,6 +7,7 @@ describe('Configuration', function () { // Arrange process.env.DEBUG = 'true'; process.env.LARBIN_FILE = '/tmp/example'; + process.env.LARBIN_THRESHOLD = '10'; // Act const configuration = new Configuration(); @@ -15,7 +16,8 @@ describe('Configuration', function () { expect(configuration.App).toStrictEqual({ Debug: true, ConfigurationPath: '/tmp/example', - ConfigurationFile: 'larbin.yml' + ConfigurationFile: 'larbin.yml', + ThresholdInSeconds: 10 } as AppConfiguration); }); @@ -23,6 +25,7 @@ describe('Configuration', function () { // Arrange delete process.env.DEBUG; delete process.env.LARBIN_FILE; + delete process.env.LARBIN_THRESHOLD; // Act const configuration = new Configuration(); @@ -33,7 +36,8 @@ describe('Configuration', function () { expect(configuration.App).toStrictEqual({ Debug: false, ConfigurationPath: configuration.App.ConfigurationPath, - ConfigurationFile: 'larbin.yml' + ConfigurationFile: 'larbin.yml', + ThresholdInSeconds: 5 } as AppConfiguration); }); diff --git a/tests/services/TwitchService.spec.ts b/tests/services/TwitchService.spec.ts index e320436..8456084 100644 --- a/tests/services/TwitchService.spec.ts +++ b/tests/services/TwitchService.spec.ts @@ -1,24 +1,30 @@ import 'reflect-metadata' -import { IConfiguration, TwitchConfiguration } from '../../src/Configuration'; +import { AppConfiguration, IConfiguration, TwitchConfiguration } from '../../src/Configuration'; import { ILoggerService, ITwitchService, TwitchService } from '../../src/services'; import { It, Mock, Times } from 'moq.ts'; import { ITmiFactory } from '../../src/factory/TmiFactory'; -import { Client } from 'tmi.js'; +import { ChatUserstate, Client } from 'tmi.js'; +import { CommandPolicies, ICommand } from '../../src/lib/Commands'; describe('Service - Twitch', function () { - let twitchService : ITwitchService; + let twitchService : TwitchService; let mockConfiguration : Mock; const twitchConfiguration = { Channel: 'TestChannel' } as TwitchConfiguration; + const appConfiguration = { + ThresholdInSeconds: 2 + } as AppConfiguration; let mockLoggerService : Mock; let mockTmiFactory : Mock; let mockClient : Mock; + let mockCommand: ICommand; beforeEach(() => { // Mock mockConfiguration = new Mock(); mockConfiguration.setup(x => x.Twitch).returns(twitchConfiguration); + mockConfiguration.setup(x => x.App).returns(appConfiguration); mockLoggerService = new Mock(); mockTmiFactory = new Mock(); mockClient = new Mock(); @@ -26,8 +32,41 @@ describe('Service - Twitch', function () { // Service twitchService = new TwitchService(mockLoggerService.object(), mockConfiguration.object(), mockTmiFactory.object()); + + // Commands + const policies = new CommandPolicies(); + policies.Others = true; + + mockCommand = { + Action: () => {}, + CanAction: () => true, + Policies: policies, + Trigger: '!test' + }; + + twitchService.AddCommand(mockCommand); }); + it('Threshold', async function () { + // Arrange + const sleep = () => new Promise(resolve => setTimeout(resolve, 2000)); + + // Act + const result1 = twitchService["_thresholdValidation"](mockCommand.Trigger); + const result2 = twitchService["_thresholdValidation"](mockCommand.Trigger); + const result3 = twitchService["_thresholdValidation"](mockCommand.Trigger); + await sleep(); + const result4 = twitchService["_thresholdValidation"](mockCommand.Trigger); + const result5 = twitchService["_thresholdValidation"](mockCommand.Trigger); + + // Assert + expect(result1).toBeTruthy(); + expect(result2).toBeFalsy(); + expect(result3).toBeFalsy(); + expect(result4).toBeTruthy(); + expect(result5).toBeFalsy(); + }, 5 * 1000); + it('Write', async function () { // Arrange const message = 'Hello !';