Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: rate limit the public api #899

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Binary file modified bun.lockb
Binary file not shown.
30 changes: 28 additions & 2 deletions cloudflare_workers/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ import { app as on_version_delete } from '../../supabase/functions/_backend/trig
import { app as on_version_update } from '../../supabase/functions/_backend/triggers/on_version_update.ts'
import { app as replicate_data } from '../../supabase/functions/_backend/triggers/replicate_data.ts'
import { app as stripe_event } from '../../supabase/functions/_backend/triggers/stripe_event.ts'
import { rateLimit } from '@elithrar/workers-hono-rate-limit'
import { Context, Next, MiddlewareHandler } from "hono";

export { AttachmentUploadHandler, UploadHandler as TemporaryKeyHandler, UploadHandler } from '../../supabase/functions/_backend/tus/uploadHandler.ts'

Expand All @@ -57,12 +59,32 @@ app.use('*', sentry({
app.use('*', logger())
app.use('*', (requestId as any)())

export function publicRateLimiter(rateLimiterAction: String, _methods: { limit: number, period: number, method: string }[]) {
const subMiddlewareKey: MiddlewareHandler<{}> = async (c: Context, next: Next) => {
const capgkey_string = c.req.header('capgkey')
const apikey_string = c.req.header('authorization')
const key = capgkey_string || apikey_string
if (!key)
return next()
await rateLimit(c.env[`API_${rateLimiterAction}_RATE_LIMITER`], () => key)(c, next);
await next()
}
return subMiddlewareKey
}

// Public API
app.route('/ok', ok)

app.route('/organization', organization)

app.use('/bundle', publicRateLimiter('BUNDLE', [{ limit: 20, period: 10, method: 'GET' }, { limit: 20, period: 10, method: 'DELETE' }]))
app.route('/bundle', bundle)

app.use('/channel', publicRateLimiter('CHANNEL', [{ limit: 20, period: 10, method: 'GET' }, { limit: 20, period: 10, method: 'POST' }, { limit: 20, period: 10, method: 'DELETE' }]))
app.route('/channel', channel)

app.use('/device', publicRateLimiter('DEVICE', [{ limit: 20, period: 10, method: 'GET' }, { limit: 20, period: 10, method: 'POST' }, { limit: 20, period: 10, method: 'DELETE' }]))
app.route('/device', device)
app.route('/organization', organization)

app.route('/on_app_create', on_app_create)

Expand Down Expand Up @@ -135,8 +157,12 @@ app.get('/test_sentry', (c) => {

app.onError((e, c) => {
c.get('sentry').captureException(e)
if (e instanceof HTTPException)
if (e instanceof HTTPException) {
if (e.status === 429) {
return c.json({ error: 'you are beeing rate limited' }, 429)
}
return c.json({ status: 'Internal Server Error', response: e.getResponse(), error: JSON.stringify(e), message: e.message }, 500)
}
console.log('app', 'onError', e)
return c.json({ status: 'Internal Server Error', error: JSON.stringify(e), message: e.message }, 500)
})
Expand Down
47 changes: 47 additions & 0 deletions cloudflare_workers/api/wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,50 @@ d1_databases = [

[env.local]
name = "capgo_api-local"
[[unsafe.bindings]]
name = "API_BUNDLE_GET_RATE_LIMITER"
type = "ratelimit"
namespace_id = "1001"
simple = { limit = 20, period = 10 }

[[unsafe.bindings]]
name = "API_BUNDLE_DELETE_RATE_LIMITER"
type = "ratelimit"
namespace_id = "1001"
simple = { limit = 20, period = 10 }

[[unsafe.bindings]]
name = "API_CHANNEL_GET_RATE_LIMITER"
type = "ratelimit"
namespace_id = "1002"
simple = { limit = 20, period = 10 }

[[unsafe.bindings]]
name = "API_CHANNEL_POST_RATE_LIMITER"
type = "ratelimit"
namespace_id = "1002"
simple = { limit = 20, period = 10 }

[[unsafe.bindings]]
name = "API_CHANNEL_DELETE_RATE_LIMITER"
type = "ratelimit"
namespace_id = "1002"
simple = { limit = 20, period = 10 }

[[unsafe.bindings]]
name = "API_DEVICE_GET_RATE_LIMITER"
type = "ratelimit"
namespace_id = "1003"
simple = { limit = 20, period = 10 }

[[unsafe.bindings]]
name = "API_DEVICE_POST_RATE_LIMITER"
type = "ratelimit"
namespace_id = "1003"
simple = { limit = 20, period = 10 }

[[unsafe.bindings]]
name = "API_DEVICE_DELETE_RATE_LIMITER"
type = "ratelimit"
namespace_id = "1003"
simple = { limit = 20, period = 10 }
65 changes: 63 additions & 2 deletions cloudflare_workers/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,77 @@ import { app as channel_self } from '../../supabase/functions/_backend/plugins/c
import { app as stats } from '../../supabase/functions/_backend/plugins/stats.ts'
import { app as updates } from '../../supabase/functions/_backend/plugins/updates.ts'
import { app as latency_drizzle } from '../../supabase/functions/_backend/private/latency_drizzle.ts'

import { app as update_stats } from '../../supabase/functions/_backend/private/updates_stats.ts'
import { app as ok } from '../../supabase/functions/_backend/public/ok.ts'
import { Context, Next, MiddlewareHandler } from "hono";
import { rateLimit, wasRateLimited } from '@elithrar/workers-hono-rate-limit'
import { z } from 'zod'
import { sendStatsAndDevice } from '../../supabase/functions/_backend/utils/stats.ts'
// import { middlewareAPISecret } from '../../supabase/functions/_backend/utils/hono.ts'

export { AttachmentUploadHandler, UploadHandler } from '../../supabase/functions/_backend/tus/uploadHandler.ts'

const app = new Hono<{ Bindings: Bindings }>()
const zodDeviceIdAppIdSchema = z.object({
device_id: z.string(),
app_id: z.string(),
})

app.use('*', sentry({
release: version,
}))
app.use('*', logger())
app.use('*', (requestId as any)())

const zodDeviceSchema = z.object({
app_id: z.string(),
device_id: z.string(),
plugin_version: z.string(),
version: z.number().optional().default(0),
custom_id: z.string().optional(),
is_emulator: z.boolean().optional(),
is_prod: z.boolean().optional(),
version_build: z.string(),
os_version: z.string().optional(),
platform: z.enum(['ios', 'android']),
updated_at: z.string().optional().default(new Date().toISOString()),
})


export function deviceAppIdRateLimiter(rateLimiterAction: String, _methods: { limit: number, period: number, method: string }[]) {
const subMiddlewareKey: MiddlewareHandler<{}> = async (c: Context, next: Next) => {
let deviceId = ''
let appId = ''
try {
const body = await c.req.json()
const { device_id, app_id } = zodDeviceIdAppIdSchema.parse(body)
deviceId = device_id
appId = app_id
} catch (e) {
console.error('publicRateLimiter', e)
await next()
}
await rateLimit(c.env[`PUBLIC_API_DEVICE_${rateLimiterAction}_${c.req.method}_RATE_LIMITER`], () => `${deviceId}-${appId}`)(c, next);
if (wasRateLimited(c)) {
const device = zodDeviceSchema.safeParse(await c.req.json())
console.log('deviceAppIdRateLimiter', JSON.stringify(device))
if (device.success) {
try {
// this as any should work. There are different honot types for hono and @hono/hono
await sendStatsAndDevice(c as any, device.data, [{ action: 'rateLimited' }])
} catch (e) {
console.error('deviceAppIdRateLimiter', `Error sending stats and device: ${e}`)
}
}
}
}
return subMiddlewareKey
}


// Plugin API
app.route('/plugin/ok', ok)
app.use('/plugin/channel_self', deviceAppIdRateLimiter('CHANNEL_SELF', [{ limit: 20, period: 10, method: 'POST' }, { limit: 20, period: 10, method: 'DELETE' }, { limit: 20, period: 10, method: 'PUT' }, { limit: 20, period: 10, method: 'GET' }]))
app.route('/plugin/channel_self', channel_self)
app.route('/plugin/updates', updates)
app.route('/plugin/updates_v2', updates)
Expand All @@ -35,7 +89,10 @@ app.route('/plugin/stats', stats)
app.route('/plugin/latency_drizzle', latency_drizzle)

// TODO: deprecated remove when everyone use the new endpoint
app.use('/channel_self', deviceAppIdRateLimiter('CHANNEL_SELF', [{ limit: 20, period: 10, method: 'POST' }, { limit: 20, period: 10, method: 'DELETE' }, { limit: 20, period: 10, method: 'PUT' }, { limit: 20, period: 10, method: 'GET' }]))
app.route('/channel_self', channel_self)
// Apply rate limiter middleware before routing to ensure it runs first
app.use('/updates*', deviceAppIdRateLimiter('ALL_UPDATES', [{ limit: 20, period: 10, method: 'POST' }]))
app.route('/updates', updates)
app.route('/updates_v2', updates)
app.route('/updates_debug', updates)
Expand Down Expand Up @@ -77,8 +134,12 @@ app.route('/stats', stats)

app.onError((e, c) => {
c.get('sentry').captureException(e)
if (e instanceof HTTPException)
if (e instanceof HTTPException) {
if (e.status === 429) {
return c.json({ error: 'you are beeing rate limited' }, 429)
}
return c.json({ status: 'Internal Server Error', response: e.getResponse(), error: JSON.stringify(e), message: e.message }, 500)
}
console.log('app', 'onError', e)
return c.json({ status: 'Internal Server Error', error: JSON.stringify(e), message: e.message }, 500)
})
Expand Down
29 changes: 29 additions & 0 deletions cloudflare_workers/plugin/wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,32 @@ hyperdrive = [ { binding = "HYPERDRIVE_DB", id = "0f1d77550db142fbb17cb6b3ee659a

[env.local]
name = "capgo_plugin-local"
[[unsafe.bindings]]
name = "PUBLIC_API_DEVICE_CHANNEL_SELF_POST_RATE_LIMITER"
type = "ratelimit"
namespace_id = "1001"
simple = { limit = 20, period = 10 }

[[unsafe.bindings]]
name = "PUBLIC_API_DEVICE_CHANNEL_SELF_DELETE_RATE_LIMITER"
type = "ratelimit"
namespace_id = "1001"
simple = { limit = 20, period = 10 }

[[unsafe.bindings]]
name = "PUBLIC_API_DEVICE_CHANNEL_SELF_PUT_RATE_LIMITER"
type = "ratelimit"
namespace_id = "1001"
simple = { limit = 20, period = 10 }

[[unsafe.bindings]]
name = "PUBLIC_API_DEVICE_CHANNEL_SELF_GET_RATE_LIMITER"
type = "ratelimit"
namespace_id = "1001"
simple = { limit = 20, period = 10 }

[[unsafe.bindings]]
name = "PUBLIC_API_DEVICE_ALL_UPDATES_POST_RATE_LIMITER"
type = "ratelimit"
namespace_id = "1002"
simple = { limit = 20, period = 10 }
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
"@capgo/inappbrowser": "^6.9.22",
"@capgo/native-audio": "^6.4.23",
"@capgo/native-market": "^6.0.2",
"@elithrar/workers-hono-rate-limit": "^0.4.3",
"@formkit/auto-animate": "1.0.0-pre-alpha.3",
"@formkit/i18n": "^1.6.9",
"@formkit/themes": "1.6.9",
Expand Down
25 changes: 25 additions & 0 deletions supabase/functions/_backend/utils/supabase.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1268,6 +1268,12 @@ export type Database = {
}
Returns: boolean
}
check_revert_to_builtin_version: {
Args: {
appid: string
}
Returns: number
}
convert_bytes_to_gb: {
Args: {
byt: number
Expand Down Expand Up @@ -1774,6 +1780,15 @@ export type Database = {
}
Returns: boolean
}
has_app_right_apikey: {
Args: {
appid: string
right: Database["public"]["Enums"]["user_min_right"]
userid: string
apikey: string
}
Returns: boolean
}
has_app_right_userid: {
Args: {
appid: string
Expand Down Expand Up @@ -2011,6 +2026,15 @@ export type Database = {
uninstall: number
}[]
}
replicate_to_d1: {
Args: {
record: Json
old_record: Json
operation: string
table_name: string
}
Returns: undefined
}
reset_and_seed_app_data: {
Args: {
p_app_id: string
Expand Down Expand Up @@ -2091,6 +2115,7 @@ export type Database = {
| "NoChannelOrOverride"
| "setChannel"
| "getChannel"
| "rateLimited"
stripe_status:
| "created"
| "succeeded"
Expand Down
1 change: 1 addition & 0 deletions supabase/migrations/20241219052050_ratelimit.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TYPE "public"."stats_action" ADD VALUE 'rateLimited';