Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 0 additions & 25 deletions .eslintrc

This file was deleted.

7 changes: 5 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npx @dappnode/dappnodesdk github-action build
- uses: actions/checkout@v7.0.1
- uses: actions/setup-node@v7.1.0
with:
node-version-file: ".nvmrc"
- run: npx --yes @dappnode/dappnodesdk@0.3.55 github-action build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PINATA_API_KEY: ${{ secrets.PINATA_API_KEY }}
Expand Down
19 changes: 11 additions & 8 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,31 @@ jobs:
runs-on: ubuntu-latest
name: Unit tests
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7.0.1
- name: Setup node
uses: actions/setup-node@v1
uses: actions/setup-node@v7.1.0
with:
node-version: "19.x"
- run: yarn
node-version-file: ".nvmrc"
- run: npm install --global yarn@1.22.22
- run: yarn install --frozen-lockfile --non-interactive
- run: yarn build
- run: yarn lint
- run: yarn lint:ejs
- run: yarn test

release:
name: Release
runs-on: ubuntu-latest
needs: [unit-test]
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v7.0.1
- name: Setup node
uses: actions/setup-node@v1
uses: actions/setup-node@v7.1.0
with:
node-version: "18.x"
node-version-file: ".nvmrc"
- name: Publish
run: npx @dappnode/dappnodesdk publish patch --dappnode_team_preset
run: npx --yes @dappnode/dappnodesdk@0.3.55 publish patch --dappnode_team_preset
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DEVELOPER_ADDRESS: "0xf35960302a07022aba880dffaec2fdd64d5bf1c1"
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
26.12.1
23 changes: 16 additions & 7 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
FROM node:19.1 as builder
FROM node:26.12.1-alpine3.24 AS base

# RHSB-2026-012: refresh the OS trust store as well as Node's bundled CAs.
RUN apk upgrade --no-cache \
&& apk add --no-cache "ca-certificates>=20261020-r0" \
&& update-ca-certificates \
&& npm install --global yarn@1.22.22

FROM base AS builder

WORKDIR /usr/src/app

COPY *.json yarn.lock ./
COPY package.json tsconfig.json yarn.lock ./

RUN yarn
RUN yarn install --frozen-lockfile --non-interactive

COPY ./src ./src
RUN yarn build

FROM node:19.1-alpine
RUN apk add nginx openssl
FROM base AS runtime
RUN apk add --no-cache nginx openssl

ENV DOMAINS_DIR=/usr/src/app/domains \
DAPPMANAGER_SIGN=http://my.dappnode/sign \
Expand All @@ -23,9 +31,10 @@ COPY package.json yarn.lock ./
COPY ./templates ./templates
COPY ./snippets /etc/nginx/snippets
COPY ./static /usr/share/nginx/html
RUN yarn --production
RUN yarn install --frozen-lockfile --production --non-interactive \
&& yarn cache clean

COPY --from=builder /usr/src/app/build/ ./build/

EXPOSE 8545
CMD [ "node", "build/index" ]
CMD [ "node", "build/index" ]
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,34 @@
This package serves as proxy server to DAppNodes packages.
<p align="center"><a href="#"><img width="150" title="HTTPS" src='https-portal-avatar.png' /></a></p>

## Development and security updates

Use Node.js `26.12.1` (see `.nvmrc`) and Yarn Classic `1.22.22`:

```sh
nvm use
npm install --global yarn@1.22.22
yarn install --frozen-lockfile
yarn build
yarn lint
yarn lint:ejs
yarn test
```

TypeScript is kept on `6.0.3`, the latest release supported by the current
TypeScript ESLint parser (`>=4.8.4 <6.1.0`).

For [RHSB-2026-012](https://access.redhat.com/security/vulnerabilities/RHSB-2026-012),
the Docker image uses patched Node.js `26.12.1` and requires Alpine's
`ca-certificates` package dated `20261020` or newer. This refreshes both Node's
bundled trust store and the operating system's CA bundle. Rebuild and redeploy
the package to apply these updates to running containers. To refresh the base
image and installed Alpine packages during a local build:

```sh
docker build --pull --no-cache -t dnp-https:local .
```

## Configurable forwarding

API was added through which containers proxied through the package can be dynamically chosen. By default the server listens at port `5000` for `GET` methods `add` and `remove` both of which have `to` and `from` query parameters i.e.
Expand Down
16 changes: 16 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import tsPlugin from "@typescript-eslint/eslint-plugin";
import globals from "globals";

export default [
...tsPlugin.configs["flat/recommended"],
{
files: ["src/**/*.ts"],
languageOptions: {
ecmaVersion: "latest",
globals: globals.node,
},
rules: {
"max-len": ["error", 1000],
},
},
];
43 changes: 24 additions & 19 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,35 @@
"main": "bin/index.js",
"license": "MIT",
"dependencies": {
"axios": "^1.2.0",
"ejs": "^3.1.8",
"express": "^4.18.2",
"morgan": "^1.10.0"
"axios": "^1.20.0",
"ejs": "^7.0.2",
"express": "^5.3.0",
"morgan": "^1.12.1"
},
"devDependencies": {
"@types/ejs": "^3.1.1",
"@types/express": "^4.17.14",
"@types/morgan": "^1.9.3",
"@types/node": "^18.11.9",
"@typescript-eslint/eslint-plugin": "^5.54.0",
"@typescript-eslint/parser": "^5.54.0",
"ejs-lint": "^2.0.0",
"eslint": "^8.35.0",
"prettier": "^2.8.0",
"ts-node": "^10.9.1",
"typescript": "^4.9.3"
"@types/ejs": "^3.1.5",
"@types/express": "^5.0.6",
"@types/morgan": "^1.9.10",
"@types/node": "^26.6.5",
"@typescript-eslint/eslint-plugin": "^8.72.0",
"@typescript-eslint/parser": "^8.72.0",
"eslint": "^10.13.0",
"globals": "^17.13.0",
"prettier": "^3.9.10",
"ts-node": "^10.9.2",
"typescript": "~6.0.3"
},
"scripts": {
"build": "tsc",
"start": "node ./build/index.js",
"dev": "ts-node ./src/index.ts",
"lint:ejs": "ejslint ./templates/*.ejs",
"lint": "eslint . --ext .ts --fix",
"prettier": "prettier --write 'src/**/*.*'"
}
"lint:ejs": "node scripts/lint-ejs.cjs",
"lint": "eslint src --max-warnings 0",
"prettier": "prettier --write 'src/**/*.*'",
"test": "node --test test/*.test.cjs"
},
"engines": {
"node": ">=26.12.1 <27"
},
"packageManager": "yarn@1.22.22"
}
4 changes: 2 additions & 2 deletions releases.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
}
},
"0.2.0": {
"hash": "/ipfs/QmU1KRM9s2Y6GrJXHE1Wzs1ZyfUqoWYe8xnApJB8uZKjwX",
"hash": "/ipfs/QmXMye5CYwxs8EC9twTEXvRWyjJf3kBvggzRho2M8mft49",
"uploadedTo": {
"remote": "Wed, 15 Feb 2023 08:54:51 GMT"
"dappnode": "Fri, 23 Oct 2026 08:08:25 GMT"
}
}
}
7 changes: 7 additions & 0 deletions scripts/lint-ejs.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const fs = require("node:fs");
const ejs = require("ejs");

for (const filename of fs.globSync("templates/**/*.ejs")) {
ejs.compile(fs.readFileSync(filename, "utf8"), { filename });
console.log(`Checked ${filename}`);
}
17 changes: 12 additions & 5 deletions src/api/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,18 @@ function getHttpsApi(dappnodeDomain: string): Express {
entries.push({ from, to, external, auth });
entriesDb.write(entries);

const reconfigured = await reconfigureNGINX(dappnodeDomain);
if (!reconfigured) {
entriesDb.write(entries.filter((e) => e.from !== from)); // rollback
await reconfigureNGINX(dappnodeDomain);
throw new HttpError("Unable to add mapping", 500);
try {
if (!(await reconfigureNGINX(dappnodeDomain))) {
throw new HttpError("Unable to add mapping", 500);
}
} catch (error) {
entriesDb.write(entriesDb.read().filter((e) => e.from !== from));
try {
await reconfigureNGINX(dappnodeDomain);
} catch (rollbackError) {
console.warn("Failed to restore NGINX configuration", rollbackError);
}
throw error;
}
})
);
Expand Down
12 changes: 12 additions & 0 deletions src/api/utils/sanitize.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { config } from "../../config";
import { BadRequestError } from "./asyncHandler";
import { isIP } from "net";
/**
* from param must be a subdomain
*/
Expand All @@ -21,6 +22,17 @@ export async function sanitizeFrom(from: string): Promise<string> {
export function sanitizeTo(to: string): string {
try {
if (!to) throw Error("not defined");
if (typeof to !== "string") throw Error("Must be a host with an optional port");
const match = /^([a-z0-9_.-]+|\[[a-z0-9:.%_-]+\])(?::(\d+))?$/i.exec(to);
if (
!match || match[0] !== to ||
(match[1].startsWith("[") && isIP(match[1].slice(1, -1)) !== 6)
) {
throw Error("Must be a host with an optional port");
}
if (match[2] && (Number(match[2]) < 1 || Number(match[2]) > 65535)) {
throw Error("Port must be between 1 and 65535");
}
} catch (e) {
throw new BadRequestError(`Bad param 'to': ${e.message}`);
}
Expand Down
25 changes: 15 additions & 10 deletions src/certificates/apiSign.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { config } from "../config";
import fs from "fs";
import axios from "axios";
import shell from "../utils/shell";
import { createPrivateKey, X509Certificate } from "crypto";

async function getEthSignature(timestamp: number): Promise<[string, string]> {
const response = await axios.post(
Expand All @@ -25,24 +25,29 @@ async function executeAPISign(
force ? 1 : 0
}`;
const file = fs.createReadStream(config.csrPath);
const response = await axios.postForm(url, {
const response = await axios.postForm<string>(url, {
csr: file,
});
await fs.writeFileSync(config.certPath, response.data);
return response.data;
}

export default async function apiSign() {
const privateKey = createPrivateKey(fs.readFileSync(config.keyPath));
const timestamp = Math.floor(Date.now() / 1000);
const [signature, address] = await getEthSignature(timestamp);
const force = process.env["FORCE"] === "true" || false;

await executeAPISign(signature, address, timestamp, force);
const cert_pubkey = shell(
`openssl x509 -pubkey -noout -in ${config.certPath}`
);
const priv_pubkey = shell(`openssl rsa -in ${config.keyPath} -pubout`);
if (cert_pubkey !== priv_pubkey) {
let certificate = await executeAPISign(signature, address, timestamp, force);
if (!new X509Certificate(certificate).checkPrivateKey(privateKey) && !force) {
console.warn("Keys do not match, trying forcing certification service");
await executeAPISign(signature, address, timestamp, true);
certificate = await executeAPISign(signature, address, timestamp, true);
}
if (!new X509Certificate(certificate).checkPrivateKey(privateKey)) {
throw new Error("Signed certificate does not match the domain private key");
}

// Keep the current certificate intact until a matching replacement is ready.
const pendingPath = `${config.certPath}.tmp`;
fs.writeFileSync(pendingPath, certificate);
fs.renameSync(pendingPath, config.certPath);
}
11 changes: 10 additions & 1 deletion src/certificates/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { config } from "../config";
import apiSign from "./apiSign";
import shell from "../utils/shell";
import {
createCSR,
generateDHParam,
Expand All @@ -11,8 +12,10 @@ import {
export async function ensureValidCert(createIfNotExists = false) {
if (await shouldRenew(createIfNotExists)) {
await signCert();
return true;
} else {
console.log("Certificate valid or not necessary, skip signing");
return false;
}
}

Expand All @@ -33,9 +36,15 @@ export default async function initCertificateProvider(dappnodeDomain: string) {
}

console.log("Setting cronjob for cert updates");
let reloadPending = false;
setInterval(async () => {
try {
await ensureValidCert();
const renewed = await ensureValidCert();
reloadPending = renewed || reloadPending;
if (reloadPending) {
await shell("nginx -s reload");
reloadPending = false;
}
} catch (e) {
console.warn(e);
}
Expand Down
1 change: 0 additions & 1 deletion src/certificates/openssl.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import path from "path";
import { config } from "../config";
import shell from "../utils/shell";
import fs from "fs";
Expand Down
Loading
Loading