diff --git a/ranges/1clickvpn.txt b/ranges/1clickvpn.txt new file mode 100644 index 0000000..6fee0ba --- /dev/null +++ b/ranges/1clickvpn.txt @@ -0,0 +1,10 @@ +95.141.32.101/32 +93.115.28.181/32 +185.136.159.181/32 +79.141.162.81/32 +5.149.253.57/32 +5.149.255.178/32 +5.149.250.222/32 +77.83.198.123/32 +185.235.137.143/32 +83.143.87.194/32 \ No newline at end of file diff --git a/ranges/cloudflare.txt b/ranges/cloudflare.txt new file mode 100644 index 0000000..fd160bd --- /dev/null +++ b/ranges/cloudflare.txt @@ -0,0 +1,22 @@ +173.245.48.0/20 +103.21.244.0/22 +103.22.200.0/22 +103.31.4.0/22 +141.101.64.0/18 +108.162.192.0/18 +190.93.240.0/20 +188.114.96.0/20 +197.234.240.0/22 +198.41.128.0/17 +162.158.0.0/15 +104.16.0.0/13 +104.24.0.0/14 +172.64.0.0/13 +131.0.72.0/22 +2400:cb00::/32 +2606:4700::/32 +2803:f800::/32 +2405:b500::/32 +2405:8100::/32 +2a06:98c0::/29 +2c0f:f248::/32 \ No newline at end of file diff --git a/ranges/expressvpn.txt b/ranges/expressvpn.txt new file mode 100644 index 0000000..1fffe2f --- /dev/null +++ b/ranges/expressvpn.txt @@ -0,0 +1,488 @@ +14.102.63.0/24 +158.173.129.0/24 +158.173.130.0/24 +158.173.50.0/24 +146.19.7.0/25 +146.19.7.128/25 +62.169.132.0/24 +14.102.85.0/24 +2a0f:e385::/32 +14.102.52.0/24 +155.2.180.0/24 +155.2.181.0/24 +158.173.20.0/24 +158.173.21.0/24 +158.173.3.0/24 +188.213.203.0/24 +203.188.164.0/24 +212.56.48.0/24 +45.128.199.0/24 +92.51.255.0/24 +2a0e:4201::/32 +158.173.42.0/24 +158.173.43.0/24 +158.173.22.0/24 +193.56.116.0/24 +203.188.165.0/24 +212.32.49.0/24 +45.146.55.0/24 +185.184.134.0/24 +2a10:5a81::/32 +2a11:f885::/32 +2a11:f886::/32 +2a11:f887::/32 +14.102.53.0/24 +185.64.78.0/25 +185.64.78.128/25 +103.213.214.0/24 +103.61.198.0/24 +157.97.122.0/24 +185.114.34.0/24 +198.55.29.0/24 +2a0e:4203::/32 +155.2.217.0/24 +170.62.235.0/24 +170.62.244.0/24 +170.62.245.0/24 +170.62.237.0/24 +66.56.86.0/24 +158.173.36.0/24 +62.169.134.0/24 +157.97.120.0/24 +158.173.49.0/24 +170.62.246.0/24 +198.55.30.0/24 +2a0b:64c3::/32 +14.102.61.0/24 +193.218.32.0/24 +2.57.170.0/24 +37.46.149.0/24 +45.134.22.0/24 +89.251.2.0/24 +92.51.253.0/24 +103.125.78.0/24 +103.125.79.0/24 +103.210.198.0/24 +103.210.199.0/24 +185.239.243.0/24 +185.34.108.0/24 +185.34.109.0/24 +185.34.110.0/24 +185.34.111.0/24 +212.119.34.0/24 +45.135.184.0/24 +84.51.234.0/24 +84.51.235.0/24 +45.129.133.0/24 +89.46.92.0/25 +89.46.92.128/25 +103.210.197.0/24 +167.160.12.0/24 +178.239.204.0/24 +185.228.225.0/24 +45.86.203.0/24 +84.51.233.0/24 +14.102.54.0/24 +103.213.215.0/24 +155.2.212.0/24 +155.2.194.0/24 +155.2.195.0/24 +170.62.224.0/24 +185.192.16.0/24 +185.227.33.0/24 +92.51.252.0/24 +2a0b:64c5::/32 +158.173.6.0/24 +158.173.7.0/24 +136.144.43.0/24 +193.56.117.0/24 +203.188.175.0/24 +212.32.68.0/24 +212.32.69.0/24 +45.132.115.0/24 +2a0e:d787::/32 +170.62.239.0/24 +158.173.140.0/24 +158.173.141.0/24 +136.144.26.0/24 +155.2.176.0/24 +155.2.177.0/24 +158.173.154.0/24 +158.173.155.0/24 +158.173.156.0/24 +170.62.94.0/24 +203.188.181.0/24 +213.254.174.0/24 +45.130.143.0/24 +45.157.139.0/25 +45.157.139.128/25 +45.86.202.0/24 +45.91.148.0/25 +45.91.148.128/25 +91.217.249.0/24 +92.51.250.0/24 +124.198.144.0/24 +66.56.84.0/24 +212.32.75.0/24 +155.2.221.0/24 +155.2.222.0/24 +170.62.234.0/24 +193.176.211.0/24 +194.5.83.0/24 +92.51.248.0/24 +92.51.251.0/24 +2a0e:d781::/32 +103.255.77.0/24 +155.2.215.0/24 +167.160.16.0/24 +185.121.122.0/24 +185.244.137.0/24 +81.95.60.0/24 +81.95.61.0/24 +92.51.232.0/24 +92.51.233.0/24 +14.102.62.0/24 +2a0f:f43::/32 +14.102.84.0/24 +45.8.25.0/24 +45.93.164.0/24 +167.160.29.0/24 +212.78.248.0/24 +62.169.131.0/24 +81.95.58.0/24 +81.95.59.0/24 +14.102.87.0/24 +45.130.81.0/24 +107.150.166.0/24 +136.144.33.0/24 +158.173.54.0/24 +170.62.160.0/24 +192.140.220.0/24 +192.140.221.0/24 +193.36.225.0/24 +194.5.52.0/24 +212.56.53.0/24 +213.254.161.0/24 +213.254.175.0/24 +45.150.181.0/24 +45.150.182.0/24 +45.150.183.0/24 +45.159.246.0/24 +62.100.210.0/24 +92.51.234.0/24 +92.51.235.0/24 +2a0e:d785::/32 +2a11:53c2::/32 +2a11:53c3::/32 +2a11:53c4::/32 +2a11:53c5::/32 +158.173.4.0/24 +158.173.5.0/24 +185.245.5.0/24 +203.188.167.0/24 +212.78.246.0/24 +103.210.196.0/24 +158.173.23.0/24 +158.173.24.0/24 +170.62.229.0/24 +178.239.198.0/24 +185.161.111.0/24 +185.192.69.0/24 +185.192.70.0/24 +185.198.243.0/24 +185.217.117.0/24 +188.240.74.0/24 +193.218.35.0/24 +194.32.120.0/24 +203.188.182.0/24 +213.109.151.0/25 +213.109.151.128/25 +45.144.226.0/24 +45.85.127.0/24 +84.51.232.0/24 +85.203.34.0/24 +85.203.8.0/24 +2a07:bdc1::/32 +2a0e:d784::/32 +2a0e:d786::/32 +2a11:53c0::/32 +2a11:53c1::/32 +2a11:f881::/32 +2a11:f882::/32 +158.173.134.0/24 +158.173.135.0/24 +212.32.50.0/24 +45.130.141.0/24 +188.240.73.0/24 +212.56.49.0/24 +45.91.23.0/24 +212.32.72.0/24 +2a0f:f42::/32 +45.154.138.0/24 +170.62.238.0/24 +45.91.20.0/24 +2a07:e342::/32 +194.61.40.0/24 +194.61.41.0/24 +62.169.128.0/24 +217.119.143.0/25 +217.119.143.128/25 +130.255.168.0/25 +130.255.168.128/25 +155.2.182.0/24 +155.2.183.0/24 +170.62.225.0/24 +185.198.155.0/25 +185.198.155.128/25 +212.78.249.0/24 +89.38.70.0/25 +89.38.70.128/25 +2a0f:f40::/32 +2a11:53c6::/32 +2a11:53c7::/32 +2a11:f883::/32 +2a11:f884::/32 +103.125.76.0/24 +136.144.19.0/24 +158.173.59.0/24 +170.62.232.0/24 +185.121.120.0/24 +185.94.65.0/24 +185.94.66.0/24 +192.140.222.0/24 +192.140.223.0/24 +193.36.224.0/24 +45.132.113.0/24 +81.95.48.0/24 +81.95.49.0/24 +92.51.236.0/24 +92.51.237.0/24 +136.144.35.0/24 +185.198.240.0/25 +185.198.240.128/25 +192.253.209.0/24 +2.57.168.0/24 +212.56.54.0/24 +213.254.163.0/24 +14.102.86.0/24 +203.159.81.0/24 +103.255.76.0/24 +109.205.190.0/25 +109.205.190.128/25 +155.2.186.0/24 +155.2.187.0/24 +155.2.188.0/24 +158.173.25.0/24 +158.173.55.0/24 +170.62.102.0/24 +170.62.103.0/24 +170.62.106.0/24 +170.62.107.0/24 +170.62.108.0/24 +185.51.54.0/24 +192.253.208.0/24 +194.62.16.0/25 +194.62.16.128/25 +45.130.83.0/24 +45.157.99.0/24 +45.8.19.0/24 +45.85.124.0/24 +45.92.229.0/24 +81.95.62.0/24 +81.95.63.0/24 +89.43.199.0/25 +89.43.199.128/25 +92.51.238.0/24 +92.51.239.0/24 +93.115.254.0/24 +2a0f:e383::/32 +185.155.103.0/25 +185.155.103.128/25 +45.129.132.0/24 +203.25.124.0/25 +203.25.124.128/25 +212.32.76.0/24 +170.62.247.0/24 +212.78.244.0/24 +45.150.93.0/24 +45.92.228.0/24 +2a0b:64c2::/32 +170.62.228.0/24 +158.173.157.0/24 +158.173.158.0/24 +170.62.233.0/24 +170.62.92.0/24 +170.62.93.0/24 +178.211.136.0/25 +178.211.136.128/25 +194.5.53.0/24 +195.64.107.0/25 +195.64.107.128/25 +195.64.108.0/25 +195.64.108.128/25 +195.64.113.0/25 +195.64.113.128/25 +203.188.183.0/24 +45.140.135.0/24 +45.146.53.0/24 +45.157.112.0/24 +45.91.22.0/24 +81.95.50.0/24 +81.95.51.0/24 +92.51.249.0/24 +2a0f:e387::/32 +103.61.196.0/24 +146.19.29.0/25 +146.19.29.128/25 +167.160.28.0/24 +170.62.88.0/24 +185.245.7.0/24 +198.55.31.0/24 +212.32.73.0/24 +220.158.199.0/24 +45.133.5.0/24 +45.150.180.0/24 +45.67.96.0/24 +66.56.87.0/24 +2a0f:e384::/32 +158.173.18.0/24 +158.173.19.0/24 +170.62.236.0/24 +66.56.85.0/24 +14.102.60.0/24 +158.173.52.0/24 +158.173.53.0/24 +2a0e:4207::/32 +2a0e:4206::/32 +103.213.212.0/24 +155.2.184.0/24 +155.2.185.0/24 +158.173.45.0/24 +167.160.17.0/24 +170.62.231.0/24 +185.121.123.0/24 +185.239.241.0/24 +45.146.54.0/24 +93.185.160.0/24 +194.62.107.0/25 +194.62.107.128/25 +220.158.198.0/24 +89.36.23.0/24 +93.114.192.0/24 +213.254.160.0/24 +45.131.195.0/24 +14.102.55.0/24 +203.188.168.0/24 +2a0b:64c4::/32 +170.62.95.0/24 +185.150.0.0/24 +193.37.32.0/24 +194.5.82.0/24 +203.188.189.0/24 +203.188.190.0/24 +203.188.191.0/24 +212.78.247.0/24 +213.109.152.0/25 +213.109.152.128/25 +31.222.218.0/24 +31.222.219.0/24 +45.128.198.0/24 +85.203.23.0/24 +89.47.15.0/25 +89.47.15.128/25 +2a0e:4205::/32 +158.173.44.0/24 +170.62.226.0/24 +170.62.89.0/24 +212.78.250.0/24 +45.135.187.0/24 +45.157.98.0/24 +45.91.21.0/24 +2a0e:4204::/32 +185.92.24.0/24 +62.169.130.0/24 +2a0b:64c7::/32 +158.173.142.0/24 +158.173.143.0/24 +158.173.33.0/24 +158.173.34.0/24 +103.125.77.0/24 +103.213.213.0/24 +158.173.153.0/24 +158.173.32.0/24 +158.173.47.0/24 +185.121.121.0/24 +193.19.109.0/24 +193.37.33.0/24 +193.39.215.0/25 +193.39.215.128/25 +85.8.130.0/25 +85.8.130.128/25 +203.188.169.0/24 +2a0b:64c6::/32 +103.61.199.0/24 +136.144.17.0/24 +170.62.110.0/24 +170.62.111.0/24 +185.205.190.0/24 +185.92.26.0/24 +198.55.28.0/24 +2.57.169.0/24 +212.32.48.0/24 +31.222.216.0/24 +31.222.217.0/24 +45.130.80.0/24 +62.169.129.0/24 +66.56.80.0/24 +66.56.81.0/24 +66.56.82.0/24 +89.251.0.0/24 +93.115.255.0/24 +2a0f:e386::/32 +45.86.200.0/24 +85.203.13.0/24 +155.2.178.0/24 +155.2.179.0/24 +155.2.216.0/24 +170.62.91.0/24 +188.212.132.0/24 +194.5.48.0/24 +203.188.170.0/24 +89.36.22.0/24 +89.37.62.0/24 +92.51.254.0/24 +93.185.161.0/24 +2a0e:4202::/32 +158.173.16.0/24 +158.173.17.0/24 +192.253.210.0/24 +188.213.202.0/24 +194.5.49.0/24 +212.32.71.0/24 +103.61.197.0/24 +158.173.48.0/24 +185.192.68.0/24 +203.188.172.0/24 +45.144.129.0/24 +45.148.25.0/24 +45.95.243.0/24 +2a0e:d783::/32 +212.56.52.0/24 +62.169.135.0/24 +136.144.42.0/24 +136.144.27.0/24 +170.62.230.0/24 +188.212.135.0/25 +188.212.135.128/25 +212.78.245.0/24 +2a0e:d782::/32 +170.62.227.0/24 +158.173.152.0/24 +170.62.90.0/24 +188.240.68.0/25 +188.240.68.128/25 +212.56.50.0/24 +212.78.251.0/24 +81.95.56.0/24 +81.95.57.0/24 +2a0f:f41::/32 diff --git a/src/router/html.ts b/src/router/html.ts index 2eb671f..552d06f 100644 --- a/src/router/html.ts +++ b/src/router/html.ts @@ -1,6 +1,7 @@ import { Elysia } from 'elysia'; import { m, eta, error } from '@/utils/html' import healthStatus from '@/utils/health'; +import { checkIpRanges } from '@/utils/ranges'; const app = new Elysia() app.get('/', async ({ set }) => { @@ -12,9 +13,17 @@ app.get('/', async ({ set }) => { })) }) -app.get('/save', async ({ query: { url }, set, error }) => { +app.get('/save', async ({ query: { url }, set, headers, error }) => { if (!url) return error(400, 'No url provided.') + const ranges = await checkIpRanges(headers['cf-connecting-ip'] || headers['x-forwarded-for'] || '') + if (ranges.blocked) { + set.headers['Content-Type'] = 'text/html; charset=utf-8' + return await m(eta.render('./blocked', { + title: 'Blocked | PreserveTube' + })) + } + let websocket = process.env.WEBSOCKET if (healthStatus[process.env.METADATA!] != 'healthy') { websocket = process.env.ALTERNATIVE_WEBSOCKET! @@ -29,9 +38,17 @@ app.get('/save', async ({ query: { url }, set, error }) => { })) }) -app.get('/savechannel', async ({ query: { url }, set, error }) => { +app.get('/savechannel', async ({ query: { url }, set, headers, error }) => { if (!url) return error(400, 'No url provided.') + const ranges = await checkIpRanges(headers['cf-connecting-ip'] || headers['x-forwarded-for'] || '') + if (ranges.blocked) { + set.headers['Content-Type'] = 'text/html; charset=utf-8' + return await m(eta.render('./blocked', { + title: 'Blocked | PreserveTube' + })) + } + let websocket = process.env.WEBSOCKET if (healthStatus[process.env.METADATA!] != 'healthy') { websocket = process.env.ALTERNATIVE_WEBSOCKET! diff --git a/src/router/websocket.ts b/src/router/websocket.ts index 11d751e..50dd1e8 100644 --- a/src/router/websocket.ts +++ b/src/router/websocket.ts @@ -10,6 +10,7 @@ import { getChannelVideos, getVideo } from '@/utils/metadata'; import { error } from '@/utils/html' import redis from '@/utils/redis'; import { parseSlop } from '@/utils/slop'; +import { checkIpRanges } from '@/utils/ranges'; const app = new Elysia() const videoIds: Record = {} @@ -92,6 +93,12 @@ app.ws('/save', { }), body: t.String(), open: async (ws) => { + const range = await checkIpRanges(ws.data.headers['x-forwarded-for'] || '') + if (range.list != 'cloudflare') return sendError(ws, 'There\'s something wrong with your connection.') + + const blacklistCheck = await checkIpRanges(ws.data.headers['cf-connecting-ip']!) + if (blacklistCheck.blocked) return sendError(ws, `Your network is flagged as malicious.`) + console.log(`${ws.id} - ${ws.data.path} - ${JSON.stringify(ws.data.query)}`) const videoId = validateVideo(ws.data.query.url) @@ -190,6 +197,12 @@ app.ws('/savechannel', { }), body: t.String(), open: async (ws) => { + const range = await checkIpRanges(ws.data.headers['x-forwarded-for'] || '') + if (range.list != 'cloudflare') return sendError(ws, 'There\'s something wrong with your connection.') + + const blacklistCheck = await checkIpRanges(ws.data.headers['cf-connecting-ip']!) + if (blacklistCheck.blocked) return sendError(ws, `Your network is flagged as malicious.`) + console.log(`${ws.id} - ${ws.data.path} - ${JSON.stringify(ws.data.query)}`) const channelId = await validateChannel(ws.data.query.url); diff --git a/src/templates/blocked.eta b/src/templates/blocked.eta new file mode 100644 index 0000000..c533560 --- /dev/null +++ b/src/templates/blocked.eta @@ -0,0 +1,37 @@ +<% layout('./layout') %> + +
+

+ We detected malicious activity from the network you are currently visiting from, so we have + blocked certain functionality for this connection. +

+

+ This is most commonly caused by free VPNs, shared VPN exit nodes, or other heavily abused + proxy networks. +

+

+ If you believe this is a mistake, please contact + admin@preservetube.com. +

+
+ + diff --git a/src/utils/ranges.ts b/src/utils/ranges.ts new file mode 100644 index 0000000..6508df8 --- /dev/null +++ b/src/utils/ranges.ts @@ -0,0 +1,185 @@ +import { readdir, readFile } from 'node:fs/promises' +import * as path from 'node:path' + +export interface BlockedIpResult { + blocked: boolean + list: string | null + range: string | null +} + +const IPV6_BITS = 128n +const IPV6_FULL_MASK = (1n << IPV6_BITS) - 1n +const toBlockLists = ['expressvpn', '1clickvpn'] + +type ParsedIp = + | { version: 4; value: number } + | { version: 6; value: bigint } + +type ParsedCidr = + | { version: 4; network: number; mask: number } + | { version: 6; network: bigint; mask: bigint } + +function ipv4ToInt(ip: string): number | null { + const parts = ip.trim().split('.') + if (parts.length !== 4) return null + + let result = 0 + for (const part of parts) { + if (!/^\d+$/.test(part)) return null + const value = Number(part) + if (value < 0 || value > 255) return null + result = (result << 8) + value + } + + return result >>> 0 +} + +function parseIpv6(ip: string): bigint | null { + let input = ip.trim().toLowerCase() + if (!input) return null + + const zoneSeparator = input.indexOf('%') + if (zoneSeparator >= 0) { + input = input.slice(0, zoneSeparator) + } + + if (input.includes('.')) { + const lastColon = input.lastIndexOf(':') + if (lastColon === -1) return null + + const ipv4Part = input.slice(lastColon + 1) + const ipv4Int = ipv4ToInt(ipv4Part) + if (ipv4Int === null) return null + + const high = ((ipv4Int >>> 16) & 0xffff).toString(16) + const low = (ipv4Int & 0xffff).toString(16) + input = `${input.slice(0, lastColon)}:${high}:${low}` + } + + if (input.split('::').length > 2) return null + + const hasCompression = input.includes('::') + const [leftPart, rightPart = ''] = input.split('::') + const left = leftPart ? leftPart.split(':').filter(Boolean) : [] + const right = rightPart ? rightPart.split(':').filter(Boolean) : [] + + const isValidHextet = (value: string) => /^[0-9a-f]{1,4}$/.test(value) + if (!left.every(isValidHextet) || !right.every(isValidHextet)) return null + + if (hasCompression) { + if (left.length + right.length > 7) return null + } else if (left.length !== 8) { + return null + } + + const missingCount = hasCompression ? 8 - (left.length + right.length) : 0 + const groups = [...left, ...Array(Math.max(0, missingCount)).fill('0'), ...right] + if (groups.length !== 8) return null + + let result = 0n + for (const group of groups) { + result = (result << 16n) + BigInt(parseInt(group, 16)) + } + + return result +} + +function parseIp(input: string): ParsedIp | null { + const ipv4 = ipv4ToInt(input) + if (ipv4 !== null) return { version: 4, value: ipv4 } + + const ipv6 = parseIpv6(input) + if (ipv6 !== null) return { version: 6, value: ipv6 } + + return null +} + +function parseCidr(cidr: string): ParsedCidr | null { + const [baseIp, prefixText] = cidr.split('/') + if (!baseIp || !prefixText || !/^\d+$/.test(prefixText)) return null + + const prefix = Number(prefixText) + + const ip = parseIp(baseIp) + if (!ip) return null + + if (ip.version === 4) { + if (prefix < 0 || prefix > 32) return null + const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0 + return { version: 4, network: ip.value & mask, mask } + } + + if (prefix < 0 || prefix > 128) return null + const mask = + prefix === 0 + ? 0n + : (IPV6_FULL_MASK ^ ((1n << (IPV6_BITS - BigInt(prefix))) - 1n)) & IPV6_FULL_MASK + + return { version: 6, network: ip.value & mask, mask } +} + +function isIpInCidr(ip: ParsedIp, cidr: string): boolean { + const parsed = parseCidr(cidr) + if (!parsed) return false + + if (ip.version === 4 && parsed.version === 4) { + return (ip.value & parsed.mask) === parsed.network + } + + if (ip.version === 6 && parsed.version === 6) { + return (ip.value & parsed.mask) === parsed.network + } + + return false +} + +function extractCidrs(text: string): string[] { + const cidrs: string[] = [] + const lines = text.split('\n') + + for (const line of lines) { + const cleaned = line.split('#')[0]?.trim() + if (!cleaned) continue + + const tokens = cleaned.split(/\s+/) + for (const token of tokens) { + if (!token.includes('/')) continue + cidrs.push(token.replace(/^[,;]+|[,;]+$/g, '')) + } + } + + return cidrs +} + +export async function checkIpRanges(ip: string): Promise { + const parsedIp = parseIp(ip) + if (parsedIp == null) { + return { blocked: false, list: null, range: null } + } + + const blockedDir = path.resolve(process.cwd(), 'ranges') + const entries = await readdir(blockedDir, { withFileTypes: true }) + + const files = entries + .filter(entry => entry.isFile() && entry.name.endsWith('.txt')) + .map(entry => entry.name) + .sort((a, b) => a.localeCompare(b)) + + for (const fileName of files) { + const filePath = path.join(blockedDir, fileName) + const content = await readFile(filePath, 'utf8') + const cidrs = extractCidrs(content) + + for (const cidr of cidrs) { + if (isIpInCidr(parsedIp, cidr)) { + return { + blocked: toBlockLists.includes(path.basename(fileName, '.txt')), + list: path.basename(fileName, '.txt'), + range: cidr + } + } + } + } + + return { blocked: false, list: null, range: null } +} \ No newline at end of file