-
Notifications
You must be signed in to change notification settings - Fork 21
/
lookup.js
151 lines (131 loc) · 4.14 KB
/
lookup.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import { default as memoize } from 'p-memoize'
import ip from 'ip'
import { decode as dagCborDecode } from '@ipld/dag-cbor'
import { CID } from 'multiformats/cid'
import fetch from 'cross-fetch'
import { formatData } from './format.js'
import { MAX_LOOKUP_RETRIES } from './constants.js'
export const GEOIP_ROOT = CID.parse('bafyreihnpl7ami7esahkfdnemm6idx4r2n6u3apmtcrxlqwuapgjsciihy') // b-tree version of GeoLite2-City-CSV_20221018
const defaultGateway = ['https://ipfs.io', 'https://dweb.link']
/**
* @param {object|string} ipfs
* @param {CID} cid
* @returns {Promise}
*/
async function getRawBlock (ipfs, cid) {
if (typeof ipfs === 'function') {
return ipfs(cid)
}
if (typeof ipfs?.block?.get === 'function') {
// use Core JS API (https://github.com/ipfs/js-ipfs/blob/master/docs/core-api/BLOCK.md)
return ipfs.block.get(cid)
}
// Assume ipfs is gateway url or a list of gateway urls to try in order
const gateways = Array.isArray(ipfs) ? ipfs : [ipfs]
for (const url of gateways) { // eslint-disable-line no-unreachable-loop
const gwUrl = new URL(url)
gwUrl.pathname = `/ipfs/${cid.toString()}`
gwUrl.search = '?format=raw' // necessary as not every gateway supports dag-cbor, but every should support sending raw block as-is
try {
const res = await fetch(gwUrl, {
headers: {
// also set header, just in case ?format= is filtered out by some reverse proxy
Accept: 'application/vnd.ipld.raw'
},
cache: 'force-cache'
})
if (!res.ok) throw res
return new Uint8Array(await res.arrayBuffer())
} catch (cause) {
throw new Error(`unable to fetch raw block for CID ${cid}`, { cause })
}
}
}
/**
* Gets Obj and Block after retrying multiple times.
*
* @param {object|string} ipfs
* @param {CID} cid
* @param {number} numTry - this will be 1 for the first try and recurse till MAX_LOOKUP_RETRIES is reached.
* @returns {Promise<{obj, block}>}
*/
async function getObjAndBlockWithRetries (ipfs, cid, numTry = 1) {
try {
const block = await getRawBlock(ipfs, cid)
const obj = await dagCborDecode(block)
return { obj, block }
} catch (e) {
if (numTry < MAX_LOOKUP_RETRIES) {
return await getObjAndBlockWithRetries(ipfs, cid, numTry + 1)
}
throw e
}
}
/**
* @param {object|string} ipfs
* @param {CID} cid
* @param {string} lookfor - ip
* @returns {Promise}
*/
async function _lookup (ipfs, cid, lookfor) {
let obj, block
try {
({ obj, block } = await getObjAndBlockWithRetries(ipfs, cid))
} catch (e) {
if (process?.env?.DEBUG || process?.env?.TEST) {
if (!block) {
console.error(`[ipfs-geoip] failed to get raw block for CID '${cid}'`, e) // eslint-disable-line no-console
} else {
console.error(`[ipfs-geoip] failed to parse DAG-CBOR behind CID '${cid}'`, e) // eslint-disable-line no-console
}
}
throw e
}
let child = 0
if (!('data' in obj)) { // regular node
while (obj.mins[child] && obj.mins[child] <= lookfor) {
child++
}
const next = obj.links[child - 1]
if (!next) {
throw new Error('Failed to lookup node')
}
const nextCid = getCid(next)
if (!nextCid) {
throw new Error('Failed to lookup node')
}
return memoizedLookup(ipfs, nextCid, lookfor)
} else if ('data' in obj) { // leaf node
while (obj.data[child] && obj.data[child].min <= lookfor) {
child++
}
const next = obj.data[child - 1]
if (!next) {
throw new Error('Failed to lookup leaf node')
}
if (!next.data) {
throw new Error('Unmapped range')
}
return formatData(next.data)
}
}
const memoizedLookup = memoize(_lookup, {
cachePromiseRejection: false,
cacheKey: args => {
// cache based on cid+ip: we ignore first argument, which is ipfs api instance
const [, cid, lookfor] = args
return `${cid}.${lookfor}`
}
})
/**
* @param {object} ipfs
* @param {string} ipstring
* @returns {Promise}
*/
export function lookup (ipfs = defaultGateway, ipstring) {
return memoizedLookup(ipfs, GEOIP_ROOT, ip.toLong(ipstring))
}
function getCid (node) {
if (!node) return null
return CID.asCID(node)
}