ip-asn-map 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/test.js ADDED
@@ -0,0 +1,91 @@
1
+ import { stat, writeFile } from 'node:fs/promises';
2
+ import { loadIpMap, parseIp2AsnGzip } from "./buffer.js";
3
+ import * as assert from 'node:assert';
4
+ import * as process from 'node:process';
5
+ const gz = `data/ip2asn-combined.tsv.gz`;
6
+ const bin = `data/ip-map.bin`;
7
+ if (!await stat(gz).catch(() => null)) {
8
+ console.log('Fetching', gz);
9
+ const res = await fetch('https://iptoasn.com/data/ip2asn-combined.tsv.gz');
10
+ if (!res.ok)
11
+ throw new Error(res.statusText);
12
+ await writeFile(gz, Buffer.from(await res.arrayBuffer()));
13
+ }
14
+ const { createInterface } = await import('node:readline');
15
+ const { createReadStream } = await import('node:fs');
16
+ const { createGunzip } = await import('node:zlib');
17
+ const stream = createReadStream(gz);
18
+ const gunzip = createGunzip();
19
+ stream.pipe(gunzip);
20
+ const rl = createInterface(gunzip);
21
+ const tsv = await Array.fromAsync(rl).then(arr => arr.filter(s => s.trim()));
22
+ let t = performance.now();
23
+ if (!await stat(bin).catch(() => null)) {
24
+ const buf = await parseIp2AsnGzip(gz);
25
+ console.log('parse:', performance.now() - t, 'ms');
26
+ await writeFile(bin, buf);
27
+ }
28
+ const map = await loadIpMap(bin);
29
+ const checkN = 10000;
30
+ console.log('IPv4 random test');
31
+ for (let i = 0; i < checkN; i++) {
32
+ const int = Math.round(Math.random() * 0xffff_ffff);
33
+ const res = map.findV4(int);
34
+ assert.equal(res.int, int);
35
+ assert.ok(res.range.firstInt <= int);
36
+ assert.ok(res.range.lastInt >= int);
37
+ }
38
+ console.log('IPv6 random test');
39
+ for (let i = 0; i < checkN; i++) {
40
+ let int = BigInt(2000 + Math.round(Math.random() * 0xcff));
41
+ for (let c = 0; c < 7; c++) {
42
+ int = (int << 16n) | BigInt(Math.round(Math.random() * 0xffff));
43
+ }
44
+ const res = map.findV6(int);
45
+ assert.equal(res.int, int);
46
+ assert.ok(res.range.firstInt <= int);
47
+ assert.ok(res.range.lastInt >= int);
48
+ }
49
+ console.log('Random TSV lines test');
50
+ for (let i = 0; i < checkN; i++) {
51
+ const line = tsv[Math.floor(Math.random() * tsv.length)];
52
+ const [start, _end, asNumStr, ccStr, desc] = line.split('\t');
53
+ const res = map.lookup(start);
54
+ assert.equal(res.as.country, ccStr == 'None' || ccStr == 'Unknown' ? null : ccStr);
55
+ assert.equal(String(res.as.number), asNumStr);
56
+ assert.equal(res.as.description, asNumStr == '0' ? null : desc);
57
+ }
58
+ if (!process.argv.includes('bench'))
59
+ process.exit();
60
+ const benchN = 1_000_000;
61
+ t = performance.now();
62
+ for (let i = 0; i < benchN; i++) {
63
+ const int = Math.round(Math.random() * 0xffff_ffff);
64
+ map.findV4(int);
65
+ }
66
+ {
67
+ const ms = performance.now() - t;
68
+ console.log('IPv4 random int', benchN, 'iters in', ms.toFixed(2), 'ms; ', Math.round(benchN / ms * 1000), 'lookups/s; ', (ms / benchN * 1000).toFixed(2), 'us per lookup');
69
+ }
70
+ t = performance.now();
71
+ for (let i = 0; i < benchN; i++) {
72
+ let int = BigInt(2000 + Math.round(Math.random() * 0xcff));
73
+ for (let c = 0; c < 7; c++) {
74
+ int = (int << 16n) | BigInt(Math.round(Math.random() * 0xffff));
75
+ }
76
+ map.findV6(int);
77
+ }
78
+ {
79
+ const ms = performance.now() - t;
80
+ console.log('IPv6 random int', benchN, 'iters in', ms.toFixed(2), 'ms; ', Math.round(benchN / ms * 1000), 'lookups/s; ', (ms / benchN * 1000).toFixed(2), 'us per lookup');
81
+ }
82
+ t = performance.now();
83
+ for (let i = 0; i < benchN; i++) {
84
+ const line = tsv[Math.floor(Math.random() * tsv.length)];
85
+ map.lookup(line.split('\t')[0]);
86
+ }
87
+ {
88
+ const ms = performance.now() - t;
89
+ console.log('Random TSV row', benchN, 'iters in', ms.toFixed(2), 'ms; ', Math.round(benchN / ms * 1000), 'lookups/s; ', (ms / benchN * 1000).toFixed(2), 'us per parse + lookup');
90
+ }
91
+ //# sourceMappingURL=test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test.js","sourceRoot":"","sources":["../src/test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,74 @@
1
+ export interface TupleOf<T> {
2
+ 0: [];
3
+ 1: [T];
4
+ 2: [T, T];
5
+ 3: [T, T, T];
6
+ 4: [T, T, T, T];
7
+ 5: [T, T, T, T, T];
8
+ 6: [T, T, T, T, T, T];
9
+ 7: [T, T, T, T, T, T, T];
10
+ 8: [T, T, T, T, T, T, T, T];
11
+ 9: [T, T, T, T, T, T, T, T, T];
12
+ 10: [T, T, T, T, T, T, T, T, T, T];
13
+ 11: [T, T, T, T, T, T, T, T, T, T, T];
14
+ 12: [T, T, T, T, T, T, T, T, T, T, T, T];
15
+ }
16
+ export type AnyTupleSize = keyof TupleOf<any>;
17
+ export type Tuple<T, N extends AnyTupleSize> = TupleOf<T>[N];
18
+ export type Numbers<N extends AnyTupleSize> = Tuple<number, N>;
19
+ /**
20
+ * Information about the Autonomous System for the IP range.
21
+ */
22
+ export interface AsInfo {
23
+ /**
24
+ * AS number. If 0, IP range is not assigned.
25
+ */
26
+ number: number;
27
+ /**
28
+ * 2-digit country code, where present
29
+ */
30
+ country: string | null;
31
+ /**
32
+ * AS description string, where present
33
+ */
34
+ description: string | null;
35
+ }
36
+ export interface AsRecord extends AsInfo {
37
+ offset: number;
38
+ }
39
+ /**
40
+ * IP range info
41
+ */
42
+ export interface RangeInfo<Int extends number | bigint> {
43
+ family: 4 | 6;
44
+ /**
45
+ * First address in string form.
46
+ */
47
+ first: string;
48
+ /**
49
+ * Last address in string form.
50
+ */
51
+ last: string;
52
+ firstInt: Int;
53
+ lastInt: Int;
54
+ /**
55
+ * Number of addresses.
56
+ */
57
+ size: Int;
58
+ /**
59
+ * Range index in the map, for debugging purposes.
60
+ */
61
+ index: number;
62
+ }
63
+ export type IpFamily = 4 | 6;
64
+ export type IntRepr<F extends IpFamily> = F extends 4 ? number : bigint;
65
+ /**
66
+ * IP address lookup result
67
+ */
68
+ export interface IpInfo<F extends IpFamily = IpFamily> {
69
+ family: F;
70
+ ip: string;
71
+ int: IntRepr<F>;
72
+ as: AsInfo;
73
+ range: RangeInfo<IntRepr<F>>;
74
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,4 @@
1
+ export declare function parseIPv6(addr: string): bigint;
2
+ export declare function parseIPv4(addr: string): number;
3
+ export declare function stringifyIP4(int: number): string;
4
+ export declare function stringifyIP6(int: bigint): string;
package/dist/utils.js ADDED
@@ -0,0 +1,33 @@
1
+ export function parseIPv6(addr) {
2
+ const [headStr, tailStr] = addr.split('::');
3
+ const headChunks = headStr.split(':').filter(s => !!s).map(c => c.padStart(4, '0'));
4
+ const tailChunks = tailStr?.split(':').filter(s => !!s).map(c => c.padStart(4, '0')) ?? [];
5
+ const full = [
6
+ '0x',
7
+ ...headChunks,
8
+ '0'.repeat(4 * (8 - (headChunks.length + tailChunks.length))),
9
+ ...tailChunks,
10
+ ];
11
+ return BigInt(full.join(''));
12
+ }
13
+ export function parseIPv4(addr) {
14
+ return parseInt(addr.split('.').map(s => parseInt(s, 10).toString(16).padStart(2, '0')).join(''), 16);
15
+ }
16
+ export function stringifyIP4(int) {
17
+ return [
18
+ (int >>> 24) & 255,
19
+ (int >>> 16) & 255,
20
+ (int >>> 8) & 255,
21
+ int & 255
22
+ ].join('.');
23
+ }
24
+ export function stringifyIP6(int) {
25
+ const chunks = [];
26
+ for (let s = 0; s < 8; s++) {
27
+ chunks.push(Number(int >> BigInt(s * 16) & 0xffffn));
28
+ }
29
+ while (chunks.length && !chunks[0])
30
+ chunks.shift();
31
+ return chunks.toReversed().map(c => c.toString(16)).join(':') + (chunks.length < 8 ? '::' : '');
32
+ }
33
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,SAAS,CAAC,IAAY;IACrC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAqB,CAAC;IAChE,MAAM,UAAU,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IACpF,MAAM,UAAU,GAAG,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAC3F,MAAM,IAAI,GAAG;QACZ,IAAI;QACJ,GAAG,UAAU;QACb,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;QAC7D,GAAG,UAAU;KACb,CAAC;IACF,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,IAAY;IACrC,OAAO,QAAQ,CACd,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,EAChF,EAAE,CACF,CAAC;AACH,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,GAAW;IACvC,OAAO;QACN,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,GAAG;QAClB,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,GAAG;QAClB,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG,GAAG;QACjB,GAAG,GAAG,GAAG;KACT,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACb,CAAC;AAGD,MAAM,UAAU,YAAY,CAAC,GAAW;IACvC,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5B,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;QAAE,MAAM,CAAC,KAAK,EAAE,CAAC;IACnD,OAAO,MAAM,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AACjG,CAAC"}
package/index.html ADDED
@@ -0,0 +1,16 @@
1
+ <html lang="en">
2
+ <body>
3
+ <a href="./docs">Docs</a>
4
+ <pre id="result"></pre>
5
+ <script type="module">
6
+ import { IpMap } from './dist/browser.js';
7
+ const self = await fetch('https://4.this.computer').then(res => res.text());
8
+ const res = await fetch('./data/demo.bin');
9
+ const buffer = await res.arrayBuffer();
10
+ const map = new IpMap(buffer);
11
+ const result = map.lookup(self.trim());
12
+ console.log(result);
13
+ document.getElementById('result').textContent = JSON.stringify(result, null, '\t');
14
+ </script>
15
+ </body>
16
+ </html>
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "ip-asn-map",
3
+ "version": "1.0.0",
4
+ "description": "Maps IP addresses to country and AS info using preprocessed IPtoASN data with low memory footprint",
5
+ "type": "module",
6
+ "homepage": "https://codeberg.org/virtulis/ip-asn-map",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://codeberg.org/virtulis/ip-asn-map.git"
10
+ },
11
+ "main": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "bin": {
14
+ "ip-asn-map": "dist/cli.js"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "default": "./dist/index.js"
19
+ }
20
+ },
21
+ "scripts": {
22
+ "build": "pnpm exec tsc",
23
+ "doc": "pnpm exec typedoc src"
24
+ },
25
+ "keywords": [
26
+ "ip",
27
+ "address",
28
+ "country",
29
+ "ip2asn",
30
+ "iptoasn",
31
+ "geoip"
32
+ ],
33
+ "author": "Danko Aleksejevs <danko@very.lv> (https://kludge.guru)",
34
+ "license": "CC0-1.0",
35
+ "packageManager": "pnpm@10.20.0",
36
+ "devDependencies": {
37
+ "@types/node": "^26.1.1",
38
+ "typedoc": "^0.28.20",
39
+ "typescript": "^6.0.3"
40
+ }
41
+ }
package/src/browser.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './types.ts';
2
+ export * from './map.ts';
package/src/buffer.ts ADDED
@@ -0,0 +1,200 @@
1
+ import type { Readable } from 'node:stream';
2
+
3
+ import { parseIPv4, parseIPv6 } from './utils.ts';
4
+ import type { AsRecord, Tuple } from './types.ts';
5
+ import { IpMap } from './map.ts';
6
+ import path from 'node:path';
7
+
8
+ /**
9
+ * Accepts a *stream of lines* from the IP2ASN combined TSV.
10
+ * Returns a buffer containing the encoded lookup map.
11
+ * @param stream
12
+ */
13
+ export async function encodeIp2AsnStream(stream: AsyncIterable<string>) {
14
+
15
+ const list4 = [0];
16
+ const list6 = [BigInt(0)];
17
+
18
+ let last4 = 0;
19
+ let last6 = BigInt(0);
20
+
21
+ const asn4 = [0];
22
+ const asn6 = [0];
23
+
24
+ let asOffset = 0;
25
+ const asMap = new Map<number, AsRecord>();
26
+
27
+ const provideAsn = (number: number, country: string | null, description: string | null) => {
28
+ const ex = asMap.get(number);
29
+ if (ex) return ex;
30
+ const descBuf = description ? Buffer.from(description, 'ascii') : null; // should throw if not ascii
31
+ const descLen = descBuf?.length ?? 0;
32
+ if (descLen > 255) throw new Error('Description too long');
33
+ const byteLen = 4 + 2 + 2 + descLen; // num + cc + len + desc
34
+ const as: AsRecord = {
35
+ offset: asOffset,
36
+ number,
37
+ country,
38
+ description,
39
+ };
40
+ asMap.set(number, as);
41
+ asOffset += byteLen;
42
+ return as;
43
+ }
44
+ provideAsn(0, null, null);
45
+
46
+ let i = 0;
47
+ let last: Tuple<string, 5> | undefined;
48
+ for await (const line of stream) {
49
+
50
+ i++;
51
+
52
+ const split = line.trim().split('\t') as Tuple<string, 5>;
53
+ const [start, end, asNumStr, ccStr, desc] = split;
54
+ const cc = ccStr.length == 2 ? ccStr : null;
55
+
56
+ const asNum = parseInt(asNumStr, 10);
57
+ const as = provideAsn(asNum, cc, desc?.trim() || null);
58
+
59
+ if (start.match(/^\d+\./)) {
60
+ const int = parseIPv4(start);
61
+ if (int < last4) throw new Error('Data out of order or read error');
62
+ list4.push(int);
63
+ asn4.push(as.offset);
64
+ last4 = parseIPv4(end);
65
+ }
66
+ else if (start.match(/^[0-9a-f]*:/)) {
67
+ const int = parseIPv6(start);
68
+ if (int < last6) throw new Error('Data out of order or read error');
69
+ list6.push(int);
70
+ asn6.push(as.offset);
71
+ last6 = parseIPv6(end);
72
+ }
73
+ else if (line) {
74
+ console.log(line);
75
+ throw new Error('Unexpected input at line ' + i);
76
+ }
77
+
78
+ last = split;
79
+
80
+ }
81
+
82
+ list4.push(last4 + 1);
83
+ list4.push(0xffffffff);
84
+ list6.push(last6 + 1n);
85
+ list6.push(BigInt('0x' + 'f'.repeat(32)));
86
+ asn4.push(0, 0);
87
+ asn6.push(0, 0);
88
+
89
+ const num4 = list4.length;
90
+ const num6 = list6.length;
91
+
92
+ const hdrLen = 32;
93
+ const lengths = [hdrLen, num6 * 16, num4 * 4, num6 * 4, num4 * 4, asOffset];
94
+ const offsets: number[] = [];
95
+ let pos = 0;
96
+ for (const len of lengths) {
97
+ pos += len;
98
+ offsets.push(pos);
99
+ }
100
+ const bufSize = offsets.at(-1)!;
101
+ // console.log(...lengths);
102
+ // console.log(...offsets);
103
+
104
+ const buf = Buffer.alloc(bufSize, 0);
105
+
106
+ const proceed = () => {
107
+ const next = offsets.shift()!;
108
+ if (pos != next) throw new Error(`Offset mismatch ${pos} != ${next}`);
109
+ }
110
+
111
+ const asStart = offsets.at(-2)!;
112
+
113
+ buf.write('ip-map', 0);
114
+ for (const [i, val] of [
115
+ 1, // version,
116
+ num6,
117
+ num4,
118
+ asStart,
119
+ bufSize,
120
+ ].entries()) {
121
+ buf.writeUInt32BE(val, 8 + i * 4);
122
+ }
123
+
124
+ pos = 32;
125
+
126
+ proceed();
127
+ for (const int of list6) {
128
+ buf.writeBigUInt64BE(BigInt.asUintN(64, int >> 64n), pos);
129
+ buf.writeBigUInt64BE(BigInt.asUintN(64, int), pos + 8);
130
+ pos += 16;
131
+ }
132
+
133
+ proceed();
134
+ for (const int of list4) {
135
+ buf.writeUint32BE(int, pos);
136
+ pos += 4;
137
+ }
138
+
139
+ for (const arr of [asn6, asn4]) {
140
+ proceed();
141
+ for (const asn of arr) {
142
+ buf.writeUInt32BE(asn, pos)
143
+ pos += 4;
144
+ }
145
+ }
146
+
147
+ proceed();
148
+ for (const as of asMap.values()) {
149
+ pos = asStart + as.offset;
150
+ buf.writeUInt32BE(as.number, pos);
151
+ buf.write(as.country ?? '', pos + 4);
152
+ if (as.description) {
153
+ buf.writeUInt16BE(as.description.length, pos + 6);
154
+ buf.write(as.description, pos + 8);
155
+ pos += 8 + as.description.length;
156
+ }
157
+ else {
158
+ pos += 8;
159
+ }
160
+ }
161
+ proceed();
162
+
163
+ return buf;
164
+
165
+ }
166
+
167
+ /**
168
+ * Helper function. Accepts a readable stream of the TSV text, splits it into lines and passes to {@link encodeIp2AsnStream}.
169
+ * @param stream Result of createReadStream etc
170
+ */
171
+ export async function parseIp2AsnStream(stream: Readable) {
172
+ const { createInterface } = await import('node:readline');
173
+ const rl = createInterface(stream);
174
+ return await encodeIp2AsnStream(rl);
175
+ }
176
+
177
+ /**
178
+ * Helper function. Accepts path to `ip2asn-combined.tsv.gz` and decodes it for {@link encodeIp2AsnStream}.
179
+ * @param src
180
+ */
181
+ export async function parseIp2AsnGzip(src: string) {
182
+ const { createReadStream } = await import('node:fs');
183
+ const { createGunzip } = await import('node:zlib');
184
+ const stream = createReadStream(src);
185
+ const gunzip = createGunzip();
186
+ stream.pipe(gunzip);
187
+ return await parseIp2AsnStream(gunzip);
188
+ }
189
+
190
+
191
+ /**
192
+ * Load buffer from file and create an IpMap instance.
193
+ * @param file path to the file, assumes default location in package directory otherwise.
194
+ */
195
+ export async function loadIpMap(file?: string) {
196
+ const { readFile } = await import('node:fs/promises');
197
+ file ??= path.join(import.meta.dirname, '../data/ip-map.bin');
198
+ const buf = await readFile(file);
199
+ return new IpMap(buf.buffer);
200
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createReadStream } from 'node:fs';
4
+ import { loadIpMap, parseIp2AsnGzip, parseIp2AsnStream } from './buffer.ts';
5
+ import * as process from 'node:process';
6
+ import { mkdir, writeFile } from 'node:fs/promises';
7
+ import { Readable } from 'node:stream';
8
+ import { ReadableStream } from 'node:stream/web';
9
+ import { createGunzip } from 'node:zlib';
10
+ import { Buffer } from 'node:buffer';
11
+ import path from 'node:path';
12
+
13
+ const [command, ...args] = process.argv.slice(2);
14
+
15
+ async function writeBuffer(buffer: Buffer, dest?: string) {
16
+ if (dest == '-') {
17
+ process.stdout.write(buffer);
18
+ return;
19
+ }
20
+ if (!dest) {
21
+ const dir = path.join(import.meta.dirname, '../data');
22
+ await mkdir(dir, { recursive: true });
23
+ dest = path.join(dir, 'ip-map.bin');
24
+ console.log('Saving to', dest);
25
+ }
26
+ await writeFile(dest, buffer);
27
+ }
28
+
29
+ if (command == 'parse') {
30
+
31
+ if (args.length < 1) throw new Error('At least a source file is expected');
32
+ const [src, dest] = args as [string, string | undefined];
33
+
34
+ let buffer;
35
+ if (src.endsWith('.gz')) {
36
+ buffer = await parseIp2AsnGzip(src)
37
+ }
38
+ else if (src == '-') {
39
+ buffer = await parseIp2AsnStream(process.stdin)
40
+ }
41
+ else {
42
+ buffer = await parseIp2AsnStream(createReadStream(src))
43
+ }
44
+
45
+ await writeBuffer(buffer, dest);
46
+
47
+ }
48
+
49
+ else if (command == 'update') {
50
+
51
+ const gunzip = createGunzip();
52
+
53
+ const url = 'https://iptoasn.com/data/ip2asn-combined.tsv.gz';
54
+ console.error('Fetching', url);
55
+ const res = await fetch(url);
56
+ if (!res.ok) throw new Error(res.statusText);
57
+
58
+ console.error('Parsing');
59
+ Readable.fromWeb(res.body as ReadableStream).pipe(gunzip);
60
+ const buffer = await parseIp2AsnStream(gunzip);
61
+
62
+ await writeBuffer(buffer, args[0]);
63
+
64
+ }
65
+
66
+ else if (command == 'lookup') {
67
+ const fn = args[1] || path.join(import.meta.dirname, '../data/ip-map.bin');
68
+ const map = await loadIpMap(fn);
69
+ const result = map.lookup(args[0]!);
70
+ console.log(result);
71
+ }
72
+
73
+ else {
74
+ throw new Error('Unknown command or no command given. See README.md for info.');
75
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './browser.ts';
2
+ export * from './buffer.ts';
3
+