Skip to content

Commit 9c673e7

Browse files
authored
Merge pull request #139 from MatteoGheza/feat_rate_limiter
Support for rate limiter and brute force protection
2 parents 86c42b0 + 7ce3497 commit 9c673e7

File tree

6 files changed

+84
-15
lines changed

6 files changed

+84
-15
lines changed

UI/src/app/_components/login/login.component.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<div class="container text-center d-flex justify-content-center mt-5 pt-5">
22
<main class="form-signin">
33
<h1 class="h3 mb-3 fw-normal">Please sign in</h1>
4-
<div class="my-2 text-danger" *ngIf="wrongUsernameOrPassword">Wrong username or password!</div>
4+
<div class="my-2 text-danger" *ngIf="!loginResponse.loginOk">{{ loginResponse.message }}</div>
55
<div class="form-floating">
66
<input type="text" class="form-control" (keydown.enter)="inputPassword.focus()" [(ngModel)]="username" id="username" placeholder="Username">
77
<label for="username">Username</label>

UI/src/app/_components/login/login.component.ts

+5-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
22
import { Router } from '@angular/router';
3-
import { AuthService } from 'src/app/_services/auth.service';
3+
import { AuthService, LoginResponse } from 'src/app/_services/auth.service';
44

55
@Component({
66
selector: 'app-login',
@@ -9,7 +9,7 @@ import { AuthService } from 'src/app/_services/auth.service';
99
})
1010
export class LoginComponent {
1111
public loading = false;
12-
public wrongUsernameOrPassword = false;
12+
public loginResponse: LoginResponse = {loginOk: false, message: ''};
1313
public username = "";
1414
public password = "";
1515
private type!: "producer" | "settings";
@@ -21,12 +21,11 @@ export class LoginComponent {
2121
login(): void {
2222
this.loading = true;
2323
this.type = this.router.url.endsWith("producer") ? "producer" : "settings";
24-
this.authService.login(this.type, this.username, this.password).then((result) => {
24+
this.authService.login(this.type, this.username, this.password).then((response: LoginResponse) => {
25+
this.loginResponse = response;
2526
this.loading = false;
26-
if (result === true) {
27+
if (response.loginOk === true) {
2728
this.router.navigate([this.type]);
28-
} else {
29-
this.wrongUsernameOrPassword = true;
3029
}
3130
});
3231
}

UI/src/app/_services/auth.service.ts

+9-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { Injectable } from '@angular/core';
22
import { SocketService } from './socket.service';
33

4+
export interface LoginResponse {
5+
loginOk: boolean;
6+
message: string;
7+
}
8+
49
@Injectable({
510
providedIn: 'root'
611
})
@@ -18,19 +23,17 @@ export class AuthService {
1823
}
1924

2025
public login(type: "producer" | "settings", username: string, password: string) {
21-
return new Promise((resolve) => {
26+
return new Promise<LoginResponse>((resolve) => {
2227
this.socketService.socket.emit("login", type, username, password);
23-
this.socketService.socket.once("login_result", (result: boolean) => {
24-
if (result === true) {
28+
this.socketService.socket.once("login_response", (response: LoginResponse) => {
29+
if (response.loginOk === true) {
2530
if (type == "producer") {
2631
this._isProducer = true;
2732
} else {
2833
this._isAdmin = true;
2934
}
30-
resolve(true);
31-
} else {
32-
resolve(false);
3335
}
36+
resolve(response);
3437
})
3538
})
3639
}

index.js

+63-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const isPi = require('detect-rpi');
1515
const clc = require('cli-color');
1616
const util = require ('util');
1717
const express = require('express');
18+
const { RateLimiterMemory } = require('rate-limiter-flexible');
1819
const bodyParser = require('body-parser');
1920
const axios = require('axios');
2021
const http = require('http');
@@ -26,6 +27,21 @@ const jspack = require('jspack').jspack;
2627
const os = require('os') // For getting available Network interfaces on host device
2728
const findRemoveSync = require('find-remove');
2829

30+
//Rate limiter configurations
31+
const maxWrongAttemptsByIPperDay = 100;
32+
const maxConsecutiveFailsByUsernameAndIP = 10;
33+
const limiterSlowBruteByIP = new RateLimiterMemory({
34+
keyPrefix: 'login_fail_ip_per_day',
35+
points: maxWrongAttemptsByIPperDay,
36+
duration: 60 * 60 * 24, // Store number for 1 day since first fail
37+
blockDuration: 60 * 60 * 24, // Block for 1 day
38+
});
39+
const limiterConsecutiveFailsByUsernameAndIP = new RateLimiterMemory({
40+
keyPrefix: 'login_fail_consecutive_username_and_ip',
41+
points: maxConsecutiveFailsByUsernameAndIP,
42+
duration: 60 * 60 * 24, // Store number for 1 day since first fail
43+
blockDuration: 60 * 60 * 2, // Block for 2 hours
44+
});
2945

3046
//Tally Arbiter variables
3147
const listenPort = process.env.PORT || 4455;
@@ -741,6 +757,22 @@ function startUp() {
741757
});*/
742758
}
743759

760+
//based on https://stackoverflow.com/a/37096512
761+
//used in login function for displaying rate limits
762+
function secondsToHms(d) {
763+
d = Number(d);
764+
var h = Math.floor(d / 3600);
765+
var m = Math.floor(d % 3600 / 60);
766+
var s = Math.floor(d % 3600 % 60);
767+
768+
var hDisplay = h > 0 ? h + (h == 1 ? " hour, " : " hours, ") : "";
769+
var mDisplay = m > 0 ? m + (m == 1 ? " minute, " : " minutes, ") : "";
770+
var sDisplay = s > 0 ? s + (s == 1 ? " second" : " seconds") : "";
771+
hmsString = hDisplay + mDisplay + sDisplay;
772+
if(hmsString.endsWith(", ")) hmsString = hmsString.slice(0, -2);
773+
return hmsString;
774+
}
775+
744776
//sets up the REST API and GUI pages and starts the Express server that will listen for incoming requests
745777
function initialSetup() {
746778
logger('Setting up the REST API.', 'info-quiet');
@@ -927,10 +959,39 @@ function initialSetup() {
927959
logger('Starting socket.IO Setup.', 'info-quiet');
928960

929961
io.sockets.on('connection', function(socket) {
962+
const ipAddr = socket.handshake.address;
930963

931964
socket.on('login', function (type, username, password) {
932-
socket.emit('login_result', (type === "producer" && username == username_producer && password == password_producer)
933-
|| (type === "settings" && username == username_settings && password == password_settings));
965+
if((type === "producer" && username == username_producer && password == password_producer)
966+
|| (type === "settings" && username == username_settings && password == password_settings)) {
967+
//login successfull
968+
socket.emit('login_result', true); //old response, for compatibility with old UI clients
969+
socket.emit('login_response', { loginOk: true, message: "" });
970+
} else {
971+
//wrong credentials
972+
Promise.all([
973+
limiterConsecutiveFailsByUsernameAndIP.consume(ipAddr),
974+
limiterSlowBruteByIP.consume(`${username}_${ipAddr}`)
975+
]).then((values) => {
976+
//rate limits not exceeded
977+
let points = values[0].remainingPoints;
978+
let message = "Wrong username or password!";
979+
if(points < 4) {
980+
message += " Remaining attemps:"+points;
981+
}
982+
socket.emit('login_result', false); //old response, for compatibility with old UI clients
983+
socket.emit('login_response', { loginOk: false, message: message });
984+
}).catch((error) => {
985+
//rate limits exceeded
986+
socket.emit('login_result', false); //old response, for compatibility with old UI clients
987+
try{
988+
retrySecs = Math.round(error.msBeforeNext / 1000) || 1;
989+
} catch(e) {
990+
retrySecs = Math.round(error[0].msBeforeNext / 1000) || 1;
991+
}
992+
socket.emit('login_response', { loginOk: false, message: "Too many attemps! Please try "+secondsToHms(retrySecs)+" later." });
993+
});
994+
}
934995
});
935996

936997
socket.on('version', function() {

package-lock.json

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"obs-websocket-js": "^4.0.2",
6161
"osc": "^2.4.1",
6262
"packet": "0.0.7",
63+
"rate-limiter-flexible": "^2.2.4",
6364
"socket.io": "4.1.2",
6465
"socket.io-client": "4.1.2",
6566
"tsl-umd": "^1.1.2",

0 commit comments

Comments
 (0)