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'" | ||
|  | 	); | ||
|  | } |