Merge pull request #942 from SkynetLabs/dnslink

dnslink beta support
This commit is contained in:
Karol Wypchło 2021-07-08 17:51:23 +02:00 committed by GitHub
commit 6ae40bb6bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 177 additions and 1 deletions

View File

@ -8,6 +8,14 @@ updates:
open-pull-requests-limit: 10
assignees:
- kwypchlo
- package-ecosystem: npm
directory: "/packages/dnslink-api"
schedule:
interval: weekly
time: "10:00"
open-pull-requests-limit: 10
assignees:
- kwypchlo
- package-ecosystem: npm
directory: "/packages/handshake-api"
schedule:

View File

@ -26,3 +26,6 @@ jobs:
- name: "Static code analysis: dashboard"
run: yarn workspace dashboard prettier --check .
- name: "Static code analysis: dnslink-api"
run: yarn workspace dnslink-api prettier --check .

View File

@ -81,6 +81,7 @@ services:
depends_on:
- sia
- handshake-api
- dnslink-api
- website
website:
@ -142,6 +143,19 @@ services:
depends_on:
- handshake
dnslink-api:
build:
context: ./packages/dnslink-api
dockerfile: Dockerfile
container_name: dnslink-api
restart: unless-stopped
logging: *default-logging
networks:
shared:
ipv4_address: 10.10.10.55
expose:
- 3100
health-check:
build:
context: ./packages/health-check

View File

@ -7,7 +7,19 @@
on_demand
}
reverse_proxy nginx:80
reverse_proxy nginx:80 {
# add Dnslink-Lookup header so nginx knows that the request comes from a domain
# outside of our certificate string and should perform a dnslink lookup
header_up Dnslink-Lookup true
}
}
:80 {
reverse_proxy nginx:80 {
# add Dnslink-Lookup header so nginx knows that the request comes from a domain
# outside of our certificate string and should perform a dnslink lookup
header_up Dnslink-Lookup true
}
}
# Make sure you have SSL_CERTIFICATE_STRING specified in .env file because you need it to fetch correct certificates.

View File

@ -88,6 +88,12 @@ server {
return 461;
}
# redirect to dnslookup endpoint
error_page 462 = @dnslink_lookup;
if ($http_dnslink_lookup) {
return 462;
}
location / {
include /etc/nginx/conf.d/include/cors;
@ -452,6 +458,31 @@ server {
proxy_pass http://siad/skynet/skylink/$skylink_v1$path$is_args$args;
}
location @dnslink_lookup {
include /etc/nginx/conf.d/include/proxy-buffer;
set $dnslink '';
rewrite_by_lua_block {
local http = require("socket.http")
local ok, statusCode, headers, statusText = http.request {
url = "http://dnslink-api:3100/dnslink/" .. ngx.var.host
}
if statusCode == ngx.HTTP_OK then
ngx.var.dnslink = headers["skynet-skylink"]
else
ngx.status = statusCode
ngx.header["content-type"] = "text/plain"
ngx.say(headers["dnslink-error"])
ngx.exit(statusCode)
end
}
proxy_set_header Dnslink-Lookup "";
proxy_pass http://127.0.0.1/$dnslink/$request_uri;
}
location @base32_subdomain {
include /etc/nginx/conf.d/include/proxy-buffer;

View File

@ -0,0 +1 @@
/package.json

View File

@ -0,0 +1,3 @@
{
"printWidth": 120
}

View File

@ -0,0 +1,10 @@
FROM node:16.3.0-alpine
WORKDIR /usr/app
COPY package.json .
RUN yarn --no-lockfile
COPY src/* src/
EXPOSE 3100
CMD node src

View File

@ -0,0 +1,14 @@
{
"name": "dnslink-api",
"version": "1.0.0",
"main": "index.js",
"license": "SEE LICENSE IN LICENSE.md",
"dependencies": {
"express": "^4.17.1",
"is-valid-domain": "^0.0.20",
"node-cache": "^5.1.2"
},
"devDependencies": {
"prettier": "^2.2.1"
}
}

View File

@ -0,0 +1,80 @@
const dns = require("dns");
const express = require("express");
const NodeCache = require("node-cache");
const isValidDomain = require("is-valid-domain");
const host = process.env.DNSLINK_API_HOSTNAME || "0.0.0.0";
const port = Number(process.env.DNSLINK_API_PORT) || 3100;
const cacheTTL = Number(process.env.DNSLINK_API_CACHE_TTL) || 300; // default to 5 minutes
const server = express();
const cache = new NodeCache({ stdTTL: cacheTTL });
const dnslinkNamespace = "skynet-ns";
const dnslinkRegExp = new RegExp(`^dnslink=/${dnslinkNamespace}/.+$`);
const dnslinkSkylinkRegExp = new RegExp(`^dnslink=/${dnslinkNamespace}/([a-zA-Z0-9_-]{46}|[a-z0-9]{55})`);
const hint = `valid example: dnslink=/${dnslinkNamespace}/3ACpC9Umme41zlWUgMQh1fw0sNwgWwyfDDhRQ9Sppz9hjQ`;
server.get("/dnslink/:name", async (req, res) => {
const success = (skylink) => res.set("Skynet-Skylink", skylink).send(skylink);
const failure = (message) => res.status(400).set("Dnslink-Error", message).send(message);
if (!isValidDomain(req.params.name)) {
return failure(`"${req.params.name}" is not a valid domain`);
}
if (cache.has(req.params.name)) {
return success(cache.get(req.params.name));
}
const lookup = `_dnslink.${req.params.name}`;
dns.resolveTxt(lookup, (error, records) => {
if (error) {
if (error.code === "ENOTFOUND") {
return failure(`ENOTFOUND: ${lookup} TXT record doesn't exist`);
}
if (error.code === "ENODATA") {
return failure(`ENODATA: ${lookup} dns lookup returned no data`);
}
return failure(`Failed to fetch ${lookup} TXT record: ${error.message}`);
}
if (records.length === 0) {
return failure(`No TXT record found for ${lookup}`);
}
const dnslinks = records.flat().filter((record) => dnslinkRegExp.test(record));
if (dnslinks.length === 0) {
return failure(`TXT records for ${lookup} found but none of them contained valid skynet dnslink - ${hint}`);
}
if (dnslinks.length > 1) {
return failure(`Multiple TXT records with valid skynet dnslink found for ${lookup}, only one allowed`);
}
const [dnslink] = dnslinks;
const matchSkylink = dnslink.match(dnslinkSkylinkRegExp);
if (!matchSkylink) {
return failure(`TXT record with skynet dnslink for ${lookup} contains invalid skylink - ${hint}`);
}
const skylink = matchSkylink[1];
cache.set(req.params.name, skylink);
console.log(`${req.params.name} => ${skylink}`);
return success(skylink);
});
});
server.listen(port, host, (error) => {
if (error) throw error;
console.info(`Server listening at http://${host}:${port}`);
});