Merge pull request #4 from dzegarra/allow-specify-http-status

This commit is contained in:
Wildan M 2023-04-24 04:47:45 +07:00 committed by GitHub
commit 9f40085899
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 46 additions and 13 deletions

View file

@ -7,7 +7,7 @@
> We're back with improvements! See [CHANGES.md](CHANGES.md) > We're back with improvements! See [CHANGES.md](CHANGES.md)
This services forwards domains using 301 HTTP(s) redirects. This service forwards domains using 301 HTTP(s) redirects by default.
Possible scenarios: Possible scenarios:
@ -49,6 +49,14 @@ The star `*` at the end tells us that the remaining URL path is also forwarded t
> If you use Cloudflare or any DNS which supports [CNAME Flattening](https://blog.cloudflare.com/introducing-cname-flattening-rfc-compliant-cnames-at-a-domains-root/), you still can use CNAME records pointing to `r.forwarddomain.net`, it's much recommended to use CNAME records rather than A/AAAA records. > If you use Cloudflare or any DNS which supports [CNAME Flattening](https://blog.cloudflare.com/introducing-cname-flattening-rfc-compliant-cnames-at-a-domains-root/), you still can use CNAME records pointing to `r.forwarddomain.net`, it's much recommended to use CNAME records rather than A/AAAA records.
You can choose the type of redirection you want to use by declaring the `http-status` value:
```
www.old.com IN CNAME r.forwarddomain.net
_.www.old.com IN TXT http-status=302;forward-domain=https://old.com/*
```
> Only the http status code 301 and 302 are supported.
## FAQ ## FAQ

View file

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

View file

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