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
8 changes: 6 additions & 2 deletions packages/api/src/keymanager/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
MAX_BUILDER_URL_SIZE,
} from "@lodestar/params";
import {Epoch, phase0, ssz, stringType} from "@lodestar/types";
import {isValidAsciiHttpUrl} from "@lodestar/utils";
import {isValidAsciiHttpUrl, toAsciiHttpUrl} from "@lodestar/utils";
import {
EmptyArgs,
EmptyMeta,
Expand Down Expand Up @@ -194,7 +194,11 @@ export function builderConfigDataFromJson(json: unknown): BuilderConfigData {
throw Error(`builders[${i}].url must not exceed ${MAX_BUILDER_URL_SIZE} bytes`);
}
if (!isValidAsciiHttpUrl(url)) {
throw Error(`builders[${i}].url must be a valid HTTP or HTTPS URL using only ASCII characters`);
const asciiUrl = toAsciiHttpUrl(url);
throw Error(
`builders[${i}].url must be a valid HTTP or HTTPS URL using only ASCII characters` +
(asciiUrl !== null ? `, use ${asciiUrl} instead` : "")
);
}
if (auth_data !== undefined && (typeof auth_data !== "string" || !BUILDER_AUTH_DATA_PATTERN.test(auth_data))) {
throw Error(
Expand Down
6 changes: 6 additions & 0 deletions packages/api/test/unit/keymanager/builderConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,10 @@ describe("builderConfigDataFromJson", () => {
);
}
});

it("suggests the ASCII form of a builder url with an internationalized hostname", () => {
expect(() => builderConfigDataFromJson({builders: [{url: "https://platåberget.dev"}]})).toThrow(
"use https://xn--platberget-45a.dev/ instead"
);
});
});
5 changes: 3 additions & 2 deletions packages/cli/src/util/proposerConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import path from "node:path";
import {routes} from "@lodestar/api";
import {BuilderEntryConfig, builderConfigDataFromJson} from "@lodestar/api/keymanager";
import {MAX_BUILDER_AUTH_DATA_SIZE, MAX_BUILDER_ENTRIES, MAX_BUILDER_URL_SIZE} from "@lodestar/params";
import {fromHex, isValidAsciiHttpUrl, toHex} from "@lodestar/utils";
import {fromHex, isValidAsciiHttpUrl, toAsciiHttpUrl, toHex} from "@lodestar/utils";
import {ValidatorProposerConfig} from "@lodestar/validator";
import {parseFeeRecipient} from "./feeRecipient.js";
import {readFile} from "./file.js";
Expand Down Expand Up @@ -252,7 +252,8 @@ export function parseBuilderUrls(urls?: string[]): BuilderEntryConfig[] | undefi
const url = fragmentIndex === -1 ? value : value.slice(0, fragmentIndex);
const authData = fragmentIndex === -1 ? undefined : value.slice(fragmentIndex + 1);
if (!isValidAsciiHttpUrl(url)) {
throw Error(`Invalid builder url: ${url}`);
const asciiUrl = toAsciiHttpUrl(url);
throw Error(`Invalid builder url: ${url}` + (asciiUrl !== null ? `, use ${asciiUrl} instead` : ""));
}
if (Buffer.byteLength(url, "utf8") > MAX_BUILDER_URL_SIZE) {
throw Error(`Invalid builder url, must not exceed ${MAX_BUILDER_URL_SIZE} bytes: ${url}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe("validator / parseBuilderUrls", () => {
it("rejects invalid urls and auth data", () => {
expect(() => parseBuilderUrls(["builder.example.com"])).toThrow(/Invalid builder url/);
expect(() => parseBuilderUrls(["https://builder.example.com/é"])).toThrow(/Invalid builder url/);
expect(() => parseBuilderUrls(["https://platåberget.dev"])).toThrow("use https://xn--platberget-45a.dev/ instead");
expect(() => parseBuilderUrls(["https://builder.example.com#"])).toThrow(/auth data/);
expect(() => parseBuilderUrls(["https://builder.example.com#0x"])).toThrow(/auth data/);
expect(() => parseBuilderUrls(["https://builder.example.com#secret"])).toThrow(/auth data/);
Expand Down
10 changes: 10 additions & 0 deletions packages/utils/src/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ export function isValidAsciiHttpUrl(urlStr: string): boolean {
return isValidHttpUrl(urlStr);
}

/** Return the ASCII form of an HTTP(S) URL with its hostname in punycode, or null if it has none. */
export function toAsciiHttpUrl(urlStr: string): string | null {
if (!isValidHttpUrl(urlStr)) {
return null;
}

const asciiUrl = new URL(urlStr).href;
return isValidAsciiHttpUrl(asciiUrl) ? asciiUrl : null;
}

/**
* Sanitize URL to prevent leaking user credentials in logs or metrics
*
Expand Down
18 changes: 18 additions & 0 deletions packages/utils/test/unit/url.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {describe, expect, it} from "vitest";
import {toAsciiHttpUrl} from "../../src/url.js";

describe("toAsciiHttpUrl", () => {
const testCases: {url: string; expected: string | null}[] = [
{url: "https://platåberget.dev", expected: "https://xn--platberget-45a.dev/"},
{url: "https://builder.example.com/é?x=1", expected: "https://builder.example.com/%C3%A9?x=1"},
{url: "https://builder.example.com:8080/path", expected: "https://builder.example.com:8080/path"},
{url: "ftp://builder.example.com", expected: null},
{url: "builder.example.com", expected: null},
];

for (const {url, expected} of testCases) {
it(url, () => {
expect(toAsciiHttpUrl(url)).toBe(expected);
});
}
});
Loading