Allow users decide what HTTP status to use for redirecting

This commit is contained in:
Daniel Zegarra 2023-04-23 17:28:26 +02:00
commit 0d27b892c9
2 changed files with 38 additions and 11 deletions

View file

@ -1,12 +1,13 @@
import { client } from "./sni.js";
import { findTxtRecord, isHostBlacklisted, combineURLs } from "./util.js";
const record_prefix = 'forward-domain=';
/**
* @typedef {Object} Cache
* @property {string} url
* @property {boolean} expand
* @property {boolean} blacklisted
* @property {number} expire
* @property {number} httpStatus
*/
/**
* @type {Object<string, Cache>}
@ -18,7 +19,11 @@ const resolveCache = {};
*/
async function buildCache(host) {
let expand = false;
let url = await findTxtRecord(host, record_prefix);
let recordData = await findTxtRecord(host);
if (!recordData) {
throw new Error(`The record data for "${host}" is missing`);
}
let {url, httpStatus = '301'} = recordData;
if (url.indexOf('http://') !== 0 && url.indexOf('https://') !== 0) {
throw new Error(url + ' in TXT record is not an absolute URL');
}
@ -26,11 +31,16 @@ async function buildCache(host) {
url = url.slice(0, -1);
expand = true;
}
if(!['301', '302'].includes(httpStatus)) {
httpStatus = '301';
console.warn(`The record "${url}" wants to use the http status code ${httpStatus} which is not allowed (only 301 and 302). Using 301 instead.`);
}
return {
url,
expand,
blacklisted: isHostBlacklisted(host),
expire: Date.now() + 86400 * 1000,
httpStatus: parseInt(httpStatus),
};
}
const acme_prefix = '/.well-known/acme-challenge/';
@ -65,12 +75,12 @@ const listener = async function (req, res) {
resolveCache[host] = cache;
}
if (cache.blacklisted) {
res.writeHead(301, {
res.writeHead(cache.httpStatus, {
'Location': (process.env.BLACKLIST_REDIRECT || 'https://forwarddomain.net/blacklisted') + "?d=" + req.headers.host,
});
return;
}
res.writeHead(301, {
res.writeHead(cache.httpStatus, {
'Location': cache.expand ? combineURLs(cache.url, url) : cache.url,
});
return;

View file

@ -2,6 +2,8 @@ import { default as axios } from "axios";
import crypto from "crypto";
import fs from "fs";
const recordParamDestUrl = 'forward-domain';
const recordParamHttpStatus = 'http-status';
const blacklistURL = (process.env.BLACKLIST_HOSTS || "").split(',').reduce((acc, host) => {
acc[host] = true;
return acc;
@ -39,21 +41,36 @@ export async function ensureDir(dir) {
}
/**
* @param {string} host
* @param {string} prefix
* @param {string} value
*/
export async function findTxtRecord(host, prefix, required = true) {
const parseTxtRecordData = (value) => {
const result = {};
for (const part of value.split(';')) {
const [key, value] = part.split('=');
if (key && value) {
result[key] = value;
}
}
return result;
}
/**
* @param {string} host
* @return {Promise<{url: string, httpStatus?: string} | null>}
*/
export async function findTxtRecord(host) {
const resolve = await axios(`https://dns.google/resolve?name=_.${encodeURIComponent(host)}&type=TXT`);
if (resolve.data.Answer) {
for (const head of resolve.data.Answer) {
if (!head.data.startsWith(prefix))
if (!head.data.includes(recordParamDestUrl))
continue;
return head.data.slice(prefix.length);
const txtData = parseTxtRecordData(head.data);
return {
url: txtData[recordParamDestUrl],
httpStatus: txtData[recordParamHttpStatus],
};
}
}
if (required) {
throw new Error(prefix + ' TXT is missing');
}
return null;
}