@proxyherd/fingerprint 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # ProxyHerd fingerprint service
2
+
3
+ The service every ProxyHerd installation points `FINGERPRINT_URL` at. Each
4
+ proxy fetches it to prove it is online, and the answer tells ProxyHerd the
5
+ proxy's public IP address, its network (ASN) and its approximate location,
6
+ resolved from a GeoIP database on your own disk. Nothing is reported to a
7
+ third party.
8
+
9
+ It must be reachable **through your proxies**, that is from the public
10
+ internet, not only from the machine running ProxyHerd.
11
+
12
+ ## Run
13
+
14
+ ```shell
15
+ docker run -d -p 3000:3000 -v proxyherd-geoip:/data/geoip ghcr.io/proxyherd/fingerprint
16
+ ```
17
+
18
+ On first start it downloads the free DB-IP "lite" city and ASN databases
19
+ (about 150 MB) into the volume and refreshes them weekly. Then set
20
+ `FINGERPRINT_URL=https://<public host>/api/json` on ProxyHerd.
21
+
22
+ Put it behind TLS (a reverse proxy or a platform that terminates HTTPS) and
23
+ set `TRUST_PROXY=true` so the client address is read from `X-Forwarded-For`.
24
+
25
+ ## Endpoints
26
+
27
+ | Path | Answer |
28
+ |---|---|
29
+ | `GET /api/json` | `{ ip, useragent, asnName, asnNetwork, continentCode, continentName, countryCode, countryName, cityName, timezone, latitude, longitude }`; every field but `ip` may be `null` |
30
+ | `GET /api/text` | the client IP address as plain text |
31
+ | `GET /health` | provider, database state and last refresh |
32
+ | `GET /` | a short description and the GeoIP attribution |
33
+
34
+ ## Configuration
35
+
36
+ | Variable | Default | Meaning |
37
+ |---|---|---|
38
+ | `FINGERPRINT_PORT` | `3000` | Listening port |
39
+ | `GEOIP_PROVIDER` | `dbip` | `dbip` (free, no key), `maxmind` (GeoLite2, needs a free licence key, better accuracy) or `none` (IP only) |
40
+ | `GEOIP_DIR` | `geoip` (`/data/geoip` in the image) | Where the `.mmdb` files live |
41
+ | `GEOIP_AUTO_DOWNLOAD` | `true` | Download the databases at start and refresh them |
42
+ | `GEOIP_REFRESH_HOURS` | `168` | Refresh interval |
43
+ | `MAXMIND_LICENSE_KEY` | | Required for `GEOIP_PROVIDER=maxmind` |
44
+ | `TRUST_PROXY` | `false` | Read the client address from `X-Forwarded-For` (only behind a reverse proxy you control) |
45
+
46
+ You can also drop your own `city.mmdb` and `asn.mmdb` into `GEOIP_DIR` and
47
+ set `GEOIP_AUTO_DOWNLOAD=false`.
48
+
49
+ ## Attribution
50
+
51
+ With the default provider, IP geolocation is provided by
52
+ [DB-IP](https://db-ip.com) under the Creative Commons Attribution 4.0
53
+ licence. With `maxmind`, the data is from MaxMind's GeoLite2 databases under
54
+ the GeoLite2 End User License Agreement; you need your own account and key.
package/fingerprint.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ (()=>{"use strict";var e={r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}};e.r({});require("reflect-metadata");const t=require("@nestjs/common"),n=require("@nestjs/core");var r;!function(e){e.Dbip="dbip",e.Maxmind="maxmind",e.None="none"}(r||(r={}));const o="FINGERPRINT_SERVICE_CONFIG";function i(e,t){return void 0===e||e.length<=0?t:["1","true","yes","on"].includes(e.toLowerCase())}function s(){const e=function(e){switch((e??"dbip").toLowerCase()){case"maxmind":return r.Maxmind;case"none":return r.None;default:return r.Dbip}}(process.env.GEOIP_PROVIDER),t=process.env.MAXMIND_LICENSE_KEY;if(e===r.Maxmind&&!t)throw new Error("GEOIP_PROVIDER=maxmind requires MAXMIND_LICENSE_KEY (free at https://www.maxmind.com/en/geolite2/signup)");return{port:parseInt(process.env.FINGERPRINT_PORT??"3000",10),provider:e,dataDir:process.env.GEOIP_DIR??"geoip",autoDownload:i(process.env.GEOIP_AUTO_DOWNLOAD,!0),refreshIntervalMs:60*parseInt(process.env.GEOIP_REFRESH_HOURS??"168",10)*60*1e3,maxmindLicenseKey:t,trustProxy:i(process.env.TRUST_PROXY,!1),downloadTimeoutMs:parseInt(process.env.GEOIP_DOWNLOAD_TIMEOUT??"600000",10)}}const a=require("fs"),c=require("maxmind"),l=require("child_process"),d=require("http"),u=require("https"),p=require("path"),f=require("stream/promises"),g=require("zlib"),h=5;function m(e,t){return new Promise(((n,r)=>{const o=new URL(e),i=("https:"===o.protocol?u.request:d.request)(o,{method:"GET",headers:{"User-Agent":"ProxyHerd-fingerprint"},timeout:t},(e=>{n(e)}));i.on("timeout",(()=>{i.destroy(new Error(`Timeout while downloading ${e}`))})),i.on("error",r),i.end()}))}async function y(e,t,n){await a.promises.mkdir((0,p.dirname)(t),{recursive:!0});const r=`${t}.download`,o=await async function(e,t){let n=e;for(let r=0;r<=h;r++){const e=await m(n,t),r=e.statusCode??0;if(!(r>=300&&r<400&&e.headers.location)){if(200!==r)throw e.resume(),new Error(`Download of ${n} failed with status ${r}`);return e}e.resume(),n=new URL(e.headers.location,n).toString()}throw new Error(`Too many redirects while downloading ${e}`)}(e,n.timeoutMs);try{n.gunzip?await(0,f.pipeline)(o,(0,g.createGunzip)(),(0,a.createWriteStream)(r)):await(0,f.pipeline)(o,(0,a.createWriteStream)(r)),await a.promises.rename(r,t)}catch(i){throw await a.promises.rm(r,{force:!0}),i}}async function w(e,t){const n=await a.promises.readdir(e,{withFileTypes:!0});for(const r of n){const n=(0,p.join)(e,r.name);if(r.isDirectory()){const e=await w(n,t);if(e)return e}else if(r.name===t)return n}return null}async function v(e,t,n){const r=(0,p.dirname)(n),o=(0,p.join)(r,`.extract-${process.pid}`);await a.promises.mkdir(o,{recursive:!0});try{await new Promise(((n,r)=>{(0,l.execFile)("tar",["-xzf",e,"-C",o,"--wildcards",t],((e,t,o)=>{e?r(new Error(`tar failed: ${o.length>0?o:e.message}`)):n()}))}));const r=await w(o,n.split("/").pop());if(!r)throw new Error(`${t} not found in ${e}`);await a.promises.rename(r,n)}finally{await a.promises.rm(o,{recursive:!0,force:!0})}}const I=require("net"),b="::ffff:";function R(e){const t=e.toLowerCase();if(t.startsWith(b)){const e=t.substring(b.length);if((0,I.isIPv4)(e))return e}return e}function $(e,t){const n=R(e);if((0,I.isIPv4)(n)){if(t<0||t>32)return null;const e=n.split(".").map((e=>parseInt(e,10))),r=((e[0]<<24>>>0)+(e[1]<<16)+(e[2]<<8)+e[3]&(0===t?0:4294967295<<32-t>>>0))>>>0;return`${[r>>>24,r>>>16&255,r>>>8&255,255&r].join(".")}/${t}`}if((0,I.isIPv6)(n)){if(t<0||t>128)return null;const e=function(e){const[t,n]=e.split("::"),r=t.length>0?t.split(":"):[],o=n&&n.length>0?n.split(":"):[],i=8-r.length-o.length,s=[...r,...new Array(Math.max(i,0)).fill("0"),...o];let a=BigInt(0);for(const c of s)a=(a<<BigInt(16))+BigInt(parseInt(c,16));return a}(n.split("%")[0]),r=(BigInt(1)<<BigInt(128))-BigInt(1);return`${function(e){const t=[];for(let s=7;s>=0;s--)t.push(Number(e>>BigInt(16*s)&BigInt(65535)));let n=0,r=-1,o=-1;for(let s=0;s<=t.length;s++)s<t.length&&0===t[s]?o<0&&(o=s):(o>=0&&s-o>n&&(r=o,n=s-o),o=-1);const i=t.map((e=>e.toString(16)));if(n>=2)return`${i.slice(0,r).join(":")}::${i.slice(r+n).join(":")}`;return i.join(":")}(e&(0===t?BigInt(0):r<<BigInt(128-t)&r))}/${t}`}return null}var P,D=function(e,t,n,r){var o,i=arguments.length,s=i<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(e,t,n,r);else for(var a=e.length-1;a>=0;a--)(o=e[a])&&(s=(i<3?o(s):i>3?o(t,n,s):o(t,n))||s);return i>3&&s&&Object.defineProperty(t,n,s),s},O=function(e,t){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(e,t)},E=function(e,t){return function(n,r){t(n,r,e)}};const x={asnName:null,asnNetwork:null,continentCode:null,continentName:null,countryCode:null,countryName:null,cityName:null,timezone:null,latitude:null,longitude:null},M="city.mmdb",j="asn.mmdb";let T=P=class GeoipService{config;logger=new t.Logger(P.name);cityReader=void 0;asnReader=void 0;refreshedAt=void 0;lastError=void 0;timer=void 0;constructor(e){this.config=e}async onModuleInit(){if(this.config.provider===r.None)return void this.logger.log('GeoIP provider "none": responses carry the IP address only');await a.promises.mkdir(this.config.dataDir,{recursive:!0});const e=await this.load();!e&&this.config.autoDownload?await this.refresh():e||this.logger.warn(`No GeoIP database found in ${this.config.dataDir} and GEOIP_AUTO_DOWNLOAD is off: responses carry the IP address only`),this.config.autoDownload&&this.config.refreshIntervalMs>0&&(this.timer=setInterval((()=>{this.refresh().catch((()=>{}))}),this.config.refreshIntervalMs),this.timer.unref())}onModuleDestroy(){this.timer&&(clearInterval(this.timer),this.timer=void 0)}get status(){let e;switch(this.config.provider){case r.Dbip:e="IP Geolocation by DB-IP (https://db-ip.com), CC BY 4.0";break;case r.Maxmind:e="This product includes GeoLite2 data created by MaxMind (https://www.maxmind.com)";break;default:e=null}return{provider:this.config.provider,attribution:e,city:!!this.cityReader,asn:!!this.asnReader,refreshedAt:this.refreshedAt?this.refreshedAt.toISOString():null,lastError:this.lastError??null}}lookup(e){const t={...x};if(this.cityReader)try{const n=this.cityReader.get(e);n&&(t.continentCode=n.continent?.code??null,t.continentName=n.continent?.names?.en??null,t.countryCode=n.country?.iso_code??null,t.countryName=n.country?.names?.en??null,t.cityName=n.city?.names?.en??null,t.timezone=n.location?.time_zone??null,t.latitude=n.location?.latitude??null,t.longitude=n.location?.longitude??null)}catch(n){this.logger.debug(`City lookup failed for ${e}: ${n.message}`)}if(this.asnReader)try{const[n,r]=this.asnReader.getWithPrefixLength(e);n&&(t.asnName=n.autonomous_system_organization??null,t.asnNetwork=$(e,r))}catch(n){this.logger.debug(`ASN lookup failed for ${e}: ${n.message}`)}return t}async refresh(){this.logger.log(`Downloading GeoIP databases from ${this.config.provider} into ${this.config.dataDir}`);try{switch(this.config.provider){case r.Dbip:await this.downloadDbip();break;case r.Maxmind:await this.downloadMaxmind();break;default:return}await this.load(),this.refreshedAt=new Date,this.lastError=void 0,this.logger.log("GeoIP databases updated")}catch(e){this.lastError=e.message,this.logger.error(`GeoIP download failed: ${e.message}`)}}async load(){const[e,t]=await Promise.all([this.openReader(M),this.openReader(j)]);return e&&(this.cityReader=e),t&&(this.asnReader=t),(void 0!==e||void 0!==t)&&(this.logger.log(`GeoIP databases loaded (city: ${!!e}, asn: ${!!t})`),!0)}async openReader(e){const t=`${this.config.dataDir}/${e}`;try{await a.promises.access(t)}catch(n){return}try{return await(0,c.open)(t)}catch(n){return void this.logger.warn(`Cannot open ${t}: ${n.message}`)}}async downloadDbip(){const e=new Date,t=[];for(let o=0;o<2;o++){const n=new Date(Date.UTC(e.getUTCFullYear(),e.getUTCMonth()-o,1));t.push(`${n.getUTCFullYear()}-${String(n.getUTCMonth()+1).padStart(2,"0")}`)}let n;for(const o of t)try{return await y(`https://download.db-ip.com/free/dbip-city-lite-${o}.mmdb.gz`,`${this.config.dataDir}/${M}`,{gunzip:!0,timeoutMs:this.config.downloadTimeoutMs}),void(await y(`https://download.db-ip.com/free/dbip-asn-lite-${o}.mmdb.gz`,`${this.config.dataDir}/${j}`,{gunzip:!0,timeoutMs:this.config.downloadTimeoutMs}))}catch(r){n=r,this.logger.warn(`DB-IP ${o} not available (${r.message}), trying the previous month`)}throw n??new Error("DB-IP download failed")}async downloadMaxmind(){const e=[["GeoLite2-City",M],["GeoLite2-ASN",j]];for(const[t,n]of e){const e=`${this.config.dataDir}/${t}.tar.gz`;await y(`https://download.maxmind.com/app/geoip_download?edition_id=${t}&license_key=${encodeURIComponent(this.config.maxmindLicenseKey)}&suffix=tar.gz`,e,{gunzip:!1,timeoutMs:this.config.downloadTimeoutMs});try{await v(e,`*/${t}.mmdb`,`${this.config.dataDir}/${n}`)}finally{await a.promises.rm(e,{force:!0})}}}};T=P=D([(0,t.Injectable)(),E(0,(0,t.Inject)(o)),O("design:paramtypes",[Object])],T);var G,N=function(e,t,n,r){var o,i=arguments.length,s=i<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(e,t,n,r);else for(var a=e.length-1;a>=0;a--)(o=e[a])&&(s=(i<3?o(s):i>3?o(t,n,s):o(t,n))||s);return i>3&&s&&Object.defineProperty(t,n,s),s},_=function(e,t){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(e,t)},C=function(e,t){return function(n,r){t(n,r,e)}};function S(e){return R(e.ip??e.socket.remoteAddress??"")}let k=class FingerprintController{geoip;constructor(e){this.geoip=e}getRoot(){const e=this.geoip.status,t=["ProxyHerd fingerprint service","GET /api/json -> IP, network and location of the caller as JSON","GET /api/text -> IP of the caller as text","GET /health -> service status"];return e.attribution&&t.push(e.attribution),`${t.join("\n")}\n`}getHealth(){return{status:"ok",...this.geoip.status}}getJson(e){const t=S(e),n=e.headers["user-agent"];return{ip:t,useragent:"string"==typeof n&&n.length>0?n:null,...this.geoip.lookup(t)}}getText(e){return S(e)}};N([(0,t.Get)(),(0,t.Header)("Content-Type","text/plain; charset=utf-8"),_("design:type",Function),_("design:paramtypes",[]),_("design:returntype",String)],k.prototype,"getRoot",null),N([(0,t.Get)("health"),_("design:type",Function),_("design:paramtypes",[]),_("design:returntype",Object)],k.prototype,"getHealth",null),N([(0,t.Get)("api/json"),C(0,(0,t.Req)()),_("design:type",Function),_("design:paramtypes",[Object]),_("design:returntype",Object)],k.prototype,"getJson",null),N([(0,t.Get)("api/text"),(0,t.Header)("Content-Type","text/plain; charset=utf-8"),C(0,(0,t.Req)()),_("design:type",Function),_("design:paramtypes",[Object]),_("design:returntype",String)],k.prototype,"getText",null),k=N([(0,t.Controller)(),_("design:paramtypes",["function"==typeof(G=void 0!==T&&T)?G:Object])],k);var A,L=function(e,t,n,r){var o,i=arguments.length,s=i<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,n):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(e,t,n,r);else for(var a=e.length-1;a>=0;a--)(o=e[a])&&(s=(i<3?o(s):i>3?o(t,n,s):o(t,n))||s);return i>3&&s&&Object.defineProperty(t,n,s),s};let q=A=class AppModule{static forRoot(e){return{module:A,controllers:[k],providers:[{provide:o,useValue:e},T]}}};q=A=L([(0,t.Module)({})],q),(async()=>{const e=s(),r=await n.NestFactory.create(q.forRoot(e),{logger:["log","warn","error"]});r.enableShutdownHooks(),e.trustProxy&&r.set("trust proxy",!0),await r.listen(e.port),new t.Logger("Fingerprint").log(`ProxyHerd fingerprint service listening on port ${e.port} (GeoIP provider: ${e.provider})`)})().catch((e=>{console.error(e.message??e),process.exit(1)}))})();
3
+ //# sourceMappingURL=fingerprint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fingerprint.js","mappings":"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","sources":["webpack:///./webpack/bootstrap","webpack:///./webpack/runtime/make namespace object","webpack:///./external commonjs \"reflect-metadata\"","webpack:///./external commonjs \"@nestjs/common\"","webpack:///./external commonjs \"@nestjs/core\"","webpack:///./packages/fingerprint/app/src/config.ts","webpack:///./external node-commonjs \"fs\"","webpack:///./external commonjs \"maxmind\"","webpack:///./external node-commonjs \"child_process\"","webpack:///./external node-commonjs \"http\"","webpack:///./external node-commonjs \"https\"","webpack:///./external node-commonjs \"path\"","webpack:///./external node-commonjs \"stream/promises\"","webpack:///./external node-commonjs \"zlib\"","webpack:///./packages/fingerprint/app/src/geoip/download.ts","webpack:///./external node-commonjs \"net\"","webpack:///./packages/fingerprint/app/src/geoip/network.ts","webpack:///./packages/fingerprint/app/src/geoip/geoip.service.ts","webpack:///./packages/fingerprint/app/src/fingerprint.controller.ts","webpack:///./packages/fingerprint/app/src/app.module.ts","webpack:///./packages/fingerprint/app/src/main.ts"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","const __WEBPACK_NAMESPACE_OBJECT__ = require(\"reflect-metadata\");","const __WEBPACK_NAMESPACE_OBJECT__ = require(\"@nestjs/common\");","const __WEBPACK_NAMESPACE_OBJECT__ = require(\"@nestjs/core\");","export enum EGeoipProvider {\n Dbip = 'dbip',\n Maxmind = 'maxmind',\n None = 'none',\n}\n\n\nexport interface IFingerprintServiceConfig {\n port: number;\n provider: EGeoipProvider;\n dataDir: string;\n autoDownload: boolean;\n refreshIntervalMs: number;\n maxmindLicenseKey: string | undefined;\n trustProxy: boolean;\n downloadTimeoutMs: number;\n}\n\n\nexport const FINGERPRINT_SERVICE_CONFIG = 'FINGERPRINT_SERVICE_CONFIG';\n\n\nfunction parseBoolean(\n value: string | undefined, defaultValue: boolean\n): boolean {\n if (value === undefined || value.length <= 0) {\n return defaultValue;\n }\n\n return [\n '1',\n 'true',\n 'yes',\n 'on',\n ].includes(value.toLowerCase());\n}\n\n\nfunction parseProvider(value: string | undefined): EGeoipProvider {\n switch ((value ?? 'dbip').toLowerCase()) {\n case 'maxmind': {\n return EGeoipProvider.Maxmind;\n }\n\n case 'none': {\n return EGeoipProvider.None;\n }\n\n default: {\n return EGeoipProvider.Dbip;\n }\n }\n}\n\n\nexport function getEnvConfig(): IFingerprintServiceConfig {\n const provider = parseProvider(process.env.GEOIP_PROVIDER);\n const maxmindLicenseKey = process.env.MAXMIND_LICENSE_KEY;\n\n if (provider === EGeoipProvider.Maxmind && !maxmindLicenseKey) {\n throw new Error('GEOIP_PROVIDER=maxmind requires MAXMIND_LICENSE_KEY (free at https://www.maxmind.com/en/geolite2/signup)');\n }\n\n return {\n port: parseInt(\n process.env.FINGERPRINT_PORT ?? '3000',\n 10\n ),\n provider,\n dataDir: process.env.GEOIP_DIR ?? 'geoip',\n autoDownload: parseBoolean(\n process.env.GEOIP_AUTO_DOWNLOAD,\n true\n ),\n refreshIntervalMs: parseInt(\n process.env.GEOIP_REFRESH_HOURS ?? '168',\n 10\n ) * 60 * 60 * 1000,\n maxmindLicenseKey,\n trustProxy: parseBoolean(\n process.env.TRUST_PROXY,\n false\n ),\n downloadTimeoutMs: parseInt(\n process.env.GEOIP_DOWNLOAD_TIMEOUT ?? '600000',\n 10\n ),\n };\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = require(\"fs\");","const __WEBPACK_NAMESPACE_OBJECT__ = require(\"maxmind\");","const __WEBPACK_NAMESPACE_OBJECT__ = require(\"child_process\");","const __WEBPACK_NAMESPACE_OBJECT__ = require(\"http\");","const __WEBPACK_NAMESPACE_OBJECT__ = require(\"https\");","const __WEBPACK_NAMESPACE_OBJECT__ = require(\"path\");","const __WEBPACK_NAMESPACE_OBJECT__ = require(\"stream/promises\");","const __WEBPACK_NAMESPACE_OBJECT__ = require(\"zlib\");","import { execFile } from 'child_process';\nimport {\n createWriteStream,\n promises as fs,\n} from 'fs';\nimport { request as requestHttp } from 'http';\nimport { request as requestHttps } from 'https';\nimport {\n dirname,\n join,\n} from 'path';\nimport { pipeline } from 'stream/promises';\nimport { createGunzip } from 'zlib';\nimport type { IncomingMessage } from 'http';\n\n\nconst MAX_REDIRECTS = 5;\n\n\nfunction get(\n url: string, timeoutMs: number\n): Promise<IncomingMessage> {\n return new Promise<IncomingMessage>((\n resolve, reject\n ) => {\n const parsed = new URL(url);\n const request = parsed.protocol === 'https:' ? requestHttps : requestHttp;\n const req = request(\n parsed,\n {\n method: 'GET',\n headers: {\n 'User-Agent': 'ProxyHerd-fingerprint',\n },\n timeout: timeoutMs,\n },\n (res) => {\n resolve(res);\n }\n );\n\n req.on(\n 'timeout',\n () => {\n req.destroy(new Error(`Timeout while downloading ${url}`));\n }\n );\n req.on(\n 'error',\n reject\n );\n req.end();\n });\n}\n\n\nasync function getFollowingRedirects(\n url: string, timeoutMs: number\n): Promise<IncomingMessage> {\n let current = url;\n\n for (let i = 0; i <= MAX_REDIRECTS; i++) {\n const res = await get(\n current,\n timeoutMs\n );\n const status = res.statusCode ?? 0;\n\n if (status >= 300 && status < 400 && res.headers.location) {\n res.resume();\n current = new URL(\n res.headers.location,\n current\n )\n .toString();\n\n continue;\n }\n\n if (status !== 200) {\n res.resume();\n\n throw new Error(`Download of ${current} failed with status ${status}`);\n }\n\n return res;\n }\n\n throw new Error(`Too many redirects while downloading ${url}`);\n}\n\n\n/**\n * Download a URL to a file. A \".gz\" body is decompressed on the fly when `gunzip` is true.\n * The file is written to a temporary name and renamed at the end, so readers never see a partial file.\n */\nexport async function downloadToFile(\n url: string, destination: string, options: { gunzip: boolean; timeoutMs: number }\n): Promise<void> {\n await fs.mkdir(\n dirname(destination),\n {\n recursive: true,\n }\n );\n\n const temporary = `${destination}.download`;\n const res = await getFollowingRedirects(\n url,\n options.timeoutMs\n );\n\n try {\n if (options.gunzip) {\n await pipeline(\n res,\n createGunzip(),\n createWriteStream(temporary)\n );\n } else {\n await pipeline(\n res,\n createWriteStream(temporary)\n );\n }\n\n await fs.rename(\n temporary,\n destination\n );\n } catch (err: any) {\n await fs.rm(\n temporary,\n {\n force: true,\n }\n );\n\n throw err;\n }\n}\n\n\nasync function findFile(\n directory: string, filename: string\n): Promise<string | null> {\n const entries = await fs.readdir(\n directory,\n {\n withFileTypes: true,\n }\n );\n\n for (const entry of entries) {\n const path = join(\n directory,\n entry.name\n );\n\n if (entry.isDirectory()) {\n const nested = await findFile(\n path,\n filename\n );\n\n if (nested) {\n return nested;\n }\n } else if (entry.name === filename) {\n return path;\n }\n }\n\n return null;\n}\n\n\n/**\n * Extract a single file matching `pattern` from a .tar.gz archive using the system tar\n * (present on Linux, macOS and the Alpine image), writing it to `destination`.\n */\nexport async function extractFromTarGz(\n archive: string, pattern: string, destination: string\n): Promise<void> {\n const directory = dirname(destination);\n const scratch = join(\n directory,\n `.extract-${process.pid}`\n );\n await fs.mkdir(\n scratch,\n {\n recursive: true,\n }\n );\n\n try {\n await new Promise<void>((\n resolve, reject\n ) => {\n execFile(\n 'tar',\n [\n '-xzf',\n archive,\n '-C',\n scratch,\n '--wildcards',\n pattern,\n ],\n (\n err, stdout, stderr\n ) => {\n if (err) {\n reject(new Error(`tar failed: ${stderr.length > 0 ? stderr : err.message}`));\n\n return;\n }\n\n resolve();\n }\n );\n });\n\n const found = await findFile(\n scratch,\n destination.split('/')\n .pop() as string\n );\n\n if (!found) {\n throw new Error(`${pattern} not found in ${archive}`);\n }\n\n await fs.rename(\n found,\n destination\n );\n } finally {\n await fs.rm(\n scratch,\n {\n recursive: true,\n force: true,\n }\n );\n }\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = require(\"net\");","import {\n isIPv4,\n isIPv6,\n} from 'net';\n\n\nconst IPV4_MAPPED_PREFIX = '::ffff:';\n\n\n/**\n * Normalise an address as reported by a socket: IPv4-mapped IPv6 addresses become plain IPv4.\n */\nexport function normalizeIp(ip: string): string {\n const lower = ip.toLowerCase();\n\n if (lower.startsWith(IPV4_MAPPED_PREFIX)) {\n const candidate = lower.substring(IPV4_MAPPED_PREFIX.length);\n\n if (isIPv4(candidate)) {\n return candidate;\n }\n }\n\n return ip;\n}\n\n\nfunction expandIpv6(ip: string): bigint {\n const [\n head, tail,\n ] = ip.split('::');\n const headParts = head.length > 0 ? head.split(':') : [];\n const tailParts = tail && tail.length > 0 ? tail.split(':') : [];\n const missing = 8 - headParts.length - tailParts.length;\n const parts = [\n ...headParts,\n ...new Array<string>(Math.max(\n missing,\n 0\n ))\n .fill('0'),\n ...tailParts,\n ];\n let value = BigInt(0);\n\n for (const part of parts) {\n value = (value << BigInt(16)) + BigInt(parseInt(\n part,\n 16\n ));\n }\n\n return value;\n}\n\n\nfunction compressIpv6(value: bigint): string {\n const groups: number[] = [];\n\n for (let i = 7; i >= 0; i--) {\n groups.push(Number(value >> BigInt(16 * i) & BigInt(0xffff)));\n }\n\n // Find the longest run of zero groups to replace with \"::\"\n let bestLength = 0,\n bestStart = -1,\n start = -1;\n\n for (let i = 0; i <= groups.length; i++) {\n if (i < groups.length && groups[ i ] === 0) {\n if (start < 0) {\n start = i;\n }\n } else {\n if (start >= 0 && i - start > bestLength) {\n bestStart = start;\n bestLength = i - start;\n }\n\n start = -1;\n }\n }\n\n const hex = groups.map((g) => g.toString(16));\n\n if (bestLength >= 2) {\n const left = hex.slice(\n 0,\n bestStart\n )\n .join(':');\n const right = hex.slice(bestStart + bestLength)\n .join(':');\n\n return `${left}::${right}`;\n }\n\n return hex.join(':');\n}\n\n\n/**\n * Compute the network in CIDR notation that contains `ip` for the given prefix length,\n * e.g. (\"13.38.69.243\", 14) => \"13.36.0.0/14\".\n */\nexport function networkFromPrefix(\n ip: string, prefixLength: number\n): string | null {\n const normalized = normalizeIp(ip);\n\n if (isIPv4(normalized)) {\n if (prefixLength < 0 || prefixLength > 32) {\n return null;\n }\n\n const octets = normalized.split('.')\n .map((o) => parseInt(\n o,\n 10\n ));\n const value = (octets[ 0 ] << 24 >>> 0) + (octets[ 1 ] << 16) + (octets[ 2 ] << 8) + octets[ 3 ];\n const mask = prefixLength === 0 ? 0 : 0xffffffff << 32 - prefixLength >>> 0;\n const network = (value & mask) >>> 0;\n const parts = [\n network >>> 24,\n network >>> 16 & 0xff,\n network >>> 8 & 0xff,\n network & 0xff,\n ];\n\n return `${parts.join('.')}/${prefixLength}`;\n }\n\n if (isIPv6(normalized)) {\n if (prefixLength < 0 || prefixLength > 128) {\n return null;\n }\n\n const value = expandIpv6(normalized.split('%')[ 0 ]);\n const all = (BigInt(1) << BigInt(128)) - BigInt(1);\n const mask = prefixLength === 0 ? BigInt(0) : all << BigInt(128 - prefixLength) & all;\n\n return `${compressIpv6(value & mask)}/${prefixLength}`;\n }\n\n return null;\n}\n","import { promises as fs } from 'fs';\nimport {\n Inject,\n Injectable,\n Logger,\n} from '@nestjs/common';\nimport { open } from 'maxmind';\nimport {\n downloadToFile,\n extractFromTarGz,\n} from './download';\nimport { networkFromPrefix } from './network';\nimport {\n EGeoipProvider,\n FINGERPRINT_SERVICE_CONFIG,\n} from '../config';\nimport type { IFingerprintServiceConfig } from '../config';\nimport type {\n OnModuleDestroy,\n OnModuleInit,\n} from '@nestjs/common';\nimport type {\n AsnResponse,\n CityResponse,\n Response as MaxmindResponse,\n Reader,\n} from 'maxmind';\n\n\nexport interface IGeoipLookup {\n asnName: string | null;\n asnNetwork: string | null;\n continentCode: string | null;\n continentName: string | null;\n countryCode: string | null;\n countryName: string | null;\n cityName: string | null;\n timezone: string | null;\n latitude: number | null;\n longitude: number | null;\n}\n\n\nexport interface IGeoipStatus {\n provider: EGeoipProvider;\n attribution: string | null;\n city: boolean;\n asn: boolean;\n refreshedAt: string | null;\n lastError: string | null;\n}\n\n\nexport const EMPTY_LOOKUP: IGeoipLookup = {\n asnName: null,\n asnNetwork: null,\n continentCode: null,\n continentName: null,\n countryCode: null,\n countryName: null,\n cityName: null,\n timezone: null,\n latitude: null,\n longitude: null,\n};\n\nconst CITY_FILENAME = 'city.mmdb';\nconst ASN_FILENAME = 'asn.mmdb';\nconst DBIP_ATTRIBUTION = 'IP Geolocation by DB-IP (https://db-ip.com), CC BY 4.0';\nconst MAXMIND_ATTRIBUTION = 'This product includes GeoLite2 data created by MaxMind (https://www.maxmind.com)';\n\n\n@Injectable()\nexport class GeoipService implements OnModuleInit, OnModuleDestroy {\n private readonly logger = new Logger(GeoipService.name);\n\n private cityReader: Reader<CityResponse> | undefined = void 0;\n\n private asnReader: Reader<AsnResponse> | undefined = void 0;\n\n private refreshedAt: Date | undefined = void 0;\n\n private lastError: string | undefined = void 0;\n\n private timer: NodeJS.Timeout | undefined = void 0;\n\n constructor(@Inject(FINGERPRINT_SERVICE_CONFIG) private readonly config: IFingerprintServiceConfig) {}\n\n async onModuleInit(): Promise<void> {\n if (this.config.provider === EGeoipProvider.None) {\n this.logger.log('GeoIP provider \"none\": responses carry the IP address only');\n\n return;\n }\n\n await fs.mkdir(\n this.config.dataDir,\n {\n recursive: true,\n }\n );\n\n const loaded = await this.load();\n\n if (!loaded && this.config.autoDownload) {\n await this.refresh();\n } else if (!loaded) {\n this.logger.warn(`No GeoIP database found in ${this.config.dataDir} and GEOIP_AUTO_DOWNLOAD is off: responses carry the IP address only`);\n }\n\n if (this.config.autoDownload && this.config.refreshIntervalMs > 0) {\n this.timer = setInterval(\n () => {\n this.refresh()\n .catch(() => void 0);\n },\n this.config.refreshIntervalMs\n );\n this.timer.unref();\n }\n }\n\n onModuleDestroy() {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = void 0;\n }\n }\n\n get status(): IGeoipStatus {\n let attribution: string | null;\n\n switch (this.config.provider) {\n case EGeoipProvider.Dbip: {\n attribution = DBIP_ATTRIBUTION;\n break;\n }\n\n case EGeoipProvider.Maxmind: {\n attribution = MAXMIND_ATTRIBUTION;\n break;\n }\n\n default: {\n attribution = null;\n }\n }\n\n return {\n provider: this.config.provider,\n attribution,\n city: !!this.cityReader,\n asn: !!this.asnReader,\n refreshedAt: this.refreshedAt ? this.refreshedAt.toISOString() : null,\n lastError: this.lastError ?? null,\n };\n }\n\n lookup(ip: string): IGeoipLookup {\n const result: IGeoipLookup = {\n ...EMPTY_LOOKUP,\n };\n\n if (this.cityReader) {\n try {\n const city = this.cityReader.get(ip);\n\n if (city) {\n result.continentCode = city.continent?.code ?? null;\n result.continentName = city.continent?.names?.en ?? null;\n result.countryCode = city.country?.iso_code ?? null;\n result.countryName = city.country?.names?.en ?? null;\n result.cityName = city.city?.names?.en ?? null;\n result.timezone = city.location?.time_zone ?? null;\n result.latitude = city.location?.latitude ?? null;\n result.longitude = city.location?.longitude ?? null;\n }\n } catch (err: any) {\n this.logger.debug(`City lookup failed for ${ip}: ${err.message}`);\n }\n }\n\n if (this.asnReader) {\n try {\n const [\n asn, prefixLength,\n ] = this.asnReader.getWithPrefixLength(ip);\n\n if (asn) {\n result.asnName = asn.autonomous_system_organization ?? null;\n result.asnNetwork = networkFromPrefix(\n ip,\n prefixLength\n );\n }\n } catch (err: any) {\n this.logger.debug(`ASN lookup failed for ${ip}: ${err.message}`);\n }\n }\n\n return result;\n }\n\n async refresh(): Promise<void> {\n this.logger.log(`Downloading GeoIP databases from ${this.config.provider} into ${this.config.dataDir}`);\n\n try {\n switch (this.config.provider) {\n case EGeoipProvider.Dbip: {\n await this.downloadDbip();\n break;\n }\n\n case EGeoipProvider.Maxmind: {\n await this.downloadMaxmind();\n break;\n }\n\n default: {\n return;\n }\n }\n\n await this.load();\n this.refreshedAt = new Date();\n this.lastError = void 0;\n this.logger.log('GeoIP databases updated');\n } catch (err: any) {\n this.lastError = err.message;\n this.logger.error(`GeoIP download failed: ${err.message}`);\n }\n }\n\n private async load(): Promise<boolean> {\n const [\n city, asn,\n ] = await Promise.all([\n this.openReader<CityResponse>(CITY_FILENAME), this.openReader<AsnResponse>(ASN_FILENAME),\n ]);\n\n if (city) {\n this.cityReader = city;\n }\n\n if (asn) {\n this.asnReader = asn;\n }\n\n if (city !== void 0 || asn !== void 0) {\n this.logger.log(`GeoIP databases loaded (city: ${!!city}, asn: ${!!asn})`);\n\n return true;\n }\n\n return false;\n }\n\n private async openReader<T extends MaxmindResponse>(filename: string): Promise<Reader<T> | undefined> {\n const path = `${this.config.dataDir}/${filename}`;\n\n try {\n await fs.access(path);\n } catch (err: any) {\n return void 0;\n }\n\n try {\n return await open<T>(path);\n } catch (err: any) {\n this.logger.warn(`Cannot open ${path}: ${err.message}`);\n\n return void 0;\n }\n }\n\n private async downloadDbip(): Promise<void> {\n // DB-IP publishes one file per month; early in a month the current one may not exist yet.\n const now = new Date();\n const months: string[] = [];\n\n for (let i = 0; i < 2; i++) {\n const d = new Date(Date.UTC(\n now.getUTCFullYear(),\n now.getUTCMonth() - i,\n 1\n ));\n months.push(`${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1)\n .padStart(\n 2,\n '0'\n )}`);\n }\n\n let lastError: Error | undefined;\n\n for (const month of months) {\n try {\n await downloadToFile(\n `https://download.db-ip.com/free/dbip-city-lite-${month}.mmdb.gz`,\n `${this.config.dataDir}/${CITY_FILENAME}`,\n {\n gunzip: true,\n timeoutMs: this.config.downloadTimeoutMs,\n }\n );\n await downloadToFile(\n `https://download.db-ip.com/free/dbip-asn-lite-${month}.mmdb.gz`,\n `${this.config.dataDir}/${ASN_FILENAME}`,\n {\n gunzip: true,\n timeoutMs: this.config.downloadTimeoutMs,\n }\n );\n\n return;\n } catch (err: any) {\n lastError = err;\n this.logger.warn(`DB-IP ${month} not available (${err.message}), trying the previous month`);\n }\n }\n\n throw lastError ?? new Error('DB-IP download failed');\n }\n\n private async downloadMaxmind(): Promise<void> {\n const editions: [string, string][] = [\n [\n 'GeoLite2-City', CITY_FILENAME,\n ],\n [\n 'GeoLite2-ASN', ASN_FILENAME,\n ],\n ];\n\n for (const [\n edition, filename,\n ] of editions) {\n const archive = `${this.config.dataDir}/${edition}.tar.gz`;\n await downloadToFile(\n `https://download.maxmind.com/app/geoip_download?edition_id=${edition}&license_key=${encodeURIComponent(this.config.maxmindLicenseKey as string)}&suffix=tar.gz`,\n archive,\n {\n gunzip: false,\n timeoutMs: this.config.downloadTimeoutMs,\n }\n );\n\n try {\n await extractFromTarGz(\n archive,\n `*/${edition}.mmdb`,\n `${this.config.dataDir}/${filename}`\n );\n } finally {\n await fs.rm(\n archive,\n {\n force: true,\n }\n );\n }\n }\n }\n}\n","import {\n Controller,\n Get,\n Header,\n Req,\n} from '@nestjs/common';\nimport { GeoipService } from './geoip/geoip.service';\nimport { normalizeIp } from './geoip/network';\nimport type { IGeoipStatus } from './geoip/geoip.service';\nimport type { IFingerprint } from '@proxyherd/common';\nimport type { Request } from 'express';\n\n\nfunction clientIp(req: Request): string {\n // Express applies the \"trust proxy\" setting to req.ip (X-Forwarded-For); the socket address otherwise.\n const ip = req.ip ?? req.socket.remoteAddress ?? '';\n\n return normalizeIp(ip);\n}\n\n\n@Controller()\nexport class FingerprintController {\n constructor(private readonly geoip: GeoipService) {}\n\n @Get()\n @Header(\n 'Content-Type',\n 'text/plain; charset=utf-8'\n )\n getRoot(): string {\n const status = this.geoip.status;\n const lines = [\n 'ProxyHerd fingerprint service',\n 'GET /api/json -> IP, network and location of the caller as JSON',\n 'GET /api/text -> IP of the caller as text',\n 'GET /health -> service status',\n ];\n\n if (status.attribution) {\n lines.push(status.attribution);\n }\n\n return `${lines.join('\\n')}\\n`;\n }\n\n @Get('health')\n getHealth(): IGeoipStatus & { status: string } {\n return {\n status: 'ok',\n ...this.geoip.status,\n };\n }\n\n @Get('api/json')\n getJson(@Req() req: Request): IFingerprint {\n const ip = clientIp(req);\n const useragent = req.headers[ 'user-agent' ];\n\n return {\n ip,\n useragent: typeof useragent === 'string' && useragent.length > 0 ? useragent : null,\n ...this.geoip.lookup(ip),\n };\n }\n\n @Get('api/text')\n @Header(\n 'Content-Type',\n 'text/plain; charset=utf-8'\n )\n getText(@Req() req: Request): string {\n return clientIp(req);\n }\n}\n","import { Module } from '@nestjs/common';\nimport { FINGERPRINT_SERVICE_CONFIG } from './config';\nimport { FingerprintController } from './fingerprint.controller';\nimport { GeoipService } from './geoip/geoip.service';\nimport type { IFingerprintServiceConfig } from './config';\nimport type { DynamicModule } from '@nestjs/common';\n\n\n@Module({})\nexport class AppModule {\n static forRoot(config: IFingerprintServiceConfig): DynamicModule {\n return {\n module: AppModule,\n controllers: [\n FingerprintController,\n ],\n providers: [\n {\n provide: FINGERPRINT_SERVICE_CONFIG,\n useValue: config,\n },\n GeoipService,\n ],\n };\n }\n}\n","import 'reflect-metadata';\nimport { Logger } from '@nestjs/common';\nimport { NestFactory } from '@nestjs/core';\nimport { AppModule } from './app.module';\nimport { getEnvConfig } from './config';\nimport type { NestExpressApplication } from '@nestjs/platform-express';\n\n\n(async() => {\n const config = getEnvConfig();\n const app = await NestFactory.create<NestExpressApplication>(\n AppModule.forRoot(config),\n {\n logger: [\n 'log', 'warn', 'error',\n ],\n }\n );\n app.enableShutdownHooks();\n\n if (config.trustProxy) {\n app.set(\n 'trust proxy',\n true\n );\n }\n\n await app.listen(config.port);\n\n new Logger('Fingerprint')\n .log(`ProxyHerd fingerprint service listening on port ${config.port} (GeoIP provider: ${config.provider})`);\n})()\n .catch((err: any) => {\n console.error(err.message ?? err);\n process.exit(1);\n });\n"],"names":["__webpack_require__","exports","Symbol","toStringTag","Object","defineProperty","value","require","EGeoipProvider","FINGERPRINT_SERVICE_CONFIG","parseBoolean","defaultValue","undefined","length","includes","toLowerCase","getEnvConfig","provider","Maxmind","None","Dbip","parseProvider","process","env","GEOIP_PROVIDER","maxmindLicenseKey","MAXMIND_LICENSE_KEY","Error","port","parseInt","FINGERPRINT_PORT","dataDir","GEOIP_DIR","autoDownload","GEOIP_AUTO_DOWNLOAD","refreshIntervalMs","GEOIP_REFRESH_HOURS","trustProxy","TRUST_PROXY","downloadTimeoutMs","GEOIP_DOWNLOAD_TIMEOUT","MAX_REDIRECTS","get","url","timeoutMs","Promise","resolve","reject","parsed","URL","req","protocol","request","method","headers","timeout","res","on","destroy","end","async","downloadToFile","destination","options","mkdir","dirname","recursive","temporary","current","i","status","statusCode","location","resume","toString","getFollowingRedirects","gunzip","pipeline","createGunzip","createWriteStream","rename","err","rm","force","findFile","directory","filename","entries","readdir","withFileTypes","entry","path","join","name","isDirectory","nested","extractFromTarGz","archive","pattern","scratch","pid","execFile","stdout","stderr","message","found","split","pop","IPV4_MAPPED_PREFIX","normalizeIp","ip","lower","startsWith","candidate","substring","isIPv4","networkFromPrefix","prefixLength","normalized","octets","map","o","network","isIPv6","head","tail","headParts","tailParts","missing","parts","Array","Math","max","fill","BigInt","part","expandIpv6","all","groups","push","Number","bestLength","bestStart","start","hex","g","slice","compressIpv6","EMPTY_LOOKUP","asnName","asnNetwork","continentCode","continentName","countryCode","countryName","cityName","timezone","latitude","longitude","CITY_FILENAME","ASN_FILENAME","GeoipService","config","logger","Logger","cityReader","asnReader","refreshedAt","lastError","timer","constructor","onModuleInit","this","log","loaded","load","refresh","warn","setInterval","catch","unref","onModuleDestroy","clearInterval","attribution","city","asn","toISOString","lookup","result","continent","code","names","en","country","iso_code","time_zone","debug","getWithPrefixLength","autonomous_system_organization","downloadDbip","downloadMaxmind","Date","error","openReader","access","open","now","months","d","UTC","getUTCFullYear","getUTCMonth","String","padStart","month","editions","edition","encodeURIComponent","Injectable","Inject","clientIp","socket","remoteAddress","FingerprintController","geoip","getRoot","lines","getHealth","getJson","useragent","getText","Get","Header","Req","Controller","AppModule","forRoot","module","controllers","providers","provide","useValue","Module","app","NestFactory","create","enableShutdownHooks","set","listen","console","exit"],"sourceRoot":""}
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@proxyherd/fingerprint",
3
+ "description": "ProxyHerd fingerprint service: tells a proxy manager whether a proxy is online and where it is (IP, network, location) from a local GeoIP database. Self-hosted; nothing leaves your infrastructure.",
4
+ "author": {
5
+ "name": "ProxyHerd maintainers",
6
+ "url": "https://github.com/proxyherd"
7
+ },
8
+ "url": "https://github.com/proxyherd/proxyherd",
9
+ "homepage": "https://proxyherd.github.io/proxyherd/",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/proxyherd/proxyherd.git"
13
+ },
14
+ "license": "MIT",
15
+ "scripts": {},
16
+ "dependencies": {
17
+ "@nestjs/common": "~10.2.0",
18
+ "@nestjs/core": "~10.2.0",
19
+ "@nestjs/platform-express": "~10.2.0",
20
+ "maxmind": "~5.0.0",
21
+ "reflect-metadata": "~0.1.0",
22
+ "rxjs": "~7.8.0",
23
+ "tslib": "^2.3.0"
24
+ },
25
+ "engines": {
26
+ "node": ">= 20",
27
+ "npm": ">= 8"
28
+ },
29
+ "bin": {
30
+ "proxyherd-fingerprint": "fingerprint.js"
31
+ },
32
+ "main": "fingerprint.js",
33
+ "version": "1.0.0"
34
+ }