140 lines
		
	
	
		
			4.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			140 lines
		
	
	
		
			4.8 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'");
 | |
| }
 |