git ssb

15+

ansuz / dnssb



Tree: 970c91583716ce49e23e1a89eb78103d6ecfdfa9

Files: 970c91583716ce49e23e1a89eb78103d6ecfdfa9 / lib / query.js

13281 bytesRaw
1var Pull = require("pull-stream");
2var KVSet = require("kvset");
3var Pad = require("pad-ipv6");
4var Query = module.exports = {};
5
6function compareRecordsBySerial(a, b) {
7 return b.data.serial - a.data.serial;
8}
9
10function 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
17function 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
24function 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
34function 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
45Query.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
61function 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
76Query.all = function (sbot) {
77 return Pull(sbot.messagesByType({
78 type: 'ssb-dns',
79 }),
80 Pull.map(msgToRecord),
81 Pull.filter());
82};
83
84function 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
116Query.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
123function 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
133function Records() {
134 this.recs = [];
135}
136
137Records.prototype.addRecord = function (record) {
138 this.recs.push(record);
139};
140
141Records.prototype.getRecords = function () {
142 return this.recs;
143};
144
145function Wildcards() {
146 this.lengths = [];
147 this.recordsByLength = {};
148}
149
150function RecordsMap() {
151 this.recs = {};
152}
153
154RecordsMap.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
162RecordsMap.prototype.popRecords = function (name) {
163 name = name.replace(/\.$/, '');
164 var recs = this.recs[name];
165 delete this.recs[name];
166 return recs || [];
167};
168
169Wildcards.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
179Wildcards.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
188Wildcards.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
197function ZoneSerials() {
198 this.serials = {};
199}
200
201ZoneSerials.prototype.addRecord = function (zones) {
202 for (var zone in zones) {
203 this.serials[zone] = (+this.serials[zone] || 0) + 1;
204 }
205};
206
207ZoneSerials.prototype.getSerial = function (zone) {
208 return this.serials[zone] % 0x100000000;
209};
210
211Query.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
230Query.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
239var nonRecurseTypes = {
240 CNAME: true,
241 AXFR: true,
242 IXFR: true,
243};
244
245Query.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
254Query.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
298function removeTrailingDot(domain) {
299 return String(domain).replace(/\.$/, '')
300}
301
302Query.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