diff --git a/modules/lobbies/config.ts b/modules/lobbies/config.ts index 1b5c4d4f..cee7a2c3 100644 --- a/modules/lobbies/config.ts +++ b/modules/lobbies/config.ts @@ -2,6 +2,8 @@ import { BackendLocalDevelopmentConfig, BackendLocalDevelopmentConfigPort } from import { BackendServerConfig } from "./utils/lobby/backend/server.ts"; import { BackendTestConfig } from "./utils/lobby/backend/test.ts"; +import type { CaptchaProvider as ExternalCaptchaProvider } from "../captcha/utils/types.ts"; + export interface Config { lobbies: LobbyConfig; lobbyRules: LobbyRule[]; @@ -11,6 +13,28 @@ export interface Config { unconnectedExpireAfter: number; autoDestroyAfter?: number; }; + captcha?: CaptchaConfig | null; +} + +export type CaptchaProvider = ExternalCaptchaProvider; + +export interface RateLimitConfig { + period: number; + requests: number; +} + + +export type RateLimitByEndpointId = { + list: RateLimitConfig; + create: RateLimitConfig; + join: RateLimitConfig; + find_or_create: RateLimitConfig; + find: RateLimitConfig; +}; + +export interface CaptchaConfig { + provider: CaptchaProvider; + endpointRateLimits: Partial; } export interface LobbyRule { diff --git a/modules/lobbies/module.json b/modules/lobbies/module.json index 837c1ab1..36912fc5 100644 --- a/modules/lobbies/module.json +++ b/modules/lobbies/module.json @@ -160,7 +160,8 @@ }, "dependencies": { "tokens": {}, - "rivet": {} + "rivet": {}, + "captcha": {} }, "defaultConfig": { "lobbies": { diff --git a/modules/lobbies/scripts/create.ts b/modules/lobbies/scripts/create.ts index 6b7ad0f1..9069a280 100644 --- a/modules/lobbies/scripts/create.ts +++ b/modules/lobbies/scripts/create.ts @@ -9,10 +9,11 @@ import { PlayerRequest, PlayerResponseWithToken, } from "../utils/player.ts"; +import { getCaptchaProvider, getRateLimitConfigByEndpoint } from "../utils/captcha_config.ts"; export interface Request { version: string; - region: string; + region: string; tags?: Record; maxPlayers: number; maxPlayersDirect: number; @@ -20,6 +21,8 @@ export interface Request { players: PlayerRequest[]; noWait?: boolean; + + captchaToken?: string; } export interface Response { @@ -33,6 +36,19 @@ export async function run( ctx: ScriptContext, req: Request, ): Promise { + const rateLimitConfig = getRateLimitConfigByEndpoint(ctx.config, "create"); + const captchaProvider = getCaptchaProvider(ctx.config); + if (captchaProvider !== null) { + await ctx.modules.captcha.guard({ + key: "default", + period: rateLimitConfig.period, + requests: rateLimitConfig.requests, + type: "lobbies.create", + captchaToken: req.captchaToken, + captchaProvider: captchaProvider + }); + } + const lobbyId = crypto.randomUUID(); const { lobby, players } = await ctx.actors.lobbyManager diff --git a/modules/lobbies/scripts/find.ts b/modules/lobbies/scripts/find.ts index 726c30a7..48392471 100644 --- a/modules/lobbies/scripts/find.ts +++ b/modules/lobbies/scripts/find.ts @@ -9,13 +9,15 @@ import { PlayerRequest, PlayerResponseWithToken, } from "../utils/player.ts"; +import { getCaptchaProvider, getRateLimitConfigByEndpoint } from "../utils/captcha_config.ts"; export interface Request { version: string; - regions?: string[]; + regions?: string[]; tags?: Record; players: PlayerRequest[]; - noWait?: boolean; + noWait?: boolean; + captchaToken?: string; } export interface Response { @@ -27,6 +29,19 @@ export async function run( ctx: ScriptContext, req: Request, ): Promise { + const rateLimitConfig = getRateLimitConfigByEndpoint(ctx.config, "find"); + const captchaProvider = getCaptchaProvider(ctx.config); + if (captchaProvider !== null) { + await ctx.modules.captcha.guard({ + key: "default", + period: rateLimitConfig.period, + requests: rateLimitConfig.requests, + type: "lobbies.find", + captchaToken: req.captchaToken, + captchaProvider: captchaProvider + }); + } + const { lobby, players } = await ctx.actors.lobbyManager .getOrCreateAndCall( "default", diff --git a/modules/lobbies/scripts/find_or_create.ts b/modules/lobbies/scripts/find_or_create.ts index 3b51c8c2..a8a6f166 100644 --- a/modules/lobbies/scripts/find_or_create.ts +++ b/modules/lobbies/scripts/find_or_create.ts @@ -9,13 +9,14 @@ import { PlayerRequest, PlayerResponseWithToken, } from "../utils/player.ts"; +import { getCaptchaProvider, getRateLimitConfigByEndpoint } from "../utils/captcha_config.ts"; export interface Request { version: string; - regions?: string[]; + regions?: string[]; tags?: Record; players: PlayerRequest[]; - noWait?: boolean; + noWait?: boolean; createConfig: { region: string; @@ -23,6 +24,8 @@ export interface Request { maxPlayers: number; maxPlayersDirect: number; }; + + captchaToken?: string; } export interface Response { @@ -34,6 +37,19 @@ export async function run( ctx: ScriptContext, req: Request, ): Promise { + const rateLimitConfig = getRateLimitConfigByEndpoint(ctx.config, "find_or_create"); + const captchaProvider = getCaptchaProvider(ctx.config); + if (captchaProvider !== null) { + await ctx.modules.captcha.guard({ + key: "default", + period: rateLimitConfig.period, + requests: rateLimitConfig.requests, + type: "lobbies.find_or_create", + captchaToken: req.captchaToken, + captchaProvider: captchaProvider + }); + } + const lobbyId = crypto.randomUUID(); const { lobby, players } = await ctx.actors diff --git a/modules/lobbies/scripts/join.ts b/modules/lobbies/scripts/join.ts index 9eb23a84..1076af65 100644 --- a/modules/lobbies/scripts/join.ts +++ b/modules/lobbies/scripts/join.ts @@ -9,11 +9,14 @@ import { PlayerRequest, PlayerResponseWithToken, } from "../utils/player.ts"; +import { getCaptchaProvider, getRateLimitConfigByEndpoint } from "../utils/captcha_config.ts"; export interface Request { lobbyId: string; players: PlayerRequest[]; - noWait?: boolean; + noWait?: boolean; + + captchaToken?: string; } export interface Response { @@ -25,6 +28,19 @@ export async function run( ctx: ScriptContext, req: Request, ): Promise { + const rateLimitConfig = getRateLimitConfigByEndpoint(ctx.config, "join"); + const captchaProvider = getCaptchaProvider(ctx.config); + if (captchaProvider !== null) { + await ctx.modules.captcha.guard({ + key: "default", + period: rateLimitConfig.period, + requests: rateLimitConfig.requests, + type: "lobbies.join", + captchaToken: req.captchaToken, + captchaProvider: captchaProvider + }); + } + const { lobby, players } = await ctx.actors .lobbyManager.getOrCreateAndCall( "default", diff --git a/modules/lobbies/scripts/list.ts b/modules/lobbies/scripts/list.ts index 8c9b5e07..dc763790 100644 --- a/modules/lobbies/scripts/list.ts +++ b/modules/lobbies/scripts/list.ts @@ -3,11 +3,14 @@ import { ListLobbiesResponse, } from "../utils/lobby_manager/rpc.ts"; import { ScriptContext } from "../module.gen.ts"; +import { getCaptchaProvider, getRateLimitConfigByEndpoint } from "../utils/captcha_config.ts"; export interface Request { version: string; - regions?: string[]; + regions?: string[]; tags?: Record; + + captchaToken?: string; } export interface Response { @@ -29,8 +32,20 @@ export async function run( ctx: ScriptContext, req: Request, ): Promise { - // TODO: Cache this without hitting the DO + const rateLimitConfig = getRateLimitConfigByEndpoint(ctx.config, "list"); + const captchaProvider = getCaptchaProvider(ctx.config); + if (captchaProvider !== null) { + await ctx.modules.captcha.guard({ + key: "default", + period: rateLimitConfig.period, + requests: rateLimitConfig.requests, + type: "lobbies.list", + captchaToken: req.captchaToken, + captchaProvider: captchaProvider + }); + } + // TODO: Cache this without hitting the DO const { lobbies } = await ctx.actors.lobbyManager.getOrCreateAndCall< undefined, ListLobbiesRequest, diff --git a/modules/lobbies/utils/captcha_config.ts b/modules/lobbies/utils/captcha_config.ts new file mode 100644 index 00000000..bec83581 --- /dev/null +++ b/modules/lobbies/utils/captcha_config.ts @@ -0,0 +1,39 @@ +import { Config, CaptchaProvider, RateLimitByEndpointId, RateLimitConfig } from "../config.ts"; + +export function getCaptchaProvider(config: Config): CaptchaProvider | null { + if (!config.captcha || !config.captcha.provider) return null; + + return config.captcha.provider; +} + +type KnownEndpoint = keyof RateLimitByEndpointId; + +const endpointRateLimits: RateLimitByEndpointId = { + list: { + period: 20, + requests: 5 + }, + create: { + period: 15, + requests: 5 + }, + join: { + period: 15, + requests: 8 + }, + find_or_create: { + period: 15, + requests: 8 + }, + find: { + period: 15, + requests: 10 + } +} + +export function getRateLimitConfigByEndpoint( + config: Config, + endpoint: KnownEndpoint +): Readonly { + return config.captcha?.endpointRateLimits?.[endpoint] ?? endpointRateLimits[endpoint]; +} \ No newline at end of file