From a7c57b3c5af6aeed1ab95074235cb58d4a433be6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20Wypch=C5=82o?= Date: Mon, 27 Jul 2020 11:30:55 +0200 Subject: [PATCH] Handshake integration (#302) --- .eslintrc.json | 2 +- .gitignore | 1 + docker-compose.yml | 41 ++++++++++++ docker/caddy/Caddyfile | 2 + docker/handshake-api/Dockerfile | 20 ++++++ docker/handshake/Dockerfile | 10 +++ docker/nginx/conf.d/client.conf | 3 + handshake-api/index.js | 90 ++++++++++++++++++++++++++ setup-scripts/README.md | 1 + setup-scripts/setup-docker-services.sh | 3 +- 10 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 docker/handshake-api/Dockerfile create mode 100644 docker/handshake/Dockerfile create mode 100644 handshake-api/index.js diff --git a/.eslintrc.json b/.eslintrc.json index 58d80ffb..0831532b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -15,7 +15,7 @@ "ecmaFeatures": { "jsx": true }, - "ecmaVersion": 2018, + "ecmaVersion": 2020, "sourceType": "module" }, "plugins": ["react", "cypress"] diff --git a/.gitignore b/.gitignore index 6effe8d8..43f25aa7 100644 --- a/.gitignore +++ b/.gitignore @@ -77,3 +77,4 @@ docker/data # Cache files __pycache__ +/.idea/ diff --git a/docker-compose.yml b/docker-compose.yml index 22551a45..3c364523 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -51,6 +51,47 @@ services: depends_on: - docker-host + handshake: + build: + context: ./docker/handshake + dockerfile: Dockerfile + container_name: handshake + restart: on-failure + environment: + - HSD_HTTP_HOST=0.0.0.0 + - HSD_NETWORK=main + - HSD_PORT=12037 + env_file: + - .env + volumes: + - ./docker/data/handshake/.hsd:/root/.hsd + networks: + - shared + expose: + - 12037 + + handshake-api: + build: + context: . + dockerfile: ./docker/handshake-api/Dockerfile + container_name: handshake-api + restart: on-failure + networks: + - shared + environment: + - HSD_HOST=handshake + - HSD_NETWORK=main + - HSD_PORT=12037 + - HOST=0.0.0.0 + - NODE_TLS_REJECT_UNAUTHORIZED=0 + env_file: + - .env + expose: + - 3100 + depends_on: + - handshake + - caddy + health-check: build: context: . diff --git a/docker/caddy/Caddyfile b/docker/caddy/Caddyfile index e1868172..9a0aacd7 100644 --- a/docker/caddy/Caddyfile +++ b/docker/caddy/Caddyfile @@ -37,6 +37,8 @@ } reverse_proxy /health-check health-check:3100 + reverse_proxy /hns/* handshake-api:3100 + reverse_proxy /hnsres/* handshake-api:3100 reverse_proxy @blacklist nginx:80 { header_up User-Agent Sia-Agent diff --git a/docker/handshake-api/Dockerfile b/docker/handshake-api/Dockerfile new file mode 100644 index 00000000..ed4ed4bf --- /dev/null +++ b/docker/handshake-api/Dockerfile @@ -0,0 +1,20 @@ +FROM node:14.5 + +WORKDIR /usr/app + +RUN yarn init -y && \ + yarn add express express-http-proxy hs-client + +COPY handshake-api/index.js ./ + +ENV HOST="localhost" +ENV PORT=3100 + +ENV HSD_NETWORK="main" +ENV HSD_HOST="0.0.0.0" +ENV HSD_PORT=12037 +ENV HSD_API_KEY="foo" + +EXPOSE $PORT + +ENTRYPOINT ["node", "index.js"] diff --git a/docker/handshake/Dockerfile b/docker/handshake/Dockerfile new file mode 100644 index 00000000..5ccade07 --- /dev/null +++ b/docker/handshake/Dockerfile @@ -0,0 +1,10 @@ +FROM node:14.5 + +WORKDIR /opt/hsd + +RUN git clone https://github.com/handshake-org/hsd.git /opt/hsd && \ + npm install --production + +ENV PATH="${PATH}:/opt/hsd/bin:/opt/hsd/node_modules/.bin" + +ENTRYPOINT ["hsd"] diff --git a/docker/nginx/conf.d/client.conf b/docker/nginx/conf.d/client.conf index d5127539..262db21e 100644 --- a/docker/nginx/conf.d/client.conf +++ b/docker/nginx/conf.d/client.conf @@ -41,6 +41,9 @@ server { client_body_buffer_size 128k; client_max_body_size 128k; + rewrite "^(/([a-zA-Z0-9-_]{46})[^/]{0})$" $1/ permanent; + rewrite "^(/hns/[^/]+)[^/]{0}$" $1/ permanent; + location /blacklist { proxy_cache skynet; proxy_cache_valid any 1m; # cache blacklist for 1 minute diff --git a/handshake-api/index.js b/handshake-api/index.js new file mode 100644 index 00000000..5538d71b --- /dev/null +++ b/handshake-api/index.js @@ -0,0 +1,90 @@ +const express = require("express"); +const proxy = require("express-http-proxy"); +const { NodeClient } = require("hs-client"); + +const host = process.env.HOST || "localhost"; +const port = Number(process.env.PORT) || 3100; + +const hsdNetworkType = process.env.HSD_NETWORK || "regtest"; +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); + +const resolveDomain = async (name) => { + const response = await client.execute("getnameresource", [name]); + + if (!response) throw new Error("API not responding"); + + console.log(`${name} => ${JSON.stringify(response.records)}`); + + return response; +}; + +const findSkylinkRecord = (records) => { + return records?.find(({ txt }) => txt?.some((entry) => isValidSkylink(entry))); +}; + +const getSkylinkFromRecord = (record) => { + return record?.txt?.find((entry) => isValidSkylink(entry)); +}; + +const resolveDomainHandler = async (req, res) => { + try { + const response = await resolveDomain(req.params.name); + const record = findSkylinkRecord(response.records); + if (!record) return res.status(404).send(`No skylink found for ${req.params.name}`); + const skylink = getSkylinkFromRecord(record); + return res.json({ skylink }); + } catch (error) { + res.status(500).send(`Handshake error: ${error.message}`); + } +}; + +const SIA_LINK_RE = /^([a-zA-Z0-9-_]{46}.*)$/; + +// Checks if the given string is a valid Sia link. +function isValidSkylink(link) { + if (!link || link.length === 0) { + return false; + } + return Boolean(link.match(SIA_LINK_RE)); +} + +const server = express(); + +server.use( + "/hns/:name", + proxy("nginx", { + proxyReqPathResolver: async (req) => { + const response = await resolveDomain(req.params.name); + const record = findSkylinkRecord(response.records); + if (!record) throw new Error(`No skylink found for ${req.params.name}`); + const skylink = getSkylinkFromRecord(record); + + // if this is exact domain call, do not append anything to skylink entry + if (req.url === "" || req.url === "/") return `/${skylink}`; + + // drop any index.html or trailing slash from the skylink entry + const path = skylink.split("/").slice(0, -1).join("/"); + + return `/${path}${req.url}`; + }, + }) +); + +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}`); +}); diff --git a/setup-scripts/README.md b/setup-scripts/README.md index b55efb90..9ae93b75 100644 --- a/setup-scripts/README.md +++ b/setup-scripts/README.md @@ -98,6 +98,7 @@ At this point we have almost everything set up. We have 2 siad instances running - `EMAIL_ADDRESS` (required) is your email address used for communication regarding SSL certification (required) - `SIA_API_AUTHORIZATION` (required) is token you just generated in the previous point - `CLOUDFLARE_AUTH_TOKEN` (optional) if using cloudflare as dns loadbalancer (it's just for siasky.net configuration) + - `HSD_API_KEY` (optional) this is a random security key for an optional handshake integration that gets generated automatically 1. if you have a custom domain and you configured it in `DOMAIN_NAME`, edit `/home/user/skynet-webportal/docker/caddy/Caddyfile` and uncomment `import custom.domain` 1. only for siasky.net domain instances: edit `/home/user/skynet-webportal/docker/caddy/Caddyfile`, uncomment `import siasky.net` 1. `sudo docker-compose up -d` to restart the services so they pick up new env variables diff --git a/setup-scripts/setup-docker-services.sh b/setup-scripts/setup-docker-services.sh index 586323dd..26cd6c95 100755 --- a/setup-scripts/setup-docker-services.sh +++ b/setup-scripts/setup-docker-services.sh @@ -23,7 +23,8 @@ docker-compose --version # sanity check # SIA_API_AUTHORIZATION - the base64 encoded :apipassword string # CLOUDFLARE_AUTH_TOKEN - cloudflare auth token for ssl generation (just for siasky.net) if ! [ -f /home/user/skynet-webportal/.env ]; then - printf "DOMAIN_NAME=example.com\nEMAIL_ADDRESS=email@example.com\nSIA_API_AUTHORIZATION=\nCLOUDFLARE_AUTH_TOKEN=\n" > /home/user/skynet-webportal/.env + HSD_API_KEY=$(openssl rand -base64 32) # generate safe random key for handshake + printf "DOMAIN_NAME=example.com\nEMAIL_ADDRESS=email@example.com\nSIA_API_AUTHORIZATION=\nCLOUDFLARE_AUTH_TOKEN=\nHSD_API_KEY=${HSD_API_KEY}\n" > /home/user/skynet-webportal/.env fi # Start docker container with nginx and client