aboutsummaryrefslogtreecommitdiff
path: root/src/lib/detect.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/detect.ts')
-rw-r--r--src/lib/detect.ts154
1 files changed, 154 insertions, 0 deletions
diff --git a/src/lib/detect.ts b/src/lib/detect.ts
new file mode 100644
index 0000000..68cb667
--- /dev/null
+++ b/src/lib/detect.ts
@@ -0,0 +1,154 @@
+import path from 'node:path';
+import { browserName, detectOS } from 'detect-browser';
+import ipaddr from 'ipaddr.js';
+import isLocalhost from 'is-localhost-ip';
+import maxmind from 'maxmind';
+import { UAParser } from 'ua-parser-js';
+import { getIpAddress, stripPort } from '@/lib/ip';
+import { safeDecodeURIComponent } from '@/lib/url';
+
+const MAXMIND = 'maxmind';
+
+const PROVIDER_HEADERS = [
+ // Cloudflare headers
+ {
+ countryHeader: 'cf-ipcountry',
+ regionHeader: 'cf-region-code',
+ cityHeader: 'cf-ipcity',
+ },
+ // Vercel headers
+ {
+ countryHeader: 'x-vercel-ip-country',
+ regionHeader: 'x-vercel-ip-country-region',
+ cityHeader: 'x-vercel-ip-city',
+ },
+ // CloudFront headers
+ {
+ countryHeader: 'cloudfront-viewer-country',
+ regionHeader: 'cloudfront-viewer-country-region',
+ cityHeader: 'cloudfront-viewer-city',
+ },
+];
+
+export function getDevice(userAgent: string, screen: string = '') {
+ const { device } = UAParser(userAgent);
+
+ const [width] = screen.split('x');
+
+ const type = device?.type || 'desktop';
+
+ if (type === 'desktop' && screen && +width <= 1920) {
+ return 'laptop';
+ }
+
+ return type;
+}
+
+function getRegionCode(country: string, region: string) {
+ if (!country || !region) {
+ return undefined;
+ }
+
+ return region.includes('-') ? region : `${country}-${region}`;
+}
+
+function decodeHeader(s: string | undefined | null): string | undefined | null {
+ if (s === undefined || s === null) {
+ return s;
+ }
+
+ return Buffer.from(s, 'latin1').toString('utf-8');
+}
+
+export async function getLocation(ip: string = '', headers: Headers, hasPayloadIP: boolean) {
+ // Ignore local ips
+ if (!ip || (await isLocalhost(ip))) {
+ return null;
+ }
+
+ if (!hasPayloadIP && !process.env.SKIP_LOCATION_HEADERS) {
+ for (const provider of PROVIDER_HEADERS) {
+ const countryHeader = headers.get(provider.countryHeader);
+ if (countryHeader) {
+ const country = decodeHeader(countryHeader);
+ const region = decodeHeader(headers.get(provider.regionHeader));
+ const city = decodeHeader(headers.get(provider.cityHeader));
+
+ return {
+ country,
+ region: getRegionCode(country, region),
+ city,
+ };
+ }
+ }
+ }
+
+ // Database lookup
+ if (!globalThis[MAXMIND]) {
+ const dir = path.join(process.cwd(), 'geo');
+
+ globalThis[MAXMIND] = await maxmind.open(
+ process.env.GEOLITE_DB_PATH || path.resolve(dir, 'GeoLite2-City.mmdb'),
+ );
+ }
+
+ const result = globalThis[MAXMIND]?.get(stripPort(ip));
+
+ if (result) {
+ const country = result.country?.iso_code ?? result?.registered_country?.iso_code;
+ const region = result.subdivisions?.[0]?.iso_code;
+ const city = result.city?.names?.en;
+
+ return {
+ country,
+ region: getRegionCode(country, region),
+ city,
+ };
+ }
+}
+
+export async function getClientInfo(request: Request, payload: Record<string, any>) {
+ const userAgent = payload?.userAgent || request.headers.get('user-agent');
+ const ip = payload?.ip || getIpAddress(request.headers);
+ const location = await getLocation(ip, request.headers, !!payload?.ip);
+ const country = safeDecodeURIComponent(location?.country);
+ const region = safeDecodeURIComponent(location?.region);
+ const city = safeDecodeURIComponent(location?.city);
+ const browser = payload?.browser ?? browserName(userAgent);
+ const os = payload?.os ?? (detectOS(userAgent) as string);
+ const device = payload?.device ?? getDevice(userAgent, payload?.screen);
+
+ return { userAgent, browser, os, ip, country, region, city, device };
+}
+
+export function hasBlockedIp(clientIp: string) {
+ const ignoreIps = process.env.IGNORE_IP;
+
+ if (ignoreIps) {
+ const ips = [];
+
+ if (ignoreIps) {
+ ips.push(...ignoreIps.split(',').map(n => n.trim()));
+ }
+
+ return ips.find(ip => {
+ if (ip === clientIp) {
+ return true;
+ }
+
+ // CIDR notation
+ if (ip.indexOf('/') > 0) {
+ const addr = ipaddr.parse(clientIp);
+ const range = ipaddr.parseCIDR(ip);
+
+ if (addr.kind() === range[0].kind() && addr.match(range)) {
+ return true;
+ }
+ }
+
+ return false;
+ });
+ }
+
+ return false;
+}