175 lines
4.6 KiB
JavaScript
175 lines
4.6 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
var crypto = require('crypto');
|
||
|
//var dnsjs = require('dns-suite');
|
||
|
var dig = require('dig.js/dns-request');
|
||
|
var request = require('util').promisify(require('@root/request'));
|
||
|
var express = require('express');
|
||
|
var app = express();
|
||
|
|
||
|
var nameservers = require('dns').getServers();
|
||
|
var index = crypto.randomBytes(2).readUInt16BE(0) % nameservers.length;
|
||
|
var nameserver = nameservers[index];
|
||
|
|
||
|
app.use('/', express.static(__dirname));
|
||
|
app.use('/api', express.json());
|
||
|
app.get('/api/dns/:domain', function(req, res, next) {
|
||
|
var domain = req.params.domain;
|
||
|
var casedDomain = domain
|
||
|
.toLowerCase()
|
||
|
.split('')
|
||
|
.map(function(ch) {
|
||
|
// dns0x20 takes advantage of the fact that the binary operation for toUpperCase is
|
||
|
// ch = ch | 0x20;
|
||
|
return Math.round(Math.random()) % 2 ? ch : ch.toUpperCase();
|
||
|
})
|
||
|
.join('');
|
||
|
var typ = req.query.type;
|
||
|
var query = {
|
||
|
header: {
|
||
|
id: crypto.randomBytes(2).readUInt16BE(0),
|
||
|
qr: 0,
|
||
|
opcode: 0,
|
||
|
aa: 0, // Authoritative-Only
|
||
|
tc: 0, // NA
|
||
|
rd: 1, // Recurse
|
||
|
ra: 0, // NA
|
||
|
rcode: 0 // NA
|
||
|
},
|
||
|
question: [
|
||
|
{
|
||
|
name: casedDomain,
|
||
|
//, type: typ || 'A'
|
||
|
typeName: typ || 'A',
|
||
|
className: 'IN'
|
||
|
}
|
||
|
]
|
||
|
};
|
||
|
var opts = {
|
||
|
onError: function(err) {
|
||
|
next(err);
|
||
|
},
|
||
|
onMessage: function(packet) {
|
||
|
var fail0x20;
|
||
|
|
||
|
if (packet.id !== query.id) {
|
||
|
console.error(
|
||
|
"[SECURITY] ignoring packet for '" +
|
||
|
packet.question[0].name +
|
||
|
"' due to mismatched id"
|
||
|
);
|
||
|
console.error(packet);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
packet.question.forEach(function(q) {
|
||
|
// if (-1 === q.name.lastIndexOf(cli.casedQuery))
|
||
|
if (q.name !== casedDomain) {
|
||
|
fail0x20 = q.name;
|
||
|
}
|
||
|
});
|
||
|
|
||
|
['question', 'answer', 'authority', 'additional'].forEach(function(
|
||
|
group
|
||
|
) {
|
||
|
(packet[group] || []).forEach(function(a) {
|
||
|
var an = a.name;
|
||
|
var i = domain
|
||
|
.toLowerCase()
|
||
|
.lastIndexOf(a.name.toLowerCase()); // answer is something like ExAMPle.cOM and query was wWw.ExAMPle.cOM
|
||
|
var j = a.name
|
||
|
.toLowerCase()
|
||
|
.lastIndexOf(domain.toLowerCase()); // answer is something like www.ExAMPle.cOM and query was ExAMPle.cOM
|
||
|
|
||
|
// it's important to note that these should only relpace changes in casing that we expected
|
||
|
// any abnormalities should be left intact to go "huh?" about
|
||
|
// TODO detect abnormalities?
|
||
|
if (-1 !== i) {
|
||
|
// "EXamPLE.cOm".replace("wWw.EXamPLE.cOm".substr(4), "www.example.com".substr(4))
|
||
|
a.name = a.name.replace(
|
||
|
casedDomain.substr(i),
|
||
|
domain.substr(i)
|
||
|
);
|
||
|
} else if (-1 !== j) {
|
||
|
// "www.example.com".replace("EXamPLE.cOm", "example.com")
|
||
|
a.name =
|
||
|
a.name.substr(0, j) +
|
||
|
a.name.substr(j).replace(casedDomain, domain);
|
||
|
}
|
||
|
|
||
|
// NOTE: right now this assumes that anything matching the query matches all the way to the end
|
||
|
// it does not handle the case of a record for example.com.uk being returned in response to a query for www.example.com correctly
|
||
|
// (but I don't think it should need to)
|
||
|
if (a.name.length !== an.length) {
|
||
|
console.error(
|
||
|
"[ERROR] question / answer mismatch: '" +
|
||
|
an +
|
||
|
"' != '" +
|
||
|
a.length +
|
||
|
"'"
|
||
|
);
|
||
|
console.error(a);
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
|
||
|
if (fail0x20) {
|
||
|
console.warn(
|
||
|
";; Warning: DNS 0x20 security not implemented (or packet spoofed). Queried '" +
|
||
|
casedDomain +
|
||
|
"' but got response for '" +
|
||
|
fail0x20 +
|
||
|
"'."
|
||
|
);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
res.send({
|
||
|
header: packet.header,
|
||
|
question: packet.question,
|
||
|
answer: packet.answer,
|
||
|
authority: packet.authority,
|
||
|
additional: packet.additional,
|
||
|
edns_options: packet.edns_options
|
||
|
});
|
||
|
},
|
||
|
onListening: function() {},
|
||
|
onSent: function(/*res*/) {},
|
||
|
onTimeout: function(res) {
|
||
|
console.error('dns timeout:', res);
|
||
|
next(new Error('DNS timeout - no response'));
|
||
|
},
|
||
|
onClose: function() {},
|
||
|
//, mdns: cli.mdns
|
||
|
nameserver: nameserver,
|
||
|
port: 53,
|
||
|
timeout: 2000
|
||
|
};
|
||
|
|
||
|
dig.resolveJson(query, opts);
|
||
|
});
|
||
|
app.get('/api/http', function(req, res) {
|
||
|
var url = req.query.url;
|
||
|
return request({ method: 'GET', url: url }).then(function(resp) {
|
||
|
res.send(resp.body);
|
||
|
});
|
||
|
});
|
||
|
app.get('/api/_acme_api_', function(req, res) {
|
||
|
res.send({ success: true });
|
||
|
});
|
||
|
|
||
|
module.exports = app;
|
||
|
if (require.main === module) {
|
||
|
// curl -L http://localhost:3000/api/dns/example.com?type=A
|
||
|
console.info('Listening on localhost:3000');
|
||
|
app.listen(3000);
|
||
|
console.info('Try this:');
|
||
|
console.info("\tcurl -L 'http://localhost:3000/api/_acme_api_/'");
|
||
|
console.info(
|
||
|
"\tcurl -L 'http://localhost:3000/api/dns/example.com?type=A'"
|
||
|
);
|
||
|
console.info(
|
||
|
"\tcurl -L 'http://localhost:3000/api/http/?url=https://example.com'"
|
||
|
);
|
||
|
}
|