2020-07-27 09:30:55 +00:00
|
|
|
const express = require("express");
|
2020-10-14 10:55:28 +00:00
|
|
|
const punycode = require("punycode");
|
2020-09-03 13:08:55 +00:00
|
|
|
const NodeCache = require("node-cache");
|
2020-07-27 09:30:55 +00:00
|
|
|
const { NodeClient } = require("hs-client");
|
|
|
|
|
2020-07-30 10:00:58 +00:00
|
|
|
const host = process.env.HOSTNAME || "0.0.0.0";
|
2020-07-27 09:30:55 +00:00
|
|
|
const port = Number(process.env.PORT) || 3100;
|
|
|
|
|
2020-11-03 15:18:23 +00:00
|
|
|
const hsdNetworkType = process.env.HSD_NETWORK || "main";
|
2020-07-27 09:30:55 +00:00
|
|
|
const hsdHost = process.env.HSD_HOST || "localhost";
|
|
|
|
const hsdPort = Number(process.env.HSD_PORT) || 12037;
|
|
|
|
const hsdApiKey = process.env.HSD_API_KEY || "foo";
|
|
|
|
|
|
|
|
const clientOptions = {
|
|
|
|
network: hsdNetworkType,
|
|
|
|
host: hsdHost,
|
|
|
|
port: hsdPort,
|
|
|
|
apiKey: hsdApiKey,
|
|
|
|
};
|
|
|
|
const client = new NodeClient(clientOptions);
|
2020-11-03 15:27:38 +00:00
|
|
|
const cache = new NodeCache({ stdTTL: 300 }); // cache for 5 minutes
|
2020-07-27 09:30:55 +00:00
|
|
|
|
2020-08-05 14:20:29 +00:00
|
|
|
// Match both `sia://HASH` and `HASH` links.
|
2020-08-07 10:03:32 +00:00
|
|
|
const startsWithSkylinkRegExp = /^(sia:\/\/)?[a-zA-Z0-9_-]{46}/;
|
2020-11-09 11:18:35 +00:00
|
|
|
const registryEntryRegExp = /^skyns:\/\/(?<publickey>[a-zA-Z0-9%]+)\/(?<datakey>[a-zA-Z0-9%]+)$/;
|
2020-07-27 09:30:55 +00:00
|
|
|
|
2020-07-30 22:50:20 +00:00
|
|
|
const getDomainRecords = async (name) => {
|
2020-09-03 13:08:55 +00:00
|
|
|
if (cache.has(name)) return cache.get(name);
|
|
|
|
|
2020-07-30 22:50:20 +00:00
|
|
|
const response = await client.execute("getnameresource", [name]);
|
|
|
|
const records = response?.records ?? null;
|
2020-07-27 09:30:55 +00:00
|
|
|
|
2020-07-30 22:50:20 +00:00
|
|
|
console.log(`${name} => ${JSON.stringify(records)}`);
|
2020-07-27 09:30:55 +00:00
|
|
|
|
2020-09-03 13:08:55 +00:00
|
|
|
cache.set(name, records);
|
|
|
|
|
2020-07-30 22:50:20 +00:00
|
|
|
return records;
|
2020-07-27 09:30:55 +00:00
|
|
|
};
|
|
|
|
|
2020-11-03 15:18:23 +00:00
|
|
|
const findSkynetCompatibleRecord = (records) => {
|
2020-08-05 14:32:51 +00:00
|
|
|
// Find the last one, so people can update their domains in a non-destructive
|
|
|
|
// way by simply adding a new link. This will also allow keeping links to
|
|
|
|
// older versions for backwards compatibility.
|
|
|
|
return records
|
|
|
|
?.slice()
|
|
|
|
.reverse()
|
2020-11-03 15:18:23 +00:00
|
|
|
.find(({ txt }) => txt?.some((entry) => isValidSkylink(entry) || isValidRegistryEntry(entry)));
|
2020-07-27 09:30:55 +00:00
|
|
|
};
|
|
|
|
|
2020-11-03 15:18:23 +00:00
|
|
|
const createResponseFromCompatibleRecord = (record) => {
|
|
|
|
const skylink = record?.txt?.find((entry) => isValidSkylink(entry));
|
|
|
|
|
|
|
|
if (skylink) return { skylink };
|
|
|
|
|
|
|
|
const entry = record?.txt?.find((entry) => isValidRegistryEntry(entry));
|
|
|
|
|
|
|
|
if (entry) {
|
|
|
|
const match = entry.match(registryEntryRegExp);
|
|
|
|
|
|
|
|
if (match)
|
|
|
|
return {
|
|
|
|
registry: {
|
|
|
|
publickey: decodeURIComponent(match.groups.publickey),
|
|
|
|
datakey: decodeURIComponent(match.groups.datakey),
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new Error(`No skylink in record: ${JSON.stringify(record)}`);
|
2020-07-27 09:30:55 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
const resolveDomainHandler = async (req, res) => {
|
|
|
|
try {
|
2020-10-14 10:55:28 +00:00
|
|
|
const domain = punycode.toASCII(req.params.name);
|
|
|
|
const records = await getDomainRecords(domain);
|
|
|
|
if (!records) return res.status(404).send(`No records found for ${domain}`);
|
2020-07-30 22:50:20 +00:00
|
|
|
|
2020-11-03 15:18:23 +00:00
|
|
|
const record = findSkynetCompatibleRecord(records);
|
|
|
|
if (!record) throw new Error(`No skynet compatible records found in dns records of ${domain}`);
|
2020-07-30 22:50:20 +00:00
|
|
|
|
2020-11-03 15:18:23 +00:00
|
|
|
return res.json(createResponseFromCompatibleRecord(record));
|
2020-07-27 09:30:55 +00:00
|
|
|
} catch (error) {
|
|
|
|
res.status(500).send(`Handshake error: ${error.message}`);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Checks if the given string is a valid Sia link.
|
2020-11-03 15:30:34 +00:00
|
|
|
function isValidSkylink(value) {
|
|
|
|
return Boolean(value && value.match(startsWithSkylinkRegExp));
|
2020-07-27 09:30:55 +00:00
|
|
|
}
|
|
|
|
|
2020-11-03 15:30:34 +00:00
|
|
|
// Checks if given string is a valid skynet registry entry
|
2020-11-03 15:18:23 +00:00
|
|
|
function isValidRegistryEntry(value) {
|
|
|
|
return Boolean(value && value.match(registryEntryRegExp));
|
|
|
|
}
|
|
|
|
|
2020-07-27 09:30:55 +00:00
|
|
|
const server = express();
|
|
|
|
|
|
|
|
server.get("/hnsres/:name", resolveDomainHandler);
|
|
|
|
|
|
|
|
server.listen(port, host, (error) => {
|
|
|
|
if (error) throw error;
|
|
|
|
|
|
|
|
console.info(`API will look for HSD Server at ${hsdHost}:${hsdPort}, "${hsdNetworkType}" network.`);
|
|
|
|
console.info(`Server listening at http://${host}:${port}`);
|
|
|
|
});
|