ニジカ投稿局 https://tv.nizika.tv
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

148 lines
4.4 KiB

  1. import { CONFIG } from '@server/initializers/config.js'
  2. import { pathExists } from 'fs-extra/esm'
  3. import { writeFile } from 'fs/promises'
  4. import throttle from 'lodash-es/throttle.js'
  5. import maxmind, { CityResponse, CountryResponse, Reader } from 'maxmind'
  6. import { join } from 'path'
  7. import { isArray } from './custom-validators/misc.js'
  8. import { logger, loggerTagsFactory } from './logger.js'
  9. import { isBinaryResponse, peertubeGot } from './requests.js'
  10. const lTags = loggerTagsFactory('geo-ip')
  11. export class GeoIP {
  12. private static instance: GeoIP
  13. private countryReader: Reader<CountryResponse>
  14. private cityReader: Reader<CityResponse>
  15. private readonly INIT_READERS_RETRY_INTERVAL = 1000 * 60 * 10 // 10 minutes
  16. private readonly countryDBPath = join(CONFIG.STORAGE.BIN_DIR, 'dbip-country-lite-latest.mmdb')
  17. private readonly cityDBPath = join(CONFIG.STORAGE.BIN_DIR, 'dbip-city-lite-latest.mmdb')
  18. private constructor () {
  19. }
  20. async safeIPISOLookup (ip: string): Promise<{ country: string, subdivisionName: string }> {
  21. const emptyResult = { country: null, subdivisionName: null }
  22. if (CONFIG.GEO_IP.ENABLED === false) return emptyResult
  23. try {
  24. await this.initReadersIfNeededThrottle()
  25. const countryResult = this.countryReader?.get(ip)
  26. const cityResult = this.cityReader?.get(ip)
  27. return {
  28. country: this.getISOCountry(countryResult),
  29. subdivisionName: this.getISOSubdivision(cityResult)
  30. }
  31. } catch (err) {
  32. logger.error('Cannot get country/city information from IP.', { err })
  33. return emptyResult
  34. }
  35. }
  36. // ---------------------------------------------------------------------------
  37. private getISOCountry (countryResult: CountryResponse) {
  38. return countryResult?.country?.iso_code || null
  39. }
  40. private getISOSubdivision (subdivisionResult: CityResponse) {
  41. const subdivisions = subdivisionResult?.subdivisions
  42. if (!isArray(subdivisions) || subdivisions.length === 0) return null
  43. // The last subdivision is the more precise one
  44. const subdivision = subdivisions[subdivisions.length - 1]
  45. return subdivision.names?.en || null
  46. }
  47. // ---------------------------------------------------------------------------
  48. async updateDatabases () {
  49. if (CONFIG.GEO_IP.ENABLED === false) return
  50. await this.updateCountryDatabase()
  51. await this.updateCityDatabase()
  52. }
  53. private async updateCountryDatabase () {
  54. if (!CONFIG.GEO_IP.COUNTRY.DATABASE_URL) return false
  55. await this.updateDatabaseFile(CONFIG.GEO_IP.COUNTRY.DATABASE_URL, this.countryDBPath)
  56. this.countryReader = undefined
  57. return true
  58. }
  59. private async updateCityDatabase () {
  60. if (!CONFIG.GEO_IP.CITY.DATABASE_URL) return false
  61. await this.updateDatabaseFile(CONFIG.GEO_IP.CITY.DATABASE_URL, this.cityDBPath)
  62. this.cityReader = undefined
  63. return true
  64. }
  65. private async updateDatabaseFile (url: string, destination: string) {
  66. logger.info('Updating GeoIP databases from %s.', url, lTags())
  67. const gotOptions = { context: { bodyKBLimit: 800_000 }, responseType: 'buffer' as 'buffer' }
  68. try {
  69. const gotResult = await peertubeGot(url, gotOptions)
  70. if (!isBinaryResponse(gotResult)) {
  71. throw new Error('Not a binary response')
  72. }
  73. await writeFile(destination, gotResult.body)
  74. logger.info('GeoIP database updated %s.', destination, lTags())
  75. } catch (err) {
  76. logger.error('Cannot update GeoIP database from %s.', url, { err, ...lTags() })
  77. }
  78. }
  79. // ---------------------------------------------------------------------------
  80. private async initReadersIfNeeded () {
  81. if (!this.countryReader) {
  82. let open = true
  83. if (!await pathExists(this.countryDBPath)) {
  84. open = await this.updateCountryDatabase()
  85. }
  86. if (open) {
  87. this.countryReader = await maxmind.open(this.countryDBPath)
  88. }
  89. }
  90. if (!this.cityReader) {
  91. let open = true
  92. if (!await pathExists(this.cityDBPath)) {
  93. open = await this.updateCityDatabase()
  94. }
  95. if (open) {
  96. this.cityReader = await maxmind.open(this.cityDBPath)
  97. }
  98. }
  99. }
  100. private readonly initReadersIfNeededThrottle = throttle(this.initReadersIfNeeded.bind(this), this.INIT_READERS_RETRY_INTERVAL)
  101. // ---------------------------------------------------------------------------
  102. static get Instance () {
  103. return this.instance || (this.instance = new this())
  104. }
  105. }