191 lines
4.8 KiB
TypeScript
191 lines
4.8 KiB
TypeScript
|
|
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<CityResponse>;
|
||
|
|
|
||
|
|
static async create( path:string, accountID:string, licenseKey:string, url = null ):Promise<LocationService>
|
||
|
|
{
|
||
|
|
|
||
|
|
|
||
|
|
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<void>
|
||
|
|
{
|
||
|
|
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<void>(
|
||
|
|
|
||
|
|
( 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<LocationData>
|
||
|
|
{
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|