/** * @description 描述来自 Cloudflare trace 接口的地理信息。 * * @example * ``` * fl=1232f78 * h=www.cloudflare.com * ip=23.132.124.129 * ts=1772261014.000 * visit_scheme=https * uag=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 Edg/144.0.0.0 * colo=SJC * sliver=none * http=http/2 * loc=US * tls=TLSv1.3 * sni=plaintext * warp=off * gateway=off * rbi=off * kex=X25519MLKEM768 * ``` */ export interface GeoInfoFromCloudflare { /** * @description Data center request was handled at (Cloudflare internal code) */ fl?: string | undefined /** * @description Hostname of the incoming HTTP request */ h?: string | undefined /** * @description Client IP address detected by Cloudflare */ ip?: string | undefined /** * @description Timestamp of request (in seconds) */ ts?: string | undefined /** * @description Request scheme used by the visitor (for example: http/https) */ visit_scheme?: string | undefined /** * @description User-Agent header value */ uag?: string | undefined /** * @description IATA-like code of the Cloudflare edge location */ colo?: string | undefined /** * @description Cloudflare edge “sliver” identifier */ sliver?: string | undefined /** * @description HTTP protocol version used for the request */ http?: string | undefined /** * @description Country code inferred from client IP (ISO 3166-1 alpha-2) */ loc?: string | undefined /** * @description TLS protocol version negotiated for the connection */ tls?: string | undefined /** * @description Server Name Indication (SNI) mode */ sni?: string | undefined /** * @description Whether WARP is enabled for the client */ warp?: string | undefined /** * @description Whether Cloudflare Gateway is enabled for the client */ gateway?: string | undefined /** * @description Whether browser isolation (RBI) is enabled */ rbi?: string | undefined /** * @description Key exchange algorithm used by TLS */ kex?: string | undefined } /** * @description Parse geolocation information from Cloudflare trace response. */ const parseGeoInfoFromCloudflare = (text: string): GeoInfoFromCloudflare => { const pairs = text .split("\n") .map((pair: string) => pair.split("=")) .filter(([key]) => key !== "") const result = pairs.reduce((geo: GeoInfoFromCloudflare, pair) => { const [key, value] = pair if (key !== undefined) { geo[key as keyof GeoInfoFromCloudflare] = value } return geo }, {}) return result } /** * @description Get device geolocation information from Cloudflare * * @see {@link https://dev.to/brojenuel/get-ip-clients-addresses-6k6} * @see {@link https://dataflowkit.com/blog/determine-location-of-users/} */ export const getGeoInfoFromCloudflare = async (): Promise => { const response = await fetch("https://www.cloudflare.com/cdn-cgi/trace", { method: "GET", mode: "cors", credentials: "omit", }) const responseText = await response.text() const geo = parseGeoInfoFromCloudflare(responseText) return geo } /** * @description 描述来自 ipapi 响应的地理信息。 * * @example * ``` * { * "ip": "23.132.124.129", * "network": "23.132.124.0/24", * "version": "IPv4", * "city": "Los Angeles", * "region": "California", * "region_code": "CA", * "country": "US", * "country_name": "United States", * "country_code": "US", * "country_code_iso3": "USA", * "country_capital": "Washington", * "country_tld": ".us", * "continent_code": "NA", * "in_eu": false, * "postal": "90001", * "latitude": 34.05223, * "longitude": -118.24368, * "timezone": "America/Los_Angeles", * "utc_offset": "-0800", * "country_calling_code": "+1", * "currency": "USD", * "currency_name": "Dollar", * "languages": "en-US,es-US,haw,fr", * "country_area": 9629091, * "country_population": 327167434, * "asn": "AS7018", * "org": "AT&T Enterprises, LLC" * } * ``` */ export interface GeoInfoFromIpapi { /** * @description Public (external) IP address (same as URL ip). */ ip?: string | undefined /** * @description Network address in CIDR notation. */ network?: string | undefined /** * @description IP version (IPv4 or IPv6). */ version?: string | undefined /** * @description City name. */ city?: string | undefined /** * @description Region name (administrative division). */ region?: string | undefined /** * @description Region code. */ region_code?: string | undefined /** * @description Country code (2 letter, ISO 3166-1 alpha-2). */ country?: string | undefined /** * @description Short country name. */ country_name?: string | undefined /** * @description Country code (2 letter, ISO 3166-1 alpha-2). */ country_code?: string | undefined /** * @description Country code (3 letter, ISO 3166-1 alpha-3). */ country_code_iso3?: string | undefined /** * @description Capital of the country. */ country_capital?: string | undefined /** * @description Country specific TLD (top-level domain). */ country_tld?: string | undefined /** * @description Continent code. */ continent_code?: string | undefined /** * @description Whether IP address belongs to a country that is a member of the European Union (EU). */ in_eu?: boolean | undefined /** * @description Postal code / zip code. */ postal?: string | undefined /** * @description Latitude coordinate. */ latitude?: number | undefined /** * @description Longitude coordinate. */ longitude?: number | undefined /** * @description Timezone (IANA format i.e. “Area/Location”). */ timezone?: string | undefined /** * @description UTC offset (with daylight saving time) as `+HHMM` or `-HHMM` (HH is hours, MM is minutes). */ utc_offset?: string | undefined /** * @description Country calling code (dial in code, comma separated). */ country_calling_code?: string | undefined /** * @description Currency code (ISO 4217 format). */ currency?: string | undefined /** * @description Currency name. */ currency_name?: string | undefined /** * @description Languages spoken (comma separated 2 or 3 letter ISO 639 code with optional hyphen separated * country suffix). */ languages?: string | undefined /** * @description Area of the country (in sq km). */ country_area?: number | undefined /** * @description Population of the country. */ country_population?: number | undefined /** * @description Autonomous System Number. */ asn?: string | undefined /** * @description Organization name. */ org?: string | undefined /** * @description Host or domain name associated with the IP (*optional beta add-on, please contact us for * details). */ hostname?: string | undefined } /** * @description Get geographical information from IP address using ipapi.co API. * * @see {@link https://ipapi.co/} */ export const getGeoInfoFromIpapi = async (): Promise => { const response = await fetch(`https://ipapi.co/json/`, { method: "GET", mode: "cors", credentials: "omit", }) const geo = (await response.json()) as GeoInfoFromIpapi return geo } /** * @description 描述来自 ipify API 的公网 IP 信息。 * * @example * ``` * { * "ip": "23.132.124.129" * } * ``` */ export interface GeoInfoFromIpify { ip: string } /** * @description Get public IP address using ipify API. * * @see {@link https://www.ipify.org/} */ export const getGeoInfoFromIpify = async (): Promise => { const response = await fetch("https://api.ipify.org?format=json", { method: "GET", }) const geo = (await response.json()) as GeoInfoFromIpify return geo } /** * @description Describe aggregated geolocation data from all providers. */ export interface GeoInfo { cloudflare: GeoInfoFromCloudflare ipapi: GeoInfoFromIpapi ipify: GeoInfoFromIpify } /** * @description Get geographical information from multiple sources. */ export const getGeoInfo = async (): Promise => { const [cloudflare, ipapi, ipify] = await Promise.all([ getGeoInfoFromCloudflare(), getGeoInfoFromIpapi(), getGeoInfoFromIpify(), ]) return { cloudflare, ipapi, ipify } }