diff --git a/package.json b/package.json index 51f0ccd6d..6cfc94e48 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "@dnd-kit/modifiers": "^6.0.0", "@dnd-kit/sortable": "^7.0.1", "@getoutline/y-prosemirror": "^1.0.18", + "@hocuspocus/extension-throttle": "1.1.2", "@hocuspocus/provider": "1.1.2", "@hocuspocus/server": "1.1.2", "@joplin/turndown-plugin-gfm": "^1.0.47", diff --git a/server/env.ts b/server/env.ts index 5b9ade57d..a22098e27 100644 --- a/server/env.ts +++ b/server/env.ts @@ -540,6 +540,16 @@ export class Environment { public RATE_LIMITER_REQUESTS = this.toOptionalNumber(process.env.RATE_LIMITER_REQUESTS) ?? 1000; + /** + * Set max allowed realtime connections in a minute before throttling. Defaults + * to 50 requests/ip/min. + */ + @IsOptional() + @IsNumber() + public RATE_LIMITER_COLLABORATION_REQUESTS = + this.toOptionalNumber(process.env.RATE_LIMITER_COLLABORATION_REQUESTS) ?? + 50; + /** * Set fixed duration window(in secs) for default rate limiter, elapsing which * the request quota is reset (the bucket is refilled with tokens). diff --git a/server/services/collaboration.ts b/server/services/collaboration.ts index e40763832..d8e20caf0 100644 --- a/server/services/collaboration.ts +++ b/server/services/collaboration.ts @@ -1,12 +1,14 @@ import http, { IncomingMessage } from "http"; import { Duplex } from "stream"; import url from "url"; +import { Throttle } from "@hocuspocus/extension-throttle"; import { Server } from "@hocuspocus/server"; import Koa from "koa"; import WebSocket from "ws"; import { DocumentValidation } from "@shared/validations"; import { ConnectionLimitExtension } from "@server/collaboration/ConnectionLimitExtension"; import { ViewsExtension } from "@server/collaboration/ViewsExtension"; +import env from "@server/env"; import Logger from "@server/logging/Logger"; import ShutdownHelper, { ShutdownOrder } from "@server/utils/ShutdownHelper"; import AuthenticationExtension from "../collaboration/AuthenticationExtension"; @@ -30,6 +32,11 @@ export default function init( timeout: 30000, maxDebounce: 10000, extensions: [ + new Throttle({ + throttle: env.RATE_LIMITER_COLLABORATION_REQUESTS, + // Ban time is defined in minutes + banTime: 5, + }), new ConnectionLimitExtension(), new AuthenticationExtension(), new PersistenceExtension(), diff --git a/yarn.lock b/yarn.lock index 6f9cacf89..5643049eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1626,6 +1626,13 @@ dependencies: lib0 "^0.2.47" +"@hocuspocus/extension-throttle@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@hocuspocus/extension-throttle/-/extension-throttle-1.1.2.tgz#4b07a72c11775931e740ea83a13ea11def5025e3" + integrity sha512-L5lE4lu7+jm2fOSxiVASjD9PB4A7u4UaqzZQAlNWz4uunsa7Cwy596tyKZtpNXZXoC0C103yn+FeT0TOWMeBIg== + dependencies: + "@hocuspocus/server" "^1.1.2" + "@hocuspocus/provider@1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@hocuspocus/provider/-/provider-1.1.2.tgz#6b20c2446555a602919f0a94d7750adef00e8b16" @@ -1635,7 +1642,7 @@ "@lifeomic/attempt" "^3.0.2" lib0 "^0.2.46" -"@hocuspocus/server@1.1.2": +"@hocuspocus/server@1.1.2", "@hocuspocus/server@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@hocuspocus/server/-/server-1.1.2.tgz#c2ceab9938d4ff679ba22446e70fa3ce71fd6600" integrity sha512-L6YHENRSyXDbYyFGt3S1etJq62XZEj6Z9QUmhqzOjlxQYHC/eaGtrCbAxi5dLKXORl3AY4CVtTfFN+3asmmT6w==