Files: 970c91583716ce49e23e1a89eb78103d6ecfdfa9 / lib / query.js
13281 bytesRaw
1 | var Pull = require("pull-stream"); |
2 | var KVSet = require("kvset"); |
3 | var Pad = require("pad-ipv6"); |
4 | var Query = module.exports = {}; |
5 | |
6 | function compareRecordsBySerial(a, b) { |
7 | return b.data.serial - a.data.serial; |
8 | } |
9 | |
10 | function compareRecords(a, b) { |
11 | return a.name > b.name ? 1 : a.name < b.name ? -1 : |
12 | a.type > b.type ? 1 : a.type < b.type ? -1 : |
13 | a.class > b.class ? 1 : a.class < b.class ? -1 : |
14 | 0 |
15 | } |
16 | |
17 | function isRecordEqual(a, b) { |
18 | return (a === b) || (a && b |
19 | && a.name === b.name |
20 | && a.type === b.type |
21 | && a.class === b.class); |
22 | } |
23 | |
24 | function mergeRecords(into, from) { |
25 | if (from) from.filter(function (a) { |
26 | return into.every(function (b) { |
27 | return !isRecordEqual(a, b); |
28 | }) |
29 | }).forEach(function (rec) { |
30 | into.push(rec); |
31 | }); |
32 | } |
33 | |
34 | function mergeResults(into, from) { |
35 | mergeRecords(into.additionals, from.additionals); |
36 | mergeRecords(into.answers, from.answers); |
37 | mergeRecords(into.authorities, from.authorities); |
38 | mergeRecords(into.questions, from.questions); |
39 | if (from.expires < into.expires) into.expires = from.expires; |
40 | into.domainExists |= from.domainExists |
41 | into.authoritative |= from.authoritative |
42 | into.cache &= from.cache |
43 | } |
44 | |
45 | Query.branches = function (sbot, name, type, _class, cb) { |
46 | if (!_class) _class = "IN"; |
47 | var branches = []; |
48 | Pull(Query.all(sbot), |
49 | Pull.filter(function (record) { |
50 | return record.name == name |
51 | && record.type == type |
52 | && record.class == _class; |
53 | }), |
54 | Query.drainSet(function (record) { |
55 | branches.push(record.id); |
56 | }, function (err) { |
57 | cb(err, branches); |
58 | })); |
59 | }; |
60 | |
61 | function msgToRecord(msg) { |
62 | var c = msg.value.content; |
63 | var r = c && c.record; |
64 | if (!r) return; |
65 | r.id = msg.key; |
66 | r.author = msg.value.author; |
67 | r.timestamp = msg.value.timestamp; |
68 | r.branch = c.branch; |
69 | if (!r.ttl) r.ttl = 500; |
70 | if (!r.class) r.class = "IN"; |
71 | if (r.value) r.data = r.value, delete r.value |
72 | if (r.type === 'AAAA') r.data = Pad(r.data); |
73 | return r; |
74 | } |
75 | |
76 | Query.all = function (sbot) { |
77 | return Pull(sbot.messagesByType({ |
78 | type: 'ssb-dns', |
79 | }), |
80 | Pull.map(msgToRecord), |
81 | Pull.filter()); |
82 | }; |
83 | |
84 | function recordsInDomain(sbot, name) { |
85 | var path = name.split(/\./g).reverse() |
86 | |
87 | /* enable this when records without path propery are to be deprecated: |
88 | // use ssb-query if it is supported |
89 | if (sbot.query) return sbot.query.read({ |
90 | query: [{$filter: {value: {content: { |
91 | type: 'ssb-dns', |
92 | path: {$prefix: path} |
93 | }}}}] |
94 | }); |
95 | */ |
96 | |
97 | // fallback to logt |
98 | return Pull(sbot.messagesByType({ |
99 | type: 'ssb-dns', |
100 | }), |
101 | Pull.filter(function (msg) { |
102 | var c = msg.value.content; |
103 | var p = c.path; |
104 | if (!p) { |
105 | var name = c.record && c.record.name; |
106 | if (typeof name !== 'string') return false; |
107 | p = name.split(/\./).reverse() |
108 | } |
109 | for (var i = 0; i < path.length; i++) { |
110 | if (path[i] !== p[i]) return false; |
111 | } |
112 | return true; |
113 | })); |
114 | } |
115 | |
116 | Query.inDomain = function (sbot, name) { |
117 | if (!name) return Query.all(sbot); |
118 | return Pull(recordsInDomain(sbot, name), |
119 | Pull.map(msgToRecord), |
120 | Pull.filter()); |
121 | }; |
122 | |
123 | function expandName(name, wildcard) { |
124 | var names = {'': true}; |
125 | names[name] = true; |
126 | for (var labels = name.split(/\./); labels.length; labels.shift()) { |
127 | if (wildcard) labels[0] = wildcard; |
128 | names[labels.join('.')] = true; |
129 | } |
130 | return names; |
131 | } |
132 | |
133 | function Records() { |
134 | this.recs = []; |
135 | } |
136 | |
137 | Records.prototype.addRecord = function (record) { |
138 | this.recs.push(record); |
139 | }; |
140 | |
141 | Records.prototype.getRecords = function () { |
142 | return this.recs; |
143 | }; |
144 | |
145 | function Wildcards() { |
146 | this.lengths = []; |
147 | this.recordsByLength = {}; |
148 | } |
149 | |
150 | function RecordsMap() { |
151 | this.recs = {}; |
152 | } |
153 | |
154 | RecordsMap.prototype.addRecord = function (r) { |
155 | if (r.name in this.recs) { |
156 | this.recs[r.name].push(r); |
157 | } else { |
158 | this.recs[r.name] = [r]; |
159 | } |
160 | }; |
161 | |
162 | RecordsMap.prototype.popRecords = function (name) { |
163 | name = name.replace(/\.$/, ''); |
164 | var recs = this.recs[name]; |
165 | delete this.recs[name]; |
166 | return recs || []; |
167 | }; |
168 | |
169 | Wildcards.prototype.addRecord = function (record) { |
170 | var len = record.name.length; |
171 | if (len in this.recordsByLength) { |
172 | this.recordsByLength[len].push(record); |
173 | } else { |
174 | this.recordsByLength[len] = [record]; |
175 | this.lengths.push(len); |
176 | } |
177 | }; |
178 | |
179 | Wildcards.prototype.getRecords = function () { |
180 | // get records for the longest name length |
181 | if (this.lengths.length) { |
182 | var len = Math.max.apply(Math, this.lengths); |
183 | return this.recordsByLength[len] || []; |
184 | } |
185 | return []; |
186 | }; |
187 | |
188 | Wildcards.prototype.getTopRecords = function () { |
189 | // get records for the shortest name length |
190 | if (this.lengths.length) { |
191 | var len = Math.min.apply(Math, this.lengths); |
192 | return this.recordsByLength[len] || []; |
193 | } |
194 | return []; |
195 | }; |
196 | |
197 | function ZoneSerials() { |
198 | this.serials = {}; |
199 | } |
200 | |
201 | ZoneSerials.prototype.addRecord = function (zones) { |
202 | for (var zone in zones) { |
203 | this.serials[zone] = (+this.serials[zone] || 0) + 1; |
204 | } |
205 | }; |
206 | |
207 | ZoneSerials.prototype.getSerial = function (zone) { |
208 | return this.serials[zone] % 0x100000000; |
209 | }; |
210 | |
211 | Query.drainSet = function (each, onEnd) { |
212 | var set = new KVSet(); |
213 | return Pull.drain(function (record) { |
214 | if (record.branch) set.remove(record.branch); |
215 | if (record.data) set.add(record.id, record); |
216 | }, function (err) { |
217 | if (err) return onEnd(err); |
218 | for (var key in set.heads) { |
219 | var record = set.heads[key]; |
220 | try { |
221 | each(record); |
222 | } catch(e) { |
223 | return onEnd(e); |
224 | } |
225 | } |
226 | onEnd(null); |
227 | }); |
228 | }; |
229 | |
230 | Query.collectSet = function (cb) { |
231 | var records = []; |
232 | return Query.drainSet(function (record) { |
233 | records.push(record); |
234 | }, function (err) { |
235 | return cb(err, records); |
236 | }); |
237 | }; |
238 | |
239 | var nonRecurseTypes = { |
240 | CNAME: true, |
241 | AXFR: true, |
242 | IXFR: true, |
243 | }; |
244 | |
245 | Query.query = function (sbot, question, cb) { |
246 | if (nonRecurseTypes[question.type]) { |
247 | Query.querySingle(sbot, question, cb); |
248 | } else { |
249 | Query.queryRecursive(sbot, question, [], cb); |
250 | } |
251 | } |
252 | |
253 | // recursive query |
254 | Query.queryRecursive = function (sbot, question, stack, cb) { |
255 | var result = { |
256 | cache: true, |
257 | answers: [], |
258 | authorities: [], |
259 | additionals: [], |
260 | questions: [], |
261 | expires: Date.now() + 60*60e3, |
262 | }; |
263 | |
264 | // avoid infinite recursion |
265 | if (stack.some(function (q) { |
266 | return isRecordEqual(q, question); |
267 | })) { |
268 | return cb(null, result) |
269 | } |
270 | |
271 | var waiting = 1; |
272 | Query.querySingle(sbot, question, next); |
273 | |
274 | function next(err, res) { |
275 | if (err) return waiting = 0, cb(err); |
276 | |
277 | mergeResults(result, res) |
278 | |
279 | // recurse on CNAMEs |
280 | var stack2 = stack.concat(question) |
281 | res.answers.filter(function (answer) { |
282 | return answer.type === 'CNAME'; |
283 | }).map(function (record) { |
284 | return { |
285 | class: question.class, |
286 | type: question.type, |
287 | name: record.data.replace(/\.$/, '') |
288 | } |
289 | }).forEach(function (q) { |
290 | waiting++ |
291 | Query.queryRecursive(sbot, q, stack2, next) |
292 | }); |
293 | |
294 | if (!--waiting) cb(null, result); |
295 | } |
296 | } |
297 | |
298 | function removeTrailingDot(domain) { |
299 | return String(domain).replace(/\.$/, '') |
300 | } |
301 | |
302 | Query.querySingle = function (sbot, question, cb) { |
303 | // look up records that match a question, including wildcard records |
304 | // and zone authority records |
305 | var qName = question.name.toLowerCase(); |
306 | var authorityDomains = expandName(qName); |
307 | var wildcardDomains = expandName(qName, '*'); |
308 | var isIncrementalTransfer = question.type === 'IXFR' |
309 | var isTransfer = isIncrementalTransfer || question.type === 'AXFR' |
310 | isIncrementalTransfer = false // TODO: fix IXFR |
311 | |
312 | var authorities = new Wildcards(); |
313 | var maybeGlue = new RecordsMap(); |
314 | var answers = isTransfer ? new Records() : new Wildcards(); |
315 | var zoneSerials = new ZoneSerials(); |
316 | var result = { |
317 | cache: !isTransfer |
318 | }; |
319 | Pull(Query.all(sbot), |
320 | Pull.filter(function (record) { |
321 | var rName = removeTrailingDot(record.name) |
322 | var recordDomains = expandName(rName); |
323 | zoneSerials.addRecord(recordDomains); |
324 | if (isIncrementalTransfer |
325 | && zoneSerials.getSerial(qName) < question.serial) { |
326 | return false |
327 | } |
328 | var nameMatches = isTransfer |
329 | ? qName in recordDomains |
330 | : rName in wildcardDomains; |
331 | if (nameMatches) { |
332 | result.domainExists = true; |
333 | } |
334 | if (!isTransfer) { |
335 | if (record.type === 'A' || record.type === 'AAAA') { |
336 | // include all because we might need them for glue |
337 | return true; |
338 | } |
339 | if (record.type === 'NS' || record.type === 'SOA') { |
340 | return record.name in authorityDomains |
341 | } |
342 | } |
343 | return nameMatches |
344 | && (isTransfer |
345 | || question.type === record.type |
346 | || question.type === '*' |
347 | || 'CNAME' === record.type) |
348 | && (question.class === record.class |
349 | || question.class === '*'); |
350 | }), |
351 | Query.drainSet(function (record) { |
352 | if (record.type === 'NS' || record.type === 'SOA') { |
353 | result.authoritative = true; |
354 | if (question.class === record.class |
355 | && question.type === record.type |
356 | && removeTrailingDot(record.name) in wildcardDomains) { |
357 | answers.addRecord(record); |
358 | } else { |
359 | authorities.addRecord(record); |
360 | if (isTransfer) { |
361 | answers.addRecord(record); |
362 | } |
363 | } |
364 | } else if (!isTransfer |
365 | && (record.type === 'A' || record.type === 'AAAA') |
366 | && (!(removeTrailingDot(record.name) in wildcardDomains) |
367 | || (question.type !== record.type && question.type !== '*') |
368 | || (question.class !== record.class && question.class !== '*')) |
369 | ) { |
370 | maybeGlue.addRecord(record); |
371 | } else { |
372 | answers.addRecord(record); |
373 | } |
374 | }, function (err) { |
375 | if (err) return cb(err); |
376 | var ttl = 3600; // max internal ttl |
377 | result.answers = answers.getRecords(); |
378 | result.answers.forEach(function (record) { |
379 | if (record.ttl < ttl) ttl = record.ttl; |
380 | }); |
381 | |
382 | result.additionals = []; |
383 | result.authorities = isTransfer |
384 | ? authorities.getTopRecords() |
385 | : authorities.getRecords(); |
386 | result.authorities.forEach(updateAuthority); |
387 | result.answers.forEach(updateAuthority); |
388 | function updateAuthority(r) { |
389 | if (r.type === 'SOA') { |
390 | if (!r.data.serial) { |
391 | // special case: calculate a serial for the SOA |
392 | r.data.serial = zoneSerials.getSerial(r.name); |
393 | } |
394 | if (r.ttl < ttl) ttl = r.ttl; |
395 | if (!result.answers.length) { |
396 | if (r.data.ttl < ttl) ttl = r.data.ttl; |
397 | } |
398 | if (!isTransfer) { |
399 | result.additionals = result.additionals.concat( |
400 | maybeGlue.popRecords(r.data.mname)) |
401 | } |
402 | } else if (r.type === 'NS') { |
403 | if (!isTransfer) { |
404 | result.additionals = result.additionals.concat( |
405 | maybeGlue.popRecords(r.data)) |
406 | } |
407 | } |
408 | } |
409 | |
410 | result.expires = Date.now() + ttl * 60e3; |
411 | result.questions = []; |
412 | |
413 | if (isTransfer) { |
414 | // RFC 5936, Section 2.2 |
415 | result.questions.push(question); |
416 | // pick a SOA record to use as the bookend |
417 | var soa = result.authorities.filter(function (r) { |
418 | return r.type === 'SOA'; |
419 | }).sort(compareRecordsBySerial)[0]; |
420 | result.authorities.length = 0; |
421 | if (soa) { |
422 | result.answers = [soa].concat( |
423 | result.answers.filter(function (r) { |
424 | return r !== soa |
425 | }).sort(compareRecords), |
426 | [soa] |
427 | ); |
428 | } |
429 | } else { |
430 | // only include SOA if there are no answers |
431 | if (result.answers.length > 0) { |
432 | result.authorities = result.authorities.filter(function (r) { |
433 | return r.type !== 'SOA'; |
434 | }); |
435 | } |
436 | // include SOA if there are no answers, NS if there are |
437 | result.authorities = result.authorities.filter(function (r) { |
438 | return r.type !== (result.answers.length ? 'SOA' : 'NS'); |
439 | }); |
440 | |
441 | // resolve wildcards in answers |
442 | result.answers.forEach(function (r) { |
443 | if (r.name !== qName && r.name in wildcardDomains) { |
444 | r.name = qName; |
445 | } |
446 | }) |
447 | } |
448 | cb(null, result); |
449 | })); |
450 | }; |
451 |
Built with git-ssb-web