arblant
BlueKnight-Panel-Marvel / railway / lib / dns-fetch-node.mjs
dns-fetch-node.mjs61 lines · 2.8 KB
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
import http2 from 'node:http2';

// Some DoH providers reject HTTP/1.1. Retry DNS requests over verified HTTP/2.
export async function dnsFetch(url, init = {}) {
  const target = new URL(url);
  try {
    const response = await fetch(url, { ...init, signal: init.signal || AbortSignal.timeout(10000) });
    if (![400, 505].includes(response.status) || target.protocol !== 'https:') return response;
    await response.body?.cancel();
  } catch (err) {
    if (target.protocol !== 'https:' || init.signal?.aborted) throw err;
  }
  return fetchHttp2(target, init);
}

export function fetchHttp2(target, init = {}, connectOptions = {}) {
  target = new URL(target);
  return new Promise((resolve, reject) => {
    const session = http2.connect(target.origin, connectOptions);
    let request, settled = false;
    const finish = (error, response) => {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      init.signal?.removeEventListener('abort', abort);
      session.destroy();
      if (error) reject(error); else resolve(response);
    };
    const abort = () => finish(init.signal.reason || new Error('DNS request aborted'));
    const timer = setTimeout(() => finish(new Error('DNS HTTP/2 timeout')), 10000);
    session.on('error', error => finish(error));
    if (init.signal?.aborted) { abort(); return; }
    init.signal?.addEventListener('abort', abort, { once: true });
    session.once('connect', () => {
      try {
        const headers = Object.fromEntries(new Headers(init.headers));
        if (init.body) headers['content-length'] = String(Buffer.byteLength(Buffer.from(init.body)));
        request = session.request({ ...headers, ':method': init.method || 'GET', ':path': target.pathname + target.search });
        let responseHeaders, length = 0;
        const chunks = [];
        request.on('response', h => { responseHeaders = h; });
        request.on('error', error => finish(error));
        request.on('aborted', () => finish(new Error('DNS HTTP/2 response aborted')));
        request.on('data', chunk => {
          length += chunk.length;
          if (length > 1024 * 1024) finish(new Error('DNS response too large'));
          else chunks.push(chunk);
        });
        request.on('end', () => {
          if (!responseHeaders) return finish(new Error('DNS HTTP/2 response missing headers'));
          try {
            const status = Number(responseHeaders[':status']);
            const headers = Object.fromEntries(Object.entries(responseHeaders).filter(([key]) => !key.startsWith(':')));
            finish(null, new Response([204, 205, 304].includes(status) ? null : Buffer.concat(chunks), { status, headers }));
          } catch (err) { finish(err); }
        });
        request.end(init.body ? Buffer.from(init.body) : undefined);
      } catch (err) { finish(err); }
    });
  });
}