/ src / index.ts
index.ts
  1  import { Router } from 'itty-router';
  2  import { Buffer } from 'node:buffer';
  3  import base64url from 'base64url';
  4  import * as dnsPacket from '@dnsquery/dns-packet';
  5  import Config from '../config.json';
  6  import Resolvers from '../resolvers.json';
  7  import Package from '../package-lock.json';
  8  
  9  const router = Router();
 10  
 11  Array.prototype.sample = function(){
 12  	return this[Math.floor(Math.random()*this.length)];
 13  }
 14  
 15  Array.prototype.sampleN = function(n: any) {
 16  	var result = new Array(n),
 17  		len = this.length,
 18  		taken = new Array(len);
 19  	if (n > len)
 20  		throw new RangeError("getRandom: more elements taken than available");
 21  	while (n--) {
 22  		var x = Math.floor(Math.random() * len);
 23  		result[n] = this[x in taken ? taken[x] : x];
 24  		taken[x] = --len in taken ? taken[len] : len;
 25  	}
 26  	return result;
 27  }
 28  
 29  function toRcode(code: any) {
 30  	switch (code.toUpperCase()) {
 31  		case 'NOERROR': return 0
 32  		case 'FORMERR': return 1
 33  		case 'SERVFAIL': return 2
 34  		case 'NXDOMAIN': return 3
 35  		case 'NOTIMP': return 4
 36  		case 'REFUSED': return 5
 37  		case 'YXDOMAIN': return 6
 38  		case 'YXRRSET': return 7
 39  		case 'NXRRSET': return 8
 40  		case 'NOTAUTH': return 9
 41  		case 'NOTZONE': return 10
 42  		case 'RCODE_11': return 11
 43  		case 'RCODE_12': return 12
 44  		case 'RCODE_13': return 13
 45  		case 'RCODE_14': return 14
 46  		case 'RCODE_15': return 15
 47  	}
 48  	return 0
 49  }
 50  
 51  function toTypes(type: any) {
 52  	switch (type.toUpperCase()) {
 53  		case 'A': return 1
 54  		case 'NULL': return 10
 55  		case 'AAAA': return 28
 56  		case 'AFSDB': return 18
 57  		case 'APL': return 42
 58  		case 'CAA': return 257
 59  		case 'CDNSKEY': return 60
 60  		case 'CDS': return 59
 61  		case 'CERT': return 37
 62  		case 'CNAME': return 5
 63  		case 'DHCID': return 49
 64  		case 'DLV': return 32769
 65  		case 'DNAME': return 39
 66  		case 'DNSKEY': return 48
 67  		case 'DS': return 43
 68  		case 'HIP': return 55
 69  		case 'HINFO': return 13
 70  		case 'IPSECKEY': return 45
 71  		case 'KEY': return 25
 72  		case 'KX': return 36
 73  		case 'LOC': return 29
 74  		case 'MX': return 15
 75  		case 'NAPTR': return 35
 76  		case 'NS': return 2
 77  		case 'NSEC': return 47
 78  		case 'NSEC3': return 50
 79  		case 'NSEC3PARAM': return 51
 80  		case 'PTR': return 12
 81  		case 'RRSIG': return 46
 82  		case 'RP': return 17
 83  		case 'SIG': return 24
 84  		case 'SOA': return 6
 85  		case 'SPF': return 99
 86  		case 'SRV': return 33
 87  		case 'SSHFP': return 44
 88  		case 'TA': return 32768
 89  		case 'TKEY': return 249
 90  		case 'TLSA': return 52
 91  		case 'TSIG': return 250
 92  		case 'TXT': return 16
 93  		case 'AXFR': return 252
 94  		case 'IXFR': return 251
 95  		case 'OPT': return 41
 96  		case 'ANY': return 255
 97  		case '*': return 255
 98  	}
 99  	if (type.toUpperCase().startsWith('UNKNOWN_')) return parseInt(name.slice(8))
100  	return 0
101  }
102  
103  async function getDNSResponse(url: any) {
104  	let p: any = new URL(url).hostname;
105  	let r: any = await fetch(url, {
106  		headers: {
107  			'Content-Type': 'application/dns-message'
108  		}
109  	})
110  
111  	if (r.status !== 200) throw Promise.reject(`Encountered a non 200 response from ${p}`);
112  	return r;
113  }
114  
115  function chooseResolvers(resolvers: any, q: any, n: any = 3) {
116  	let p = [];
117  	if (resolvers.length > n) {
118  		for (let r of resolvers.sampleN(n)) {
119  			p.push(getDNSResponse(`${Resolvers[r]}?dns=${q}`))
120  		}
121  	}
122  	else {
123  		// Otherwise, pick one
124  		p.push(getDNSResponse(`${Resolvers[resolvers.sample()]}?dns=${q}`))
125  	}
126  
127  	return p;
128  }
129  
130  function getRandomInt (min: any, max: any) {
131  	return Math.floor(Math.random() * (max - min + 1)) + min
132  }
133  
134  router.all('/resolve', async (request, env, context) => {
135  	// First, grab some request information
136  	let url: any = new URL(request.url)
137  
138  	// Now, we refuse anything that isn't GET or POST
139  	if (!['GET', 'POST'].includes(request.method)) {
140  		return new Response('Not Found.', { status: 404 })
141  	}
142  
143  	let name: any;
144  	let rrtype: any = 'A';
145  
146  	if (request.method == 'GET') {
147  		if (request.query.name) name = request.query.name || null;
148  		if (request.query.type) rrtype = request.query.type.toUpperCase();
149  
150  		if (name == null) return new Response('Missing name in ?name=', { status: 400 })
151  	}
152  
153  	if (!['A', 'AAAA', 'DNSKEY', 'MX', 'SRV', 'TXT'].includes(rrtype)) return new Response('Unsupported rrtype', { status: 400 })
154  
155  	// Next, we need to prepare a query
156  	let query: any = dnsPacket.encode({
157  		type: 'query',
158  		id: getRandomInt(1, 65534),
159  		flags: dnsPacket.RECURSION_DESIRED,
160  		questions: [{
161  			type: rrtype,
162  			name: name
163  		}]
164  	})
165  	query = base64url(query);
166  
167  	// Next, we prepare to send it on, first pick a resolver (by default, we use the default)
168  	let resolver: any = Config['default'].resolvers
169  	if (Config[url.hostname]) {
170  		// Check now for a resolvers set for the hostname the request came in on
171  		resolver = Config[url.hostname].resolvers
172  	}
173  
174  	let providers = chooseResolvers(resolver, query);
175  	
176  	// And send it off
177  	let answer: any;
178  	try {
179  		answer = await Promise.any(providers);
180  	}
181  	catch(e: any) {
182  		return new Response('We encountered a server error. Please try again later', { status: 500 })
183  	}
184  
185  	// Once we have an answer, we read it in
186  	let decoded: any = await answer.arrayBuffer();
187  	decoded = Buffer.from(decoded);
188  
189  	// And next, we decode it
190  	decoded = dnsPacket.decode(decoded);
191  	
192  	// Now, we need to prepare the response
193  	let resp: any = {}
194  
195  	// Initially, did the query even work?
196  	resp.Status = toRcode(decoded.rcode);
197  
198  	// Next, we'll add some flags
199  	for (let key of Object.keys(decoded)) {
200  		if (key.includes('flag_')) {
201  			if (['AD', 'CD', 'RA', 'RD', 'TC'].includes(key.replaceAll('flag_', '').toUpperCase())) {
202  				resp[key.replaceAll('flag_', '').toUpperCase()] = decoded[key]
203  			}
204  		}
205  	}
206  
207  	// And the question
208  	resp.Question = [];
209  	for (let q of decoded.questions) {
210  		resp.Question.push({
211  			'name': `${q.name}.`,
212  			'type': toTypes(q.type)
213  		})
214  	};
215  
216  
217  	// Now, we determine if there is an answer to give
218  	if (decoded.answers.length > 0) {
219  		resp.Answer = [];
220  		for (let ans of decoded.answers) {
221  
222  			let r: any = {
223  				'name': `${ans.name}.`,
224  				'type': toTypes(ans.type),
225  				'TTL': ans.ttl,
226  				'data': ans.data
227  			}
228  
229  			if (['DNSKEY'].includes(ans.type)) r.data = `${ans.data.flags} ${ans.data.algorithm} ${btoa(String.fromCharCode.apply(null, ans.data.key))}`;
230  			if (['TXT'].includes(ans.type)) r.data = ans.data[0].toString()
231  			if (['SRV'].includes(ans.type)) r.data = `${ans.data.priority} ${ans.data.weight} ${ans.data.port} ${ans.data.target}.`
232  
233  			resp.Answer.push(r)
234  		}
235  	}
236  	if (decoded.answers.length == 0) {
237  		resp.Authority = [];
238  		for (let auth of decoded.authorities) {
239  			resp.Authority.push({
240  				'name': auth.name,
241  				'type': toTypes(auth.type),
242  				'TTL': auth.ttl,
243  				'data': `${auth.data.mname}. ${auth.data.rname}. ${auth.data.serial} ${auth.data.refresh} ${auth.data.retry} ${auth.data.expire} ${auth.data.minimum}`
244  			})
245  		}
246  	}
247  
248  	// And a comment from where it came from
249  	let prov: any = new URL(answer.url).hostname;
250  	resp.Comment = `Response from ${prov}`
251  
252  	return new Response(JSON.stringify(resp), { headers: { 'Content-Type': 'application/json'}})
253  })
254  
255  router.all('/dns-query', async (request, env, context) => {
256  	// First, grab some request information
257  	let url: any = new URL(request.url)
258  
259  	// Now, we refuse anything that isn't GET or POST
260  	if (!['GET', 'POST'].includes(request.method)) {
261  		return new Response('Not Found.', { status: 404 })
262  	}
263  
264  	// And grab the question
265  	let q: any = null;
266  	if (request.method == 'GET') {
267  		if (request.query.dns) {
268  			q = request.query.dns;
269  		}
270  		else {
271  			return new Response('Missing query in ?dns=', { status: 400 })
272  		}
273  	}
274  	if (request.method == 'POST') {
275  		q = await request.arrayBuffer();
276  		q = Buffer.from(q);
277  	}
278  
279  	// Now, to validate the payload
280  	let t: any;
281  	try {
282  		t = Buffer.from(q, 'base64');
283  		t = dnsPacket.decode(t);
284  	}
285  	catch(e: any) {
286  		return new Response('Invalid query', { status: 500 })
287  	}
288  
289  	// Next, we prepare to send it on, first pick a resolver (by default, we use the default)
290  	let resolver: any = Config['default'].resolvers
291  	if (Object.keys(Config).includes(url.hostname)) {
292  		// Check now for a resolvers set for the hostname the request came in on
293  		resolver = Config[url.hostname].resolvers
294  	}
295  
296  	if (request.method == 'POST') q = base64url(q);
297  	let providers = chooseResolvers(resolver, q);
298  	
299  	// And send it off
300  	let answer: any;
301  	let a: any;
302  	try {
303  		answer = await Promise.any(providers);
304  		a = await answer.arrayBuffer();
305  	}
306  	catch(e: any) {
307  		// So if we get here, something happened, so we'll try and build our own response
308  		return new Response(`We encountered an error while performing this lookup: ${e}`, { status: 500 });
309  	}
310  
311  	// And if we need a debug issue
312  	if (request.url.includes('?debug')) console.log(new URL(answer.url).hostname)
313  	return new Response(a, {
314  		headers: {
315  			'Content-Type': answer.headers.get('Content-Type'),
316  			'X-Provider': new URL(answer.url).hostname
317  		},
318  		status: answer.status
319  	})
320  })
321  
322  router.get('/dns-providers', async (request) => {
323  
324  	// First, grab some request information
325  	let url: any = new URL(request.url);
326  
327  	// Now, prepare a payload
328  	let resp: any = {
329  		'providers': []
330  	}
331  
332  	// Next, we prepare to send it on, first pick a resolver (set to our default set)
333  	let resolver: any = Config['default'].resolvers;
334  	if (Config[url.hostname]) {
335  		// Check now for a resolvers set for the hostname the request came in on
336  		resolver = Config[url.hostname].resolvers
337  	}
338  	
339  	// Add each provider to the response, so they can be seen
340  	for (let r of resolver) {
341  		resp.providers.push(Resolvers[r])
342  	}
343  
344  	// And return that data
345  	return new Response(JSON.stringify(resp, null, 2), { headers: {'Content-Type': 'application/json'}})
346  })
347  
348  router.get('/version', async (request) => {
349  	return new Response(Package.version, {
350  		headers: {
351  			'Content-Type': 'text/plain'
352  		}
353  	})
354  });
355  
356  router.get('/', async (request) => {
357  	// First, we grab the hostname they asked for
358  	let hostname: any = new URL(request.url).hostname
359  	let resolver: any = Config['default'].resolvers;
360  	if (Config[hostname]) {
361  		// Check now for a resolvers set for the hostname the request came in on
362  		resolver = Config[hostname].resolvers;
363  	}
364  
365  	// Now to grab the resolver URLs
366  	let resolvers: any = [];
367  	for (let r of resolver) {
368  		resolvers.push(Resolvers[r])
369  	}
370  
371  	// This is going to be an amazing hack so I don't have to mess with KV
372  	let data: any = await fetch('https://mydns.network/_resolver.html', {
373  		cf: {
374  			cacheTtl: 90,
375  			cacheEverything: true,
376  		}
377  	})
378  	data = await data.text()
379  
380  	// And now we make some changes to the stored HTML
381  	data = data.replaceAll('[HOSTNAME]', hostname);
382  	data = data.replaceAll('[NAME]', hostname.replace('.mydns.network', ''));
383  	data = data.replaceAll('[RESOLVERS]', resolvers.join('\n'))
384  
385  	// And craft a new response
386  	return new Response(data, {
387  		headers: {
388  			'Content-Type': 'text/html'
389  		}
390  	})
391  });
392  
393  router.all("*", () => new Response("404, not found!", { status: 404 }))
394  
395  export default {
396  	fetch: router.handle
397  }