/
nmcresolver.js
315 lines (281 loc) · 9.49 KB
/
nmcresolver.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
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
/**
* The point of this module is to do exactly one thing:
*
* Given a hostname, transform it according to the
* Namecoin shared database.
*
* Ideally, whatever it returns can either be connected
* to directly, or can be resolved through an ordinary
* DNS server.
*
* This code should obey the spec as best as possible,
* avoid infinite loops, and avoid hax.
*
*
*/
var dns = require("./ndns/ndns-client");
// if true, disable external DNS resolution
var private_mode = false;
// if true, prefer Tor addresses over alternatives
var tor_mode = false;
// if true, prefer i2p addresses over alternatives
var i2p_mode = false;
// if true, load a test name file that override live namecoin values
var testing = false;
var testData = {};
// mostly here to break infinite delegate loops or similar shenanigans
const MAX_SCAN = 10; // tweak if we start to see legitimate cases for more record access for a single name resolution
function resolveHostWithNamecoin(host, counter, callback) {
if (counter > MAX_SCAN) {
return callback(new Error("Too many loops to resolve."));
}
var chunks = host.toLowerCase().split(".").reverse();
var TLD = chunks.shift();
if (TLD==="") { TLD = chunks.shift(); }
if (TLD!=="bit") {
// not one of mine.
callback(null, host);
return;
}
var domain = chunks.shift(); // chunks now has sub-domains, if any
getRecord("d/"+domain, "", function(err, data) {
if (err) { return callback(err); }
resolveFromValue(host, null, chunks, data, counter, callback);
});
}
function getRecord(key, sub, callback) {
if (testing) {
if (testData[key]!=null) {
return nameHandler(null, [{
name:key,
value:JSON.stringify(testData[key])
}]);
} else {
}
}
require("./nmcrpc").call("name_scan", [key, 1], nameHandler);
function nameHandler(err, data) {
if (err) { return callback(err); }
if (data.length != 1 || data[0].name!=key) {
return callback(new Error("Namecoin key not found"));
}
try {
//console.log("Value for",key,"with sub=",sub,"is",data[0].value);
var value = JSON.parse(data[0].value);
// crawl into the sub.
if (sub!="") {
var chunks = sub.split(".").reverse();
for (var i=0,l=chunks.length;i<l;i++) {
value = value.map[chunks[i]];
}
}
} catch(e) {
return callback(new Error("Invalid namecoin value"));
}
if (value != null) {
callback(null, value);
} else {
callback(new Error("Empty namecoin value."));
}
}
}
function mergeKeys(from, to) {
for (var key in from) {
switch(typeof to[key]) {
case "undefined":
to[key] = from[key]; break;
case "object":
if (to[key] == null) {
to[key] = from[key];
} else {
mergeKeys(from[key], to[key]);
}
break;
default:
// the spec calls for not clobbering data with import, so do nothing.
}
}
}
function resolveFromValue(host, parent, chunks, value, counter, callback) {
//console.log("resolveFromValue(",host,",",parent,",",chunks,",",value,",",counter,")");
if (counter > MAX_SCAN) {
return callback(new Error("Too many loops to resolve."));
}
// 1. delegate processing
// we have a delegate situation if: we have a delegate key OR the whole value is an array.
var delegate = (value instanceof Array)?value:value.delegate;
if (delegate) {
if (typeof delegate == "string") { delegate = [ delegate, "" ]; }
return getRecord(delegate[0], delegate[1]||"", function(err, data) {
if (err) { return callback(err); }
resolveFromValue(host, parent, chunks, data, counter + 1, callback);
});
}
// 2. import processing
while (value.import instanceof Array && value.import.length) {
// implemented by progressively modifying the current value.
var import = value.import.shift();
if (import instanceof Array) {
return getRecord(import[0], import[1], function(err, data) {
if (err) {
// XXX is this too lax?
return resolveFromValue(host, parent, chunks, value, counter + 1, callback);
}
var subchunks = import[1].split(".").reverse();
var base = value;
while(subchunks.length) {
var key = subchunks.shift();
if (!base.map) { base.map = {}; }
if (!base.map.key) { base.map[key] = {}; }
base = base.map[key];
}
// merge items from data into base.
mergeKeys(data, base);
// restart processing with our changes
resolveFromValue(host, parent, chunks, value, counter + 1, callback);
});
}
}
// 3. dns (/ns) resolvers are looked up here.
var dns;
if (value.ns&&!value.dns) { value.dns = value.ns; }
if (value.dns) {
dns = oneOf(value.dns);
}
// 4. translate should happen here
if (value.translate) {
// XXX the spec currently says it should only apply to subs. ignoring that part for now.
var new_host = (chunks.length?chunks.reverse().join(".")+".":"")+value.translate;
return resolveHostWithNamecoin(new_host, counter+1, callback);
}
// 5. alias check.
if (value.alias != undefined) {
// 2 options: absolute (ends with "."), or relative to parent of current state
var alias=value.alias;
if (alias.substr(-1)==".") {
// absolute alias. just use that value directly
return resolveHostWithNamecoin(alias, counter+1, callback);
}
if (alias.substr(-1)=="@") { // something like "us.@"..
var domain=host.split(".").filter(function(e){return e!=""}).slice(-2).join("."); // XXX could probably be cached from somewhere.
return resolveHostWithNamecoin(alias.split("@")[0]+domain, counter + 1, callback);
}
// relative-to-parent alias..
if (alias=="") {
if (parent) {
return resolveFromValue(host, null, [], parent, counter + 1, callback);
}
} else {
// crawl the parent's map to resolve this alias..
try {
var aliasChunks = alias.split(".");
var data = parent;
while (aliasChunks.length) {
data = data.map[aliasChunks.shift()];
}
return resolveFromValue(host, null, [], data, counter + 1, callback);
} catch (e) {
// XXX out-of-spec fallback. invalid relative alias, process as if it was an absolute alias.
return resolveHostWithNamecoin(alias, counter+1, callback);
}
}
}
// 6. apply DNS if it is set.
// (note: if private_mode is set, avoid this path and keep going to allow
// alternate resolution mechanisms to happen.)
if (dns&&!private_mode) {
// resolve host with @dns.
//console.log("resolving ",host,"through dns server",dns);
return resolveWithDNS(host, dns, callback);
}
// map processing
if (chunks.length) {
var sub = chunks.shift();
// check for a map match.
if (!value.map || !value.map[sub]) {
// check for a wildcard.
if (value.map && value.map["*"]) {
return resolveFromValue(host, value, [], value.map["*"], counter, callback);
}
return callback(new Error("Host not found"));
}
return resolveFromValue(host, value, chunks, value.map[sub], counter, callback);
}
// 1. legacy crap: if value a string?
if (typeof value == "string") {
return callback(null, value);
}
// enforce tor_mode
if (tor_mode && value.tor) {
return callback(null, value.tor);
}
// enforce i2p_mode
if (i2p_mode && value.i2p) {
return callback(null, value.i2p.b32);
}
// 2. else, find some other hardcoded value to use.
var ip;
if (value.ip) {
ip = oneOf(value.ip);
// if it's not an ipv4 address, we ignore it.
if (require("net").isIP(ip)==4) {
return callback(null, ip);
}
}
if (value.ip6) {
ip = oneOf(value.ip6);
// if it's not an ipv6 address, we ignore it.
if (require("net").isIP(ip)==6) {
return callback(null, ip);
}
}
// do the map[""] special case
if (value.map && value.map[""]) {
return resolveFromValue(host, parent, chunks, value.map[""], counter, callback);
}
// last-ditch attempts. Those are not likely to be useful.
if (value.tor) { return callback(null, value.tor); }
if (value.i2p) { return callback(null, value.i2p.b32); }
callback(new Error("Host not found, or something's not implemented"));
function oneOf(value) {
if (typeof value == "string") { return value; }
if (value instanceof Array) {
return value[~~(Math.random()*value.length)];
}
}
}
function resolveWithDNS(host, server, callback) {
if (private_mode) {
// refuse to resolve
return callback(new Error("Cannot use external DNS server in private mode."));
}
require("./ndns/ndns-client").resolve4(host, server, function(err, data) {
if (err) {
callback(err);
} else {
if (!data.length) { return callback(new Error("DNS server returned no results.")); }
callback(null, data[~~(Math.random()*data.length)]);
}
});
}
// allow for easy command line testing.
// # node resolve.js some.domain.name
if (process.argv[1].indexOf("nmcresolver.js")>-1) {
testing = true;
testData = require("./name_scan");
resolveHostWithNamecoin(process.argv[2], 0, function(err, data) {
if (err) {
console.log("ERROR: ", err.message);
} else {
console.log("ANSWER:",data);
}
});
}
module.exports = {
resolve: function(host, callback) {
return resolveHostWithNamecoin(host, 0, callback);
},
setPrivateMode: function(flag) { private_mode = !!flag; },
setTorMode: function(flag) { tor_mode = !!flag; },
setI2PMode: function(flag) { i2p_mode = !!flag; }
};