-
Notifications
You must be signed in to change notification settings - Fork 323
/
dnslink.js
229 lines (209 loc) · 8.93 KB
/
dnslink.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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
'use strict'
/* eslint-env browser */
const debug = require('debug')
const log = debug('ipfs-companion:dnslink')
log.error = debug('ipfs-companion:dnslink:error')
const IsIpfs = require('is-ipfs')
const LRU = require('lru-cache')
const { default: PQueue } = require('p-queue')
const { offlinePeerCount } = require('./state')
const { pathAtHttpGateway } = require('./ipfs-path')
// TODO: add Preferences toggle to disable redirect of DNSLink websites (while keeping async dnslink lookup)
module.exports = function createDnslinkResolver (getState) {
// DNSLink lookup result cache
const cacheOptions = { max: 1000, maxAge: 1000 * 60 * 60 * 12 }
const cache = new LRU(cacheOptions)
// upper bound for concurrent background lookups done by resolve(url)
const lookupQueue = new PQueue({ concurrency: 4 })
// preload of DNSLink data
const preloadUrlCache = new LRU(cacheOptions)
const preloadQueue = new PQueue({ concurrency: 4 })
const dnslinkResolver = {
get _cache () {
return cache
},
setDnslink (fqdn, value) {
cache.set(fqdn, value)
},
clearCache () {
cache.reset()
},
cachedDnslink (fqdn) {
return cache.get(fqdn)
},
canLookupURL (requestUrl) {
// skip URLs that could produce infinite recursion or weird loops
const state = getState()
return state.dnslinkPolicy &&
requestUrl.startsWith('http') &&
!IsIpfs.url(requestUrl) &&
!requestUrl.startsWith(state.apiURLString) &&
!requestUrl.startsWith(state.gwURLString)
},
dnslinkRedirect (url, dnslink) {
if (typeof url === 'string') {
url = new URL(url)
}
if (dnslinkResolver.canRedirectToIpns(url, dnslink)) {
const state = getState()
// redirect to IPNS and leave it up to the gateway
// to load the correct path from IPFS
// - https://github.com/ipfs/ipfs-companion/issues/298
const ipnsPath = dnslinkResolver.convertToIpnsPath(url)
const gateway = state.ipfsNodeType === 'embedded' ? state.pubGwURLString : state.gwURLString
// TODO: redirect to `ipns://` if hasNativeProtocolHandler === true
return { redirectUrl: pathAtHttpGateway(ipnsPath, gateway) }
}
},
readAndCacheDnslink (fqdn) {
let dnslink = dnslinkResolver.cachedDnslink(fqdn)
if (typeof dnslink === 'undefined') {
try {
log(`dnslink cache miss for '${fqdn}', running DNS TXT lookup`)
dnslink = dnslinkResolver.readDnslinkFromTxtRecord(fqdn)
if (dnslink) {
// TODO: set TTL as maxAge: setDnslink(fqdn, dnslink, maxAge)
dnslinkResolver.setDnslink(fqdn, dnslink)
log(`found dnslink: '${fqdn}' -> '${dnslink}'`)
} else {
dnslinkResolver.setDnslink(fqdn, false)
log(`found NO dnslink for '${fqdn}'`)
}
} catch (error) {
log.error(`error in readAndCacheDnslink for '${fqdn}'`, error)
console.error(error)
}
} else {
// Most of the time we will hit cache, which makes below line is too noisy
// console.info(`[ipfs-companion] using cached dnslink: '${fqdn}' -> '${dnslink}'`)
}
return dnslink
},
// runs async lookup in a queue in the background and returns the record
async resolve (url) {
if (!dnslinkResolver.canLookupURL(url)) return
const fqdn = new URL(url).hostname
const cachedResult = dnslinkResolver.cachedDnslink(fqdn)
if (cachedResult) return cachedResult
return lookupQueue.add(() => {
return dnslinkResolver.readAndCacheDnslink(fqdn)
})
},
// preloads data behind the url to local node
async preloadData (url) {
const state = getState()
if (!state.dnslinkDataPreload || state.dnslinkRedirect) return
if (preloadUrlCache.get(url)) return
preloadUrlCache.set(url, true)
const dnslink = await dnslinkResolver.resolve(url)
if (!dnslink) return
if (state.ipfsNodeType === 'embedded') return
if (state.peerCount < 1) return
return preloadQueue.add(async () => {
const { pathname } = new URL(url)
const preloadUrl = new URL(state.gwURLString)
preloadUrl.pathname = `${dnslink}${pathname}`
await fetch(preloadUrl.toString(), { method: 'HEAD' })
return preloadUrl
})
},
// low level lookup without cache
readDnslinkFromTxtRecord (fqdn) {
const state = getState()
let apiProvider
// TODO: fix DNS resolver for ipfsNodeType='embedded:chromesockets', for now use ipfs.io
if (!state.ipfsNodeType.startsWith('embedded') && state.peerCount !== offlinePeerCount) {
apiProvider = state.apiURLString
} else {
// fallback to resolver at public gateway
apiProvider = 'https://ipfs.io/'
}
// js-ipfs-api does not provide method for fetching this
// TODO: revisit after https://github.com/ipfs/js-ipfs-api/issues/501 is addressed
// TODO: consider worst-case-scenario fallback to https://developers.google.com/speed/public-dns/docs/dns-over-https
const apiCall = `${apiProvider}api/v0/dns/${fqdn}?r=true`
const xhr = new XMLHttpRequest() // older XHR API us used because window.fetch appends Origin which causes error 403 in go-ipfs
// synchronous mode with small timeout
// (it is okay, because we do it only once, then it is cached and read via readAndCacheDnslink)
xhr.open('GET', apiCall, false)
xhr.setRequestHeader('Accept', 'application/json')
xhr.send(null)
if (xhr.status === 200) {
const dnslink = JSON.parse(xhr.responseText).Path
// console.log('readDnslinkFromTxtRecord', readDnslinkFromTxtRecord)
if (!IsIpfs.path(dnslink)) {
throw new Error(`dnslink for '${fqdn}' is not a valid IPFS path: '${dnslink}'`)
}
return dnslink
} else if (xhr.status === 500) {
// go-ipfs returns 500 if host has no dnslink or an error occurred
// TODO: find/fill an upstream bug to make this more intuitive
return false
} else {
throw new Error(xhr.statusText)
}
},
canRedirectToIpns (url, dnslink) {
if (typeof url === 'string') {
url = new URL(url)
}
// Safety check: detect and skip gateway paths
// Public gateways such as ipfs.io are often exposed under the same domain name.
// We don't want dnslink to interfere with content-addressing redirects,
// or things like /api/v0 paths exposed by the writable gateway
// so we ignore known namespaces exposed by HTTP2IPFS gateways
// and ignore them even if things like CID are invalid
// -- we don't want to skew errors from gateway
const path = url.pathname
const httpGatewayPath = path.startsWith('/ipfs/') || path.startsWith('/ipns/') || path.startsWith('/api/v')
if (!httpGatewayPath) {
const fqdn = url.hostname
// If dnslink policy is 'enabled' then lookups will be
// executed for every unique hostname on every visited website.
// Until we get efficient DNS TXT Lookup API there will be an overhead,
// so 'enabled' policy is an opt-in for now. By default we use
// 'best-effort' policy which does async lookups to populate dnslink cache
// in the background and do blocking lookup only when X-Ipfs-Path header
// is found in initial response.
// More: https://github.com/ipfs-shipyard/ipfs-companion/blob/master/docs/dnslink.md
const foundDnslink = dnslink ||
(getState().dnslinkPolicy === 'enabled'
? dnslinkResolver.readAndCacheDnslink(fqdn)
: dnslinkResolver.cachedDnslink(fqdn))
if (foundDnslink) {
return true
}
}
return false
},
convertToIpnsPath (url) {
if (typeof url === 'string') {
url = new URL(url)
}
return `/ipns/${url.hostname}${url.pathname}${url.search}${url.hash}`
},
// Test if URL contains a valid DNSLink FQDN
// in url.hostname OR in url.pathname (/ipns/<fqdn>)
// and return matching FQDN if present
findDNSLinkHostname (url) {
const { hostname, pathname } = new URL(url)
// check //foo.tld/ipns/<fqdn>
if (IsIpfs.ipnsPath(pathname)) {
// we may have false-positives here, so we do additional checks below
const ipnsRoot = pathname.match(/^\/ipns\/([^/]+)/)[1]
// console.log('findDNSLinkHostname ==> inspecting IPNS root', ipnsRoot)
// Ignore PeerIDs, match DNSLink only
if (!IsIpfs.cid(ipnsRoot) && dnslinkResolver.readAndCacheDnslink(ipnsRoot)) {
// console.log('findDNSLinkHostname ==> found DNSLink for FQDN in url.pathname: ', ipnsRoot)
return ipnsRoot
}
}
// check //<fqdn>/foo/bar
if (dnslinkResolver.readAndCacheDnslink(hostname)) {
// console.log('findDNSLinkHostname ==> found DNSLink for url.hostname', hostname)
return hostname
}
}
}
return dnslinkResolver
}