Skip to content

Commit 0341a78

Browse files
committed
Address VPN gate review feedback
1 parent 2c94b30 commit 0341a78

5 files changed

Lines changed: 77 additions & 10 deletions

File tree

web/src/app/api/v1/chat/completions/__tests__/completions.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ describe('/api/v1/chat/completions POST endpoint', () => {
6969
const allowedFreeModeHeaders = (apiKey: string) => ({
7070
Authorization: `Bearer ${apiKey}`,
7171
'cf-ipcountry': 'US',
72+
'cf-connecting-ip': '203.0.113.10',
7273
})
7374

7475
beforeEach(() => {

web/src/app/api/v1/freebuff/session/__tests__/session.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ function makeReq(
2727
if (apiKey) headers.set('Authorization', `Bearer ${apiKey}`)
2828
if (opts.instanceId) headers.set(FREEBUFF_INSTANCE_HEADER, opts.instanceId)
2929
const cfCountry = opts.cfCountry === null ? null : (opts.cfCountry ?? 'US')
30-
if (cfCountry) headers.set('cf-ipcountry', cfCountry)
30+
if (cfCountry) {
31+
headers.set('cf-ipcountry', cfCountry)
32+
headers.set('cf-connecting-ip', '203.0.113.10')
33+
}
3134
if (opts.model) headers.set(FREEBUFF_MODEL_HEADER, opts.model)
3235
return {
3336
headers,

web/src/app/api/v1/freebuff/session/_handlers.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import type { NextRequest } from 'next/server'
2525
* the 10s error retry cadence. The new CLI parses the 403 body directly. */
2626
async function countryBlockedResponse(
2727
req: NextRequest,
28-
deps: FreebuffSessionDeps,
2928
): Promise<NextResponse | null> {
3029
const countryAccess = await getFreeModeCountryAccess(req, {
3130
ipinfoToken: env.IPINFO_TOKEN,
@@ -132,7 +131,7 @@ export async function postFreebuffSession(
132131
const auth = await resolveUser(req, deps)
133132
if ('error' in auth) return auth.error
134133

135-
const blocked = await countryBlockedResponse(req, deps)
134+
const blocked = await countryBlockedResponse(req)
136135
if (blocked) return blocked
137136

138137
const requestedModel = req.headers.get(FREEBUFF_MODEL_HEADER) ?? ''
@@ -176,7 +175,7 @@ export async function getFreebuffSession(
176175
const auth = await resolveUser(req, deps)
177176
if ('error' in auth) return auth.error
178177

179-
const blocked = await countryBlockedResponse(req, deps)
178+
const blocked = await countryBlockedResponse(req)
180179
if (blocked) return blocked
181180

182181
try {

web/src/server/__tests__/free-mode-country.test.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ const noAnonymousNetwork = {
2020
describe('free mode country access', () => {
2121
test('allows allowlisted Cloudflare countries', async () => {
2222
const access = await getFreeModeCountryAccess(
23-
makeReq({ 'cf-ipcountry': 'us' }),
23+
makeReq({
24+
'cf-ipcountry': 'us',
25+
'cf-connecting-ip': '203.0.113.10',
26+
}),
2427
noAnonymousNetwork,
2528
)
2629
expect(access.allowed).toBe(true)
@@ -58,6 +61,30 @@ describe('free mode country access', () => {
5861
expect(access.blockReason).toBe('missing_client_ip')
5962
})
6063

64+
test('blocks allowlisted Cloudflare countries when client IP is missing', async () => {
65+
const access = await getFreeModeCountryAccess(
66+
makeReq({ 'cf-ipcountry': 'US' }),
67+
noAnonymousNetwork,
68+
)
69+
expect(access.allowed).toBe(false)
70+
expect(access.countryCode).toBe(null)
71+
expect(access.blockReason).toBe('missing_client_ip')
72+
expect(access.cfCountry).toBe('US')
73+
})
74+
75+
test('uses CF-Connecting-IP as a client IP fallback', async () => {
76+
const access = await getFreeModeCountryAccess(
77+
makeReq({
78+
'cf-ipcountry': 'US',
79+
'cf-connecting-ip': '203.0.113.10',
80+
}),
81+
noAnonymousNetwork,
82+
)
83+
expect(access.allowed).toBe(true)
84+
expect(access.countryCode).toBe('US')
85+
expect(access.hasClientIp).toBe(true)
86+
})
87+
6188
test('blocks allowlisted countries when the client IP is an anonymous network', async () => {
6289
const access = await getFreeModeCountryAccess(
6390
makeReq({

web/src/server/free-mode-country.ts

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ type ResolvedCountryAccess = Omit<
7070
}
7171

7272
const IPINFO_PRIVACY_CACHE_TTL_MS = 30 * 60 * 1000
73+
const IPINFO_PRIVACY_CACHE_MAX_ENTRIES = 5000
7374
const ipinfoPrivacyCache = new Map<
7475
string,
7576
{ expiresAt: number; privacy: FreeModeIpPrivacy | null }
@@ -80,7 +81,34 @@ export function extractClientIp(req: NextRequest): string | undefined {
8081
if (forwardedFor) {
8182
return forwardedFor.split(',')[0].trim()
8283
}
83-
return req.headers.get('x-real-ip') ?? undefined
84+
return (
85+
req.headers.get('cf-connecting-ip') ??
86+
req.headers.get('x-real-ip') ??
87+
undefined
88+
)
89+
}
90+
91+
function setIpinfoPrivacyCache(
92+
ip: string,
93+
privacy: FreeModeIpPrivacy | null,
94+
): void {
95+
const now = Date.now()
96+
for (const [cachedIp, cached] of ipinfoPrivacyCache) {
97+
if (cached.expiresAt <= now) {
98+
ipinfoPrivacyCache.delete(cachedIp)
99+
}
100+
}
101+
102+
while (ipinfoPrivacyCache.size >= IPINFO_PRIVACY_CACHE_MAX_ENTRIES) {
103+
const oldestIp = ipinfoPrivacyCache.keys().next().value
104+
if (!oldestIp) break
105+
ipinfoPrivacyCache.delete(oldestIp)
106+
}
107+
108+
ipinfoPrivacyCache.set(ip, {
109+
expiresAt: now + IPINFO_PRIVACY_CACHE_TTL_MS,
110+
privacy,
111+
})
84112
}
85113

86114
function privacySignalsFromIpinfo(
@@ -123,10 +151,7 @@ export async function lookupIpinfoPrivacy(params: {
123151
const privacy = {
124152
signals,
125153
}
126-
ipinfoPrivacyCache.set(params.ip, {
127-
expiresAt: Date.now() + IPINFO_PRIVACY_CACHE_TTL_MS,
128-
privacy,
129-
})
154+
setIpinfoPrivacyCache(params.ip, privacy)
130155
return privacy
131156
}
132157

@@ -218,6 +243,18 @@ export async function getFreeModeCountryAccess(
218243
}
219244
}
220245

246+
if (!clientIp) {
247+
return {
248+
allowed: false,
249+
countryCode: null,
250+
blockReason: 'missing_client_ip',
251+
cfCountry,
252+
geoipCountry: null,
253+
ipPrivacy: null,
254+
hasClientIp: false,
255+
}
256+
}
257+
221258
const ipPrivacy = await getIpPrivacy(clientIp, options)
222259
if (ipPrivacy?.signals.length) {
223260
return {

0 commit comments

Comments
 (0)