Init commit
This commit is contained in:
155
server/core/helpers/geo-ip.ts
Normal file
155
server/core/helpers/geo-ip.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { pathExists } from 'fs-extra/esm'
|
||||
import { writeFile } from 'fs/promises'
|
||||
import maxmind, { CityResponse, CountryResponse, Reader } from 'maxmind'
|
||||
import { join } from 'path'
|
||||
import { isArray } from './custom-validators/misc.js'
|
||||
import { logger, loggerTagsFactory } from './logger.js'
|
||||
import { isBinaryResponse, unsafeSSRFGot } from './requests.js'
|
||||
|
||||
const lTags = loggerTagsFactory('geo-ip')
|
||||
|
||||
export class GeoIP {
|
||||
private static instance: GeoIP
|
||||
|
||||
private countryReader: Reader<CountryResponse>
|
||||
private cityReader: Reader<CityResponse>
|
||||
|
||||
private lastInitTry: Date
|
||||
private initReadersPromise: Promise<any>
|
||||
|
||||
private readonly INIT_READERS_RETRY_INTERVAL = 1000 * 60 * 10 // 10 minutes
|
||||
private readonly countryDBPath = join(CONFIG.STORAGE.BIN_DIR, 'dbip-country-lite-latest.mmdb')
|
||||
private readonly cityDBPath = join(CONFIG.STORAGE.BIN_DIR, 'dbip-city-lite-latest.mmdb')
|
||||
|
||||
private constructor () {
|
||||
}
|
||||
|
||||
async safeIPISOLookup (ip: string): Promise<{ country: string, subdivisionName: string }> {
|
||||
const emptyResult = { country: null, subdivisionName: null }
|
||||
if (CONFIG.GEO_IP.ENABLED === false) return emptyResult
|
||||
|
||||
try {
|
||||
if (this.initReadersPromise === undefined) {
|
||||
this.initReadersPromise = this.initReadersIfNeeded()
|
||||
}
|
||||
|
||||
await this.initReadersPromise
|
||||
this.initReadersPromise = undefined
|
||||
|
||||
const countryResult = this.countryReader?.get(ip)
|
||||
const cityResult = this.cityReader?.get(ip)
|
||||
|
||||
return {
|
||||
country: this.getISOCountry(countryResult),
|
||||
subdivisionName: this.getISOSubdivision(cityResult)
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Cannot get country/city information from IP.', { err })
|
||||
|
||||
return emptyResult
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private getISOCountry (countryResult: CountryResponse) {
|
||||
return countryResult?.country?.iso_code || null
|
||||
}
|
||||
|
||||
private getISOSubdivision (subdivisionResult: CityResponse) {
|
||||
const subdivisions = subdivisionResult?.subdivisions
|
||||
if (!isArray(subdivisions) || subdivisions.length === 0) return null
|
||||
|
||||
// The last subdivision is the more precise one
|
||||
const subdivision = subdivisions[subdivisions.length - 1]
|
||||
|
||||
return subdivision.names?.en || null
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async updateDatabases () {
|
||||
if (CONFIG.GEO_IP.ENABLED === false) return
|
||||
|
||||
await this.updateCountryDatabase()
|
||||
await this.updateCityDatabase()
|
||||
}
|
||||
|
||||
private async updateCountryDatabase () {
|
||||
if (!CONFIG.GEO_IP.COUNTRY.DATABASE_URL) return false
|
||||
|
||||
await this.updateDatabaseFile(CONFIG.GEO_IP.COUNTRY.DATABASE_URL, this.countryDBPath)
|
||||
|
||||
this.countryReader = undefined
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private async updateCityDatabase () {
|
||||
if (!CONFIG.GEO_IP.CITY.DATABASE_URL) return false
|
||||
|
||||
await this.updateDatabaseFile(CONFIG.GEO_IP.CITY.DATABASE_URL, this.cityDBPath)
|
||||
|
||||
this.cityReader = undefined
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private async updateDatabaseFile (url: string, destination: string) {
|
||||
logger.info('Updating GeoIP databases from %s.', url, lTags())
|
||||
|
||||
const gotOptions = { context: { bodyKBLimit: 800_000 }, responseType: 'buffer' as 'buffer' }
|
||||
|
||||
try {
|
||||
const gotResult = await unsafeSSRFGot(url, gotOptions)
|
||||
|
||||
if (!isBinaryResponse(gotResult)) {
|
||||
throw new Error('Not a binary response')
|
||||
}
|
||||
|
||||
await writeFile(destination, gotResult.body)
|
||||
|
||||
logger.info('GeoIP database updated %s.', destination, lTags())
|
||||
} catch (err) {
|
||||
logger.error('Cannot update GeoIP database from %s.', url, { err, ...lTags() })
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private async initReadersIfNeeded () {
|
||||
if (this.lastInitTry && this.lastInitTry.getTime() > Date.now() - this.INIT_READERS_RETRY_INTERVAL) return
|
||||
this.lastInitTry = new Date()
|
||||
|
||||
if (!this.countryReader) {
|
||||
let open = true
|
||||
|
||||
if (!await pathExists(this.countryDBPath)) {
|
||||
open = await this.updateCountryDatabase()
|
||||
}
|
||||
|
||||
if (open) {
|
||||
this.countryReader = await maxmind.open(this.countryDBPath)
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.cityReader) {
|
||||
let open = true
|
||||
|
||||
if (!await pathExists(this.cityDBPath)) {
|
||||
open = await this.updateCityDatabase()
|
||||
}
|
||||
|
||||
if (open) {
|
||||
this.cityReader = await maxmind.open(this.cityDBPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static get Instance () {
|
||||
return this.instance || (this.instance = new this())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user