Skip to content

Commit 724d908

Browse files
committed
Use IPinfo Max anonymous signals
1 parent bf4f449 commit 724d908

2 files changed

Lines changed: 69 additions & 15 deletions

File tree

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

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,24 @@ describe('free mode country access', () => {
106106
expect(access.ipPrivacy?.signals).toEqual(['vpn'])
107107
})
108108

109+
test('blocks allowlisted countries when IPinfo reports a residential proxy', async () => {
110+
const access = await getFreeModeCountryAccess(
111+
makeReq({
112+
'cf-ipcountry': 'US',
113+
'x-forwarded-for': '203.0.113.10',
114+
}),
115+
{
116+
ipinfoToken: 'test-token',
117+
lookupIpPrivacy: async () => ({
118+
signals: ['res_proxy'],
119+
}),
120+
},
121+
)
122+
expect(access.allowed).toBe(false)
123+
expect(access.blockReason).toBe('anonymous_network')
124+
expect(access.ipPrivacy?.signals).toEqual(['res_proxy'])
125+
})
126+
109127
test('allows allowlisted countries when privacy lookup finds no anonymous signals', async () => {
110128
const access = await getFreeModeCountryAccess(
111129
makeReq({
@@ -141,25 +159,49 @@ describe('free mode country access', () => {
141159
expect(access.ipPrivacy).toBe(null)
142160
})
143161

144-
test('parses IPinfo privacy signals', async () => {
162+
test('parses IPinfo Max anonymous signals', async () => {
163+
let requestedUrl = ''
164+
const fetch = async (url: string | URL | Request) => {
165+
requestedUrl = String(url)
166+
return Response.json({
167+
anonymous: {
168+
is_proxy: false,
169+
is_relay: true,
170+
is_tor: true,
171+
is_vpn: false,
172+
is_res_proxy: true,
173+
},
174+
is_anonymous: true,
175+
is_hosting: true,
176+
})
177+
}
178+
179+
const privacy = await lookupIpinfoPrivacy({
180+
ip: IPINFO_PRIVACY_TEST_IP,
181+
token: 'test-token',
182+
fetch: fetch as unknown as typeof globalThis.fetch,
183+
})
184+
185+
expect(requestedUrl).toContain('https://api.ipinfo.io/lookup/')
186+
expect(privacy).toEqual({
187+
signals: ['tor', 'relay', 'res_proxy', 'hosting'],
188+
})
189+
})
190+
191+
test('blocks generic IPinfo anonymous results without a specific signal', async () => {
145192
const fetch = async () =>
146193
Response.json({
147-
vpn: true,
148-
proxy: false,
149-
tor: true,
150-
relay: false,
151-
hosting: true,
152-
service: 'Example VPN',
194+
is_anonymous: true,
153195
})
154196

155197
const privacy = await lookupIpinfoPrivacy({
156-
ip: IPINFO_PRIVACY_TEST_IP,
198+
ip: '198.51.100.43',
157199
token: 'test-token',
158200
fetch: fetch as unknown as typeof globalThis.fetch,
159201
})
160202

161203
expect(privacy).toEqual({
162-
signals: ['vpn', 'tor', 'hosting', 'service'],
204+
signals: ['anonymous'],
163205
})
164206
})
165207
})

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

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,12 @@ export type FreeModeCountryBlockReason =
3131
| 'unresolved_client_ip'
3232

3333
export type FreeModeIpPrivacySignal =
34+
| 'anonymous'
3435
| 'vpn'
3536
| 'proxy'
3637
| 'tor'
3738
| 'relay'
39+
| 'res_proxy'
3840
| 'hosting'
3941
| 'service'
4042

@@ -114,18 +116,28 @@ function setIpinfoPrivacyCache(
114116
function privacySignalsFromIpinfo(
115117
data: Record<string, unknown>,
116118
): FreeModeIpPrivacySignal[] {
119+
const anonymous =
120+
data.anonymous && typeof data.anonymous === 'object'
121+
? (data.anonymous as Record<string, unknown>)
122+
: {}
117123
const signals: FreeModeIpPrivacySignal[] = []
118-
if (data.vpn === true) signals.push('vpn')
119-
if (data.proxy === true) signals.push('proxy')
120-
if (data.tor === true) signals.push('tor')
121-
if (data.relay === true) signals.push('relay')
122-
if (data.hosting === true) signals.push('hosting')
124+
if (data.vpn === true || anonymous.is_vpn === true) signals.push('vpn')
125+
if (data.proxy === true || anonymous.is_proxy === true) signals.push('proxy')
126+
if (data.tor === true || anonymous.is_tor === true) signals.push('tor')
127+
if (data.relay === true || anonymous.is_relay === true) signals.push('relay')
128+
if (anonymous.is_res_proxy === true) signals.push('res_proxy')
129+
if (data.hosting === true || data.is_hosting === true) {
130+
signals.push('hosting')
131+
}
123132
if (
124133
data.service === true ||
125134
(typeof data.service === 'string' && data.service.length > 0)
126135
) {
127136
signals.push('service')
128137
}
138+
if (signals.length === 0 && data.is_anonymous === true) {
139+
signals.push('anonymous')
140+
}
129141
return signals
130142
}
131143

@@ -140,7 +152,7 @@ export async function lookupIpinfoPrivacy(params: {
140152
}
141153

142154
const response = await params.fetch(
143-
`https://ipinfo.io/${encodeURIComponent(params.ip)}/privacy?token=${encodeURIComponent(params.token)}`,
155+
`https://api.ipinfo.io/lookup/${encodeURIComponent(params.ip)}?token=${encodeURIComponent(params.token)}`,
144156
)
145157
if (!response.ok) {
146158
return null

0 commit comments

Comments
 (0)