import { Files } from "../../files/Files"; import { LocationData } from "./LocationData"; import * as https from "https"; import * as zlib from "zlib"; import * as tar from "tar"; import maxmind, { CityResponse, Reader, Response } from "maxmind"; import { RJLog } from "../../log/RJLog"; export class LocationService { _dbPath:string; _accountID:string; _licenseKey:string; _url:string = "https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz"; _lookUpDB:Reader; static async create( path:string, accountID:string, licenseKey:string, url = null ):Promise { let ipToL = new LocationService(); ipToL._dbPath = path; ipToL._accountID = accountID; ipToL._licenseKey = licenseKey; if ( url ) { ipToL._url = url; } RJLog.log( "Starting Location Service", ipToL._dbPath, ipToL._accountID, ipToL._licenseKey, ipToL._url ); await ipToL.update(); if ( ipToL._lookUpDB == null ) { await ipToL._loadDB(); } return ipToL; } async update( forceUpdate:boolean = false ):Promise { let needsUpdate = forceUpdate || ( await this._needsUpdate() ); if ( ! needsUpdate ) { return Promise.resolve(); } try { await this._downloadDB(); await this._loadDB(); } catch( e ) { RJLog.error( "Updating failed:", e ); } return Promise.resolve(); } async _loadDB() { this._lookUpDB = await maxmind.open( this._dbPath ); } async _needsUpdate( maxAgeDays = 7 ) { try { let stats = await Files.getStatistics( this._dbPath ); let ageDays = ( Date.now() - stats.mtimeMs ) / (1000 * 60 * 60 * 24); return Promise.resolve( ageDays > maxAgeDays ); } catch ( error ) { return Promise.resolve( true ); } } async _downloadDB() { RJLog.log( "Downloading DB" ); await Files.ensureParentDirectoryExists( this._dbPath ); let authenticification = `${this._accountID}:${this._licenseKey}`; let promise = new Promise( ( resolve, reject ) => { let sendRequest = ( url:string, writeAuthHeader:boolean ) => { let options:any = { headers: { "Accept": "application/gzip" } }; if ( writeAuthHeader ) { let authHeader = "Basic " + Buffer.from( authenticification ).toString( "base64" ); options = { headers: { "Authorization": authHeader, "Accept": "application/gzip" } }; } RJLog.log( "Downloading at ", url, options ); https.get( url, options, ( result ) => { if ( result.statusCode === 302 || result.statusCode == 301 ) { let redirectUrl = result.headers.location; console.log(`Redirecting to ${redirectUrl}`); result.resume(); return sendRequest( redirectUrl, false ); } if ( result.statusCode !== 200 ) { RJLog.log( `Location db download failed: ${result.statusCode} ${result.statusMessage}` ); reject( new Error( `Location db download failed: ${result.statusCode} ${result.statusMessage}` ) ); return; } // pipe through gunzip then tar extractor, writing .mmdb to DB_DIR let gunzip = zlib.createGunzip(); let extractor = tar.x( { cwd: Files.parentPath( this._dbPath ), filter: ( tarPath ) => tarPath.endsWith( ".mmdb" ), strip: 1 } ); result.pipe( gunzip ).pipe( extractor ); extractor.on( "finish", () => resolve()); extractor.on( "error", ( error ) => reject( error ) ); result.on( "error", ( error ) => reject( error ) ); } ).on("error", (error) => reject(error)); } sendRequest( this._url, true ); } ); return promise; } async getLocation( ip:string ):Promise { let locationData = new LocationData(); locationData.country = "Unknown"; locationData.city = "Unknown"; locationData.km_range = 20000; if ( this._lookUpDB !== null ) { let geo = this._lookUpDB.get( ip ); locationData.country = geo?.country?.names?.en ?? "Unknown"; locationData.city = geo?.city?.names?.en ?? "Unknown"; locationData.km_range = geo?.location?.accuracy_radius ?? 20000; } return locationData; } }