-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
633 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { ActorBase, ActorContext, Empty } from "../module.gen.ts"; | ||
import { ThrottleRequest, ThrottleResponse } from "../utils/types.ts"; | ||
|
||
type Input = undefined; | ||
|
||
interface State { | ||
start: number; | ||
count: number; | ||
} | ||
|
||
export class Actor extends ActorBase<Input, State> { | ||
public initialize(_ctx: ActorContext): State { | ||
// Will refill on first call of `throttle` | ||
return { | ||
start: 0, | ||
count: 0, | ||
}; | ||
} | ||
|
||
throttle(_ctx: ActorContext, req: ThrottleRequest): ThrottleResponse { | ||
const now = Date.now(); | ||
|
||
if (now - this.state.start > req.period) { | ||
this.state.start = now; | ||
this.state.count = 1; | ||
return { success: true }; | ||
} | ||
|
||
if (this.state.count >= req.requests) { | ||
return { success: false }; | ||
} | ||
|
||
this.state.count += 1; | ||
|
||
return { success: true }; | ||
} | ||
|
||
reset(_ctx: ActorContext, req: Empty): Empty { | ||
this.state.start = 0; | ||
this.state.count = 0; | ||
|
||
return {}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
{ | ||
"status": "stable", | ||
"name": "Captcha", | ||
"description": "", | ||
"icon": "", | ||
"tags": [], | ||
"authors": [ | ||
"rivet-gg", | ||
"ABCxFF" | ||
], | ||
"scripts": { | ||
"verify_captcha_token": { | ||
"name": "Verify Captcha Response", | ||
"public": false | ||
}, | ||
"guard": { | ||
"name": "Ratelimit Guarded with Captcha Challenge", | ||
"public": false | ||
} | ||
}, | ||
"errors": { | ||
"captcha_failed": { | ||
"name": "Captcha Challenge Failed", | ||
"internal": false | ||
}, | ||
"captcha_needed": { | ||
"name": "Captcha Required (Rate Limit Exceeded)", | ||
"internal": false | ||
} | ||
}, | ||
"actors": { | ||
"throttle": {} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { RuntimeError, ScriptContext } from "../module.gen.ts"; | ||
import { getPublicConfig } from "../utils/get_sitekey.ts"; | ||
// import { getPublicConfig } from "../utils/get_sitekey.ts"; | ||
import type { CaptchaProvider, ThrottleRequest, ThrottleResponse } from "../utils/types.ts"; | ||
|
||
export interface Request { | ||
type: string; | ||
key: string; | ||
requests: number; | ||
period: number; | ||
captchaToken?: string | null, | ||
captchaProvider: CaptchaProvider | ||
} | ||
|
||
export type Response = Record<string, never>; | ||
|
||
export async function run( | ||
ctx: ScriptContext, | ||
req: Request, | ||
): Promise<Response> { | ||
const key = `${JSON.stringify(req.type)}.${JSON.stringify(req.key)}`; | ||
|
||
if (req.captchaToken) { | ||
try { | ||
await ctx.modules.captcha.verifyCaptchaToken({ | ||
token: req.captchaToken, | ||
provider: req.captchaProvider | ||
}); | ||
|
||
await ctx.actors.throttle.getOrCreateAndCall<undefined, {}, {}>(key, undefined, "reset", {}); | ||
|
||
return {}; | ||
} catch { | ||
// If we error, it means the captcha failed, we can continue with our normal ratelimitting | ||
} | ||
} | ||
|
||
const res = await ctx.actors.throttle.getOrCreateAndCall< | ||
undefined, | ||
ThrottleRequest, | ||
ThrottleResponse | ||
>(key, undefined, "throttle", { | ||
requests: req.requests, | ||
period: req.period, | ||
}); | ||
|
||
if (!res.success) { | ||
throw new RuntimeError("captcha_needed", { | ||
meta: getPublicConfig(req.captchaProvider) | ||
}); | ||
} | ||
|
||
return {}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { RuntimeError, ScriptContext } from "../module.gen.ts"; | ||
import { validateHCaptchaResponse } from "../utils/providers/hcaptcha.ts"; | ||
import { validateCFTurnstileResponse } from "../utils/providers/turnstile.ts"; | ||
// import { validateHCaptchaResponse } from "../providers/hcaptcha.ts"; | ||
// import { validateCFTurnstileResponse } from "../providers/turnstile.ts"; | ||
import { CaptchaProvider } from "../utils/types.ts"; | ||
|
||
export interface Request { | ||
token: string, | ||
provider: CaptchaProvider | ||
} | ||
|
||
export type Response = Record<string, never>; | ||
|
||
export async function run( | ||
ctx: ScriptContext, | ||
req: Request, | ||
): Promise<Response> { | ||
const captchaToken = req.token; | ||
const captchaProvider = req.provider; | ||
|
||
let success: boolean = false; | ||
if ("hcaptcha" in captchaProvider) { | ||
success = await validateHCaptchaResponse(captchaProvider.hcaptcha.secret, captchaToken); | ||
} else if ("turnstile" in captchaProvider) { | ||
success = await validateCFTurnstileResponse(captchaProvider.turnstile.secret, captchaToken); | ||
} else { | ||
success = true; | ||
} | ||
|
||
if (!success) { | ||
throw new RuntimeError("captcha_failed"); | ||
} | ||
|
||
return {}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { test, TestContext } from "../module.gen.ts"; | ||
import { assertEquals } from "https://deno.land/[email protected]/assert/mod.ts"; | ||
|
||
const didFail = async (x: () => Promise<void>) => { | ||
try { | ||
await x(); | ||
return false | ||
} catch { | ||
return true; | ||
} | ||
} | ||
|
||
test("e2e success and failure", async (ctx: TestContext) => { | ||
const PERIOD = 5000; | ||
const REQUESTS = 5; | ||
|
||
const captchaProvider = { | ||
turnstile: { | ||
secret: "0x0000000000000000000000000000000000000000", | ||
sitekey: "" // doesn't really matter here | ||
} | ||
} | ||
|
||
assertEquals(false, await didFail(async () => { | ||
for (let i = 0; i < REQUESTS; ++i) { | ||
await ctx.modules.captcha.guard({ | ||
type: "ip", | ||
key: "aaaa", | ||
requests: REQUESTS, | ||
period: PERIOD, | ||
captchaProvider | ||
}); | ||
} | ||
})); | ||
|
||
assertEquals(true, await didFail(async () => { | ||
await ctx.modules.captcha.guard({ | ||
type: "ip", | ||
key: "aaaa", | ||
requests: REQUESTS, | ||
period: PERIOD, | ||
captchaProvider | ||
}); | ||
})); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import { test, TestContext } from "../module.gen.ts"; | ||
import { assertEquals } from "https://deno.land/[email protected]/assert/mod.ts"; | ||
|
||
const didFail = async (x: () => Promise<void>) => { | ||
try { | ||
await x(); | ||
return false | ||
} catch { | ||
return true; | ||
} | ||
} | ||
|
||
test( | ||
"hcaptcha success and failure", | ||
async (ctx: TestContext) => { | ||
const shouldBeFalse = await didFail(async () => { | ||
await ctx.modules.captcha.verifyCaptchaToken({ | ||
provider: { | ||
hcaptcha: { | ||
secret: "0x0000000000000000000000000000000000000000", | ||
sitekey: "" // doesn't really matter here | ||
} | ||
}, | ||
token: "10000000-aaaa-bbbb-cccc-000000000001" | ||
}); | ||
}); | ||
assertEquals(shouldBeFalse, false); | ||
|
||
const shouldBeTrue = await didFail(async () => { | ||
await ctx.modules.captcha.verifyCaptchaToken({ | ||
provider: { | ||
hcaptcha: { | ||
secret: "0x0000000000000000000000000000000000000000", | ||
sitekey: "" // doesn't really matter here | ||
} | ||
}, | ||
token: "lorem" | ||
}); | ||
}); | ||
assertEquals(shouldBeTrue, true); | ||
}, | ||
); | ||
|
||
test( | ||
"turnstile success and failure", | ||
async (ctx: TestContext) => { | ||
// Always passes | ||
const shouldBeTrue = await didFail(async () => { | ||
await ctx.modules.captcha.verifyCaptchaToken({ | ||
provider: { | ||
turnstile: { | ||
secret: "2x0000000000000000000000000000000AA", | ||
sitekey: "" // doesn't really matter here | ||
} | ||
}, | ||
token: "lorem" | ||
}); | ||
}); | ||
assertEquals(shouldBeTrue, true); | ||
|
||
// Always fails | ||
const shouldBeFalse = await didFail(async () => { | ||
await ctx.modules.captcha.verifyCaptchaToken({ | ||
provider: { | ||
turnstile: { | ||
secret: "1x0000000000000000000000000000000AA", | ||
sitekey: "" // doesn't really matter here | ||
} | ||
}, | ||
token: "ipsum" | ||
}); | ||
}); | ||
assertEquals(shouldBeFalse, false); | ||
}, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { CaptchaProvider, PublicCaptchaProviderConfig } from "./types.ts"; | ||
|
||
export const getPublicConfig = (provider: CaptchaProvider): PublicCaptchaProviderConfig => { | ||
if ("hcaptcha" in provider) { | ||
return { | ||
hcaptcha: { sitekey: provider.hcaptcha.sitekey } | ||
}; | ||
} else if ("turnstile" in provider) { | ||
return { | ||
turnstile: { | ||
sitekey: provider.turnstile.sitekey | ||
} | ||
} | ||
} else { | ||
return { | ||
test: {} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
const API = "https://api.hcaptcha.com/siteverify"; | ||
export const validateHCaptchaResponse = async ( | ||
secret: string, | ||
response: string | ||
): Promise<boolean> => { | ||
try { | ||
const body = new FormData(); | ||
body.append("secret", secret); | ||
body.append("response", response); | ||
const result = await fetch(API, { | ||
body, | ||
method: "POST", | ||
}); | ||
|
||
const { success } = await result.json(); | ||
|
||
return success; | ||
} catch {} | ||
|
||
return false; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
const API = "https://challenges.cloudflare.com/turnstile/v0/siteverify"; | ||
export const validateCFTurnstileResponse = async ( | ||
secret: string, | ||
response: string | ||
): Promise<boolean> => { | ||
try { | ||
const result = await fetch(API, { | ||
body: JSON.stringify({ secret, response }), | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
} | ||
}); | ||
|
||
const { success } = await result.json(); | ||
|
||
return success; | ||
} catch {} | ||
|
||
return false; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
|
||
|
||
interface ProviderCFTurnstile { | ||
sitekey: string; | ||
secret: string; | ||
} | ||
|
||
interface ProviderHCaptcha { | ||
// TODO: Score threshold | ||
sitekey: string; | ||
secret: string; | ||
} | ||
type PublicCFTurnstileConfig = { sitekey: string; } | ||
type PublicHCaptchaConfig = { sitekey: string; } | ||
|
||
export type CaptchaProvider = { test: Record<never, never> } | ||
| { turnstile: ProviderCFTurnstile } | ||
| { hcaptcha: ProviderHCaptcha }; | ||
|
||
export type PublicCaptchaProviderConfig = { test: Record<never, never> } | ||
| { turnstile: PublicCFTurnstileConfig } | ||
| { hcaptcha: PublicHCaptchaConfig }; | ||
|
||
export interface ThrottleRequest { | ||
requests: number; | ||
period: number; | ||
} | ||
|
||
export interface ThrottleResponse { | ||
success: boolean; | ||
} |
Oops, something went wrong.