From 97354d007387684a5f83c4c71aafd2646b38d25c Mon Sep 17 00:00:00 2001 From: ivy Date: Mon, 21 Aug 2023 12:15:19 -0700 Subject: [PATCH] feat(ext): firewall extension (#184) * feat: add firewall extention closes #152 * test: add tetsing for firewall extention * refactor: removed customLists, use customRanges instead chore: `deno task check` * test: removed debug option and added proxy to cloudflare in firewall tests * feat: check for updates on request * chore: removed unused file fix: double checking --------- Co-authored-by: Samuel Kopp <62482066+boywithkeyboard@users.noreply.github.com> --- ext/firewall.ts | 53 ++++++++++++++++++++++++++++++++++ test/ext/firewall.test.ts | 61 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 ext/firewall.ts create mode 100644 test/ext/firewall.test.ts diff --git a/ext/firewall.ts b/ext/firewall.ts new file mode 100644 index 0000000..644ec43 --- /dev/null +++ b/ext/firewall.ts @@ -0,0 +1,53 @@ +import { createExtension } from '../mod.ts' + +let VPN_LIST: string[] = [] +let DATACENTER_LIST: string[] = [] +let LAST_UPDATE = 0 + +type FirewallOptions = { + blockVPN?: boolean + blockDatacenter?: boolean + customRanges?: string[] +} + +export const firewall = createExtension({ + async onRequest({ _: opts, app }) { + await checkUpdate() + if ( + opts?.blockVPN && VPN_LIST.find((range) => isIpInRange(app.ip, range)) + ) { + return new Response(null, { status: 403 }) + } + if ( + opts?.blockDatacenter && + DATACENTER_LIST.find((range) => isIpInRange(app.ip, range)) + ) return new Response(null, { status: 403 }) + if ( + opts?.customRanges && + opts.customRanges.find((range) => isIpInRange(app.ip, range)) + ) return new Response(null, { status: 403 }) + }, +}) + +async function checkUpdate() { + if (Date.now() - LAST_UPDATE < 9e5 /* 15 minutes */) return + VPN_LIST = (await (await fetch( + 'https://raw.githubusercontent.com/X4BNet/lists_vpn/main/output/vpn/ipv4.txt', + )).text()).split('\n') + DATACENTER_LIST = (await (await fetch( + 'https://raw.githubusercontent.com/X4BNet/lists_vpn/main/output/datacenter/ipv4.txt', + )).text()).split('\n') + LAST_UPDATE = Date.now() +} + +function isIpInRange(ip: string, range: string) { + const [rangeIp, rangeMask] = range.split('/') + const rangeStart = ipToNumber(rangeIp) >>> 0 + const rangeEnd = rangeStart + ((1 << (32 - parseInt(rangeMask))) - 1) + const numIp = ipToNumber(ip) + return numIp >= rangeStart && numIp <= rangeEnd +} + +function ipToNumber(ip: string) { + return ip.split('.').reduce((acc, val) => (acc << 8) | parseInt(val), 0) >>> 0 +} diff --git a/test/ext/firewall.test.ts b/test/ext/firewall.test.ts new file mode 100644 index 0000000..cc204e0 --- /dev/null +++ b/test/ext/firewall.test.ts @@ -0,0 +1,61 @@ +import { assertEquals } from '../deps.ts' +import cheetah from '../../mod.ts' +import { firewall } from '../../ext/firewall.ts' + +Deno.test('ext/firewall', async (t) => { + await t.step('vpn', async () => { + const app = new cheetah({ proxy: 'cloudflare' }) + + app.use(firewall({ + blockVPN: true, + })) + + const res = await app.fetch( + new Request('http://localhost', { + headers: { + 'cf-connecting-ip': '2.56.16.0', + }, + }), + ) + + assertEquals(res.status, 403) + }) +}) + +Deno.test('datacenters', async () => { + const app = new cheetah({ proxy: 'cloudflare' }) + + app.use(firewall({ + blockDatacenter: true, + })) + + const res = await app.fetch( + new Request('http://localhost', { + headers: { + 'cf-connecting-ip': '1.12.32.0', + }, + }), + ) + + assertEquals(res.status, 403) +}) + +Deno.test('customRanges', async () => { + const app = new cheetah({ proxy: 'cloudflare' }) + + app.use(firewall({ + customRanges: [ + '1.2.3.4/32', + ], + })) + + const res = await app.fetch( + new Request('http://localhost', { + headers: { + 'cf-connecting-ip': '1.2.3.4', + }, + }), + ) + + assertEquals(res.status, 403) +})