Skip to content

Commit b52bc35

Browse files
committed
wip: lookup
1 parent e69c305 commit b52bc35

File tree

18 files changed

+610
-76
lines changed

18 files changed

+610
-76
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ node_modules/
88
.npm
99
.eslintcache
1010
*.tgz
11-
.env
11+
.env
12+
tmp
13+
data

packages/ip-location-api/package.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,23 @@
4040
"scripts": {
4141
"build": "vite build",
4242
"build:watch": "vite build --watch"
43+
},
44+
"peerDependencies": {
45+
"countries-list": "^3.1.1"
46+
},
47+
"peerDependenciesMeta": {
48+
"countries-list": {
49+
"optional": true
50+
}
51+
},
52+
"dependencies": {
53+
"@fast-csv/parse": "^5.0.0",
54+
"ip-address": "^9.0.5",
55+
"ky": "^1.7.2",
56+
"yauzl": "^3.1.3"
57+
},
58+
"devDependencies": {
59+
"@iplookup/util": "workspace:*",
60+
"countries-list": "^3.1.1"
4361
}
4462
}
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import type { ICountry } from 'countries-list'
2+
import { Buffer } from 'node:buffer'
3+
import { open } from 'node:fs/promises'
4+
import { join } from 'node:path'
5+
import { binarySearch, getSmallMemoryFile, type IpLocationApiSettings, type LocalDatabase, number37ToString, parseIp, SAVED_SETTINGS } from '@iplookup/util'
6+
import { LOADED_DATA } from './reload.js'
7+
8+
interface GeoData {
9+
latitude?: number
10+
longitude?: number
11+
postcode?: string
12+
area?: number
13+
country?: string
14+
eu?: boolean
15+
region1?: string
16+
region1_name?: string
17+
region2?: string
18+
region2_name?: string
19+
metro?: number
20+
timezone?: string
21+
city?: string
22+
country_name?: string
23+
country_native?: string
24+
continent?: string
25+
continent_name?: string
26+
capital?: string
27+
phone?: number[]
28+
currency?: string[]
29+
languages?: string[]
30+
}
31+
32+
export async function lookup(ip: string): Promise<GeoData | null> {
33+
//* We don't use net.isIP(ip) as it is slow for ipv6
34+
const { version, ip: ipNumber } = parseIp(ip)
35+
36+
const settings = SAVED_SETTINGS
37+
const db = version === 4 ? settings.v4 : settings.v6
38+
39+
if (!db.loadedData)
40+
return null
41+
if (!(ipNumber >= db.loadedData.firstIp))
42+
return null
43+
const list = db.loadedData.startIps
44+
const line = binarySearch(list, ipNumber)
45+
if (line === null)
46+
return null
47+
48+
if (settings.smallMemory) {
49+
const buffer = await lineToFile(line, db, settings)
50+
const endIp = version === 4 ? buffer.readUInt32LE(0) : buffer.readBigUInt64LE(0)
51+
if (ipNumber > endIp)
52+
return null
53+
54+
if (settings.dataType === 'Country') {
55+
return setCountryInfo({
56+
country: buffer.toString('latin1', version === 4 ? 4 : 8, version === 4 ? 6 : 10),
57+
}, settings)
58+
}
59+
return setCityInfo(buffer, version === 4 ? 4 : 8, settings)
60+
}
61+
62+
const endIps = db.loadedData.endIps
63+
if (!endIps || ipNumber > endIps[line]!)
64+
return null
65+
66+
if (settings.dataType === 'Country') {
67+
return setCountryInfo({
68+
country: db.loadedData.mainBuffer!.toString('latin1', line * db.recordSize, line * db.recordSize + 2),
69+
}, settings)
70+
}
71+
return setCityInfo(db.loadedData.mainBuffer!, line * db.recordSize, settings)
72+
}
73+
74+
async function lineToFile(line: number, db: LocalDatabase, settings: IpLocationApiSettings): Promise<Buffer> {
75+
const [dir, file, offset] = getSmallMemoryFile(line, db)
76+
const fd = await open(join(settings.fieldDir, dir, file), 'r')
77+
const buffer = Buffer.alloc(db.recordSize)
78+
await fd.read(buffer, 0, db.recordSize, offset)
79+
fd.close().catch(() => {
80+
// TODO console.warn
81+
})
82+
return buffer
83+
}
84+
85+
function setCityInfo(buffer: Buffer, offset: number, settings: IpLocationApiSettings): Promise<GeoData> {
86+
let locationId: number | undefined
87+
const geodata: GeoData = {}
88+
if (settings.locationFile) {
89+
locationId = buffer.readUInt32LE(offset)
90+
offset += 4
91+
}
92+
if (settings.fields.includes('latitude')) {
93+
geodata.latitude = buffer.readInt32LE(offset) / 10000
94+
offset += 4
95+
}
96+
if (settings.fields.includes('longitude')) {
97+
geodata.longitude = buffer.readInt32LE(offset) / 10000
98+
offset += 4
99+
}
100+
if (settings.fields.includes('postcode')) {
101+
const postcodeLength = buffer.readUInt32LE(offset)
102+
const postcodeValue = buffer.readInt8(offset + 4)
103+
if (postcodeLength) {
104+
let postcode: string
105+
if (postcodeValue < -9) {
106+
const code = (-postcodeValue).toString()
107+
postcode = postcodeLength.toString(36)
108+
postcode = `${getZeroFill(
109+
postcode.slice(0, -Number.parseInt(code[1]!)),
110+
Number.parseInt(code[0]!) - 0,
111+
)}-${getZeroFill(postcode.slice(-Number.parseInt(code[1]!)), Number.parseInt(code[1]!) - 0)}`
112+
}
113+
else if (postcodeValue < 0) {
114+
postcode = getZeroFill(postcodeLength.toString(36), -postcodeValue)
115+
}
116+
else if (postcodeValue < 10) {
117+
postcode = getZeroFill(postcodeLength.toString(10), postcodeValue)
118+
}
119+
else if (postcodeValue < 72) {
120+
const code = String(postcodeValue)
121+
postcode = getZeroFill(postcodeLength.toString(10), (Number.parseInt(code[0]!) - 0) + (Number.parseInt(code[1]!) - 0))
122+
postcode = `${postcode.slice(0, Number.parseInt(code[0]!) - 0)}-${postcode.slice(Number.parseInt(code[0]!) - 0)}`
123+
}
124+
else {
125+
postcode = postcodeValue.toString(36).slice(1) + postcodeLength.toString(36)
126+
}
127+
geodata.postcode = postcode.toUpperCase()
128+
}
129+
offset += 5
130+
}
131+
if (settings.fields.includes('area')) {
132+
const areaMap = LOADED_DATA.sub?.area
133+
if (areaMap) {
134+
geodata.area = areaMap[buffer.readUInt8(offset)]
135+
}
136+
// offset += 1
137+
}
138+
139+
if (locationId) {
140+
let locationOffset = (locationId - 1) * settings.locationRecordSize
141+
const locationBuffer = LOADED_DATA.location
142+
if (locationBuffer) {
143+
if (settings.fields.includes('country')) {
144+
geodata.country = locationBuffer.toString('utf8', locationOffset, locationOffset += 2)
145+
const euMap = LOADED_DATA.sub?.eu
146+
if (settings.fields.includes('eu') && euMap) {
147+
geodata.eu = euMap[geodata.country]
148+
}
149+
}
150+
if (settings.fields.includes('region1')) {
151+
const region1 = locationBuffer.readUInt16LE(locationOffset)
152+
locationOffset += 2
153+
if (region1 > 0) {
154+
geodata.region1 = number37ToString(region1)
155+
}
156+
}
157+
if (settings.fields.includes('region1_name')) {
158+
const region1Name = locationBuffer.readUInt16LE(locationOffset)
159+
locationOffset += 2
160+
const region1Map = LOADED_DATA.sub?.region1
161+
if (region1Name > 0 && region1Map) {
162+
geodata.region1_name = region1Map[region1Name]
163+
}
164+
}
165+
if (settings.fields.includes('region2')) {
166+
const region2 = locationBuffer.readUInt16LE(locationOffset)
167+
locationOffset += 2
168+
if (region2 > 0) {
169+
geodata.region2 = number37ToString(region2)
170+
}
171+
}
172+
if (settings.fields.includes('region2_name')) {
173+
const region2Name = locationBuffer.readUInt16LE(locationOffset)
174+
locationOffset += 2
175+
const region2Map = LOADED_DATA.sub?.region2
176+
if (region2Name > 0 && region2Map) {
177+
geodata.region2_name = region2Map[region2Name]
178+
}
179+
}
180+
if (settings.fields.includes('metro')) {
181+
const metro = locationBuffer.readUInt16LE(locationOffset)
182+
locationOffset += 2
183+
if (metro > 0) {
184+
geodata.metro = metro
185+
}
186+
}
187+
if (settings.fields.includes('timezone')) {
188+
const timezone = locationBuffer.readUInt16LE(locationOffset)
189+
locationOffset += 2
190+
const timezoneMap = LOADED_DATA.sub?.timezone
191+
if (timezone > 0 && timezoneMap) {
192+
geodata.timezone = timezoneMap[timezone]
193+
}
194+
}
195+
if (settings.fields.includes('city')) {
196+
const city = locationBuffer.readUInt32LE(locationOffset)
197+
// locationOffset += 4
198+
const cityMap = LOADED_DATA.city
199+
if (city > 0 && cityMap) {
200+
const start = city >>> 8
201+
geodata.city = cityMap.toString('utf8', start, start + (city & 255))
202+
}
203+
}
204+
}
205+
}
206+
207+
return setCountryInfo(geodata, settings)
208+
}
209+
210+
function getZeroFill(text: string, length: number) {
211+
return '0'.repeat(length - text.length) + text
212+
}
213+
214+
async function setCountryInfo(geodata: GeoData, settings: IpLocationApiSettings): Promise<GeoData> {
215+
if (settings.addCountryInfo && geodata.country) {
216+
//* Import the countries-list package (optional peer dependency)
217+
try {
218+
const { countries, continents } = await import('countries-list')
219+
const country = countries[geodata.country as keyof typeof countries] as ICountry | undefined
220+
geodata.country_name = country?.name
221+
geodata.country_native = country?.native
222+
geodata.continent = country?.continent ? continents[country.continent] : undefined
223+
geodata.capital = country?.capital
224+
geodata.phone = country?.phone
225+
geodata.currency = country?.currency
226+
geodata.languages = country?.languages
227+
}
228+
catch (error) {
229+
// TODO add correct debug message
230+
console.error('Error importing countries-list', error)
231+
}
232+
}
233+
return geodata
234+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import type { IpLocationApiInputSettings } from '@iplookup/util'
2+
import type { Buffer } from 'node:buffer'
3+
import { existsSync } from 'node:fs'
4+
import { readFile } from 'node:fs/promises'
5+
import { join } from 'node:path'
6+
import { getSettings, SAVED_SETTINGS } from '@iplookup/util'
7+
import { update } from '@iplookup/util/db'
8+
9+
export const LOADED_DATA: {
10+
location?: Buffer
11+
city?: Buffer
12+
sub?: {
13+
region1?: string[]
14+
region2?: string[]
15+
timezone?: string[]
16+
area?: number[]
17+
eu?: Record<string, boolean>
18+
}
19+
} = {}
20+
21+
export async function reload(inputSettings?: IpLocationApiInputSettings): Promise<void> {
22+
const settings = getSettings(inputSettings)
23+
24+
let directoryToFind = settings.dataDir
25+
if (settings.smallMemory) {
26+
directoryToFind = join(settings.dataDir, 'v4')
27+
}
28+
29+
if (!existsSync(directoryToFind)) {
30+
await update(settings)
31+
}
32+
33+
const buffers = {
34+
v4: {
35+
dat1: undefined as Buffer | undefined,
36+
dat2: undefined as Buffer | undefined,
37+
dat3: undefined as Buffer | undefined,
38+
},
39+
v6: {
40+
dat1: undefined as Buffer | undefined,
41+
dat2: undefined as Buffer | undefined,
42+
dat3: undefined as Buffer | undefined,
43+
},
44+
location: undefined as Buffer | undefined,
45+
city: undefined as Buffer | undefined,
46+
sub: undefined as Buffer | undefined,
47+
}
48+
const promises: Promise<void>[] = [
49+
readFile(join(settings.fieldDir, '4-1.dat')).then((buffer) => { buffers.v4.dat1 = buffer }),
50+
readFile(join(settings.fieldDir, '6-1.dat')).then((buffer) => { buffers.v6.dat1 = buffer }),
51+
]
52+
53+
if (!settings.smallMemory) {
54+
promises.push(
55+
readFile(join(settings.fieldDir, '4-2.dat')).then((buffer) => { buffers.v4.dat2 = buffer }),
56+
readFile(join(settings.fieldDir, '4-3.dat')).then((buffer) => { buffers.v4.dat3 = buffer }),
57+
readFile(join(settings.fieldDir, '6-2.dat')).then((buffer) => { buffers.v6.dat2 = buffer }),
58+
readFile(join(settings.fieldDir, '6-3.dat')).then((buffer) => { buffers.v6.dat3 = buffer }),
59+
)
60+
}
61+
62+
if (settings.locationFile) {
63+
promises.push(
64+
readFile(join(settings.fieldDir, 'location.dat')).then((buffer) => { buffers.location = buffer }),
65+
)
66+
67+
if (settings.fields.includes('city')) {
68+
promises.push(
69+
readFile(join(settings.fieldDir, 'name.dat')).then((buffer) => { buffers.city = buffer }),
70+
)
71+
}
72+
73+
if (settings.fields.some(field => ['region1_name', 'region2_name', 'timezone', 'area', 'eu'].includes(field))) {
74+
promises.push(
75+
readFile(join(settings.fieldDir, 'sub.json')).then((buffer) => { buffers.sub = buffer }),
76+
)
77+
}
78+
}
79+
80+
await Promise.all(promises)
81+
82+
const v4 = settings.v4
83+
const v6 = settings.v6
84+
85+
const v4StartIps = new Uint32Array(buffers.v4.dat1!.buffer, 0, buffers.v4.dat1!.byteLength >> 2)
86+
const v6StartIps = new BigUint64Array(buffers.v6.dat1!.buffer, 0, buffers.v6.dat1!.byteLength >> 3)
87+
88+
v4.loadedData = {
89+
startIps: v4StartIps,
90+
endIps: buffers.v4.dat2 ? new Uint32Array(buffers.v4.dat2.buffer, 0, buffers.v4.dat2.byteLength >> 2) : undefined,
91+
mainBuffer: buffers.v4.dat3,
92+
lastLine: v4StartIps.length - 1,
93+
firstIp: v4StartIps[0]!,
94+
}
95+
96+
v6.loadedData = {
97+
startIps: v6StartIps,
98+
endIps: buffers.v6.dat2 ? new BigUint64Array(buffers.v6.dat2.buffer, 0, buffers.v6.dat2.byteLength >> 3) : undefined,
99+
mainBuffer: buffers.v6.dat3,
100+
lastLine: v6StartIps.length - 1,
101+
firstIp: v6StartIps[0]!,
102+
}
103+
104+
if (settings.dataType === 'City') {
105+
LOADED_DATA.location = buffers.location
106+
LOADED_DATA.city = buffers.city
107+
if (buffers.sub) {
108+
const subJson = JSON.parse(buffers.sub.toString())
109+
LOADED_DATA.sub = {
110+
region1: subJson.region1_name,
111+
region2: subJson.region2_name,
112+
timezone: subJson.timezone,
113+
area: subJson.area,
114+
eu: subJson.eu,
115+
}
116+
}
117+
}
118+
}
119+
120+
export function clear() {
121+
const settings = SAVED_SETTINGS
122+
settings.v4.loadedData = undefined
123+
settings.v6.loadedData = undefined
124+
125+
LOADED_DATA.location = undefined
126+
LOADED_DATA.city = undefined
127+
LOADED_DATA.sub = undefined
128+
}

0 commit comments

Comments
 (0)