diff --git a/CHANGES.md b/CHANGES.md index 834b45965..089e42898 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,16 @@ Version 2.0.33 To be released. +### @fedify/cli + + - Fixed `fedify webfinger` ignoring `-p`/`--allow-private-address` and the + `webfinger.allowPrivateAddress` configuration setting, preventing lookups + against local development servers even with an explicit opt-in. Private + addresses remain blocked by default. [[#1274], [#1276]] + +[#1274]: https://github.com/fedify-dev/fedify/issues/1274 +[#1276]: https://github.com/fedify-dev/fedify/pull/1276 + Version 2.0.32 -------------- diff --git a/changes.d/cli/webfinger-private-address.md b/changes.d/cli/webfinger-private-address.md new file mode 100644 index 000000000..6b969c392 --- /dev/null +++ b/changes.d/cli/webfinger-private-address.md @@ -0,0 +1,9 @@ +--- +links: + '#1274': https://github.com/fedify-dev/fedify/issues/1274 + '#1276': https://github.com/fedify-dev/fedify/pull/1276 +--- + - Fixed `fedify webfinger` ignoring `-p`/`--allow-private-address` and the + `webfinger.allowPrivateAddress` configuration setting, preventing lookups + against local development servers even with an explicit opt-in. Private + addresses remain blocked by default. [[#1274], [#1276]] diff --git a/packages/cli/src/webfinger/action.ts b/packages/cli/src/webfinger/action.ts index 8a4d98aa0..1a3c6ddf9 100644 --- a/packages/cli/src/webfinger/action.ts +++ b/packages/cli/src/webfinger/action.ts @@ -12,10 +12,15 @@ import { getErrorMessage, NotFoundError } from "./error.ts"; import { convertUrlIfHandle } from "./lib.ts"; export default async function runWebFinger( - { command: _, resources, ...options }: WebFingerCommand, + { command: _, resources, allowPrivateAddresses, ...options }: + WebFingerCommand, ) { await Array.fromAsync( - resources.map((resource) => ({ resource, ...options })), + resources.map((resource) => ({ + resource, + ...options, + allowPrivateAddress: allowPrivateAddresses, + })), spinnerWrapper(lookupSingleWebFinger), ); } diff --git a/packages/cli/src/webfinger/mod.test.ts b/packages/cli/src/webfinger/mod.test.ts index fedca199f..56bc8c448 100644 --- a/packages/cli/src/webfinger/mod.test.ts +++ b/packages/cli/src/webfinger/mod.test.ts @@ -1,9 +1,11 @@ import { clearActiveConfig, setActiveConfig } from "@optique/config"; +import { runWithConfig } from "@optique/config/run"; import { parse } from "@optique/core/parser"; import assert from "node:assert/strict"; +import process from "node:process"; import test from "node:test"; -import { configContext } from "../config.ts"; -import { lookupSingleWebFinger } from "./action.ts"; +import { type Config, configContext } from "../config.ts"; +import runWebFinger, { lookupSingleWebFinger } from "./action.ts"; import { webFingerCommand } from "./command.ts"; const COMMAND = "webfinger"; @@ -17,6 +19,94 @@ const ALIASES = [ "https://hollo.social/@fedify", ]; +for ( + const { name, args, config, allowed } of [ + { name: "short flag", args: ["-p"], config: {}, allowed: true }, + { + name: "long flag", + args: ["--allow-private-address"], + config: {}, + allowed: true, + }, + { + name: "configuration opt-in", + args: [], + config: { webfinger: { allowPrivateAddress: true } }, + allowed: true, + }, + { name: "default rejection", args: [], config: {}, allowed: false }, + { + name: "configuration opt-out", + args: [], + config: { webfinger: { allowPrivateAddress: false } }, + allowed: false, + }, + { + name: "flag overrides configuration opt-out", + args: ["-p"], + config: { webfinger: { allowPrivateAddress: false } }, + allowed: true, + }, + ] satisfies { + name: string; + args: string[]; + config: Config; + allowed: boolean; + }[] +) { + test(`runWebFinger private address - ${name}`, async () => { + const resource = "http://127.0.0.1/users/alice"; + const descriptor = { subject: resource, aliases: [resource] }; + const requests: { url: string; userAgent: string | null }[] = []; + const originalFetch = globalThis.fetch; + const originalWrite = process.stdout.write; + let output = ""; + globalThis.fetch = (input, init) => { + requests.push({ + url: String(input), + userAgent: new Headers(init?.headers).get("User-Agent"), + }); + return Promise.resolve( + new Response(JSON.stringify(descriptor), { + headers: { "Content-Type": "application/jrd+json" }, + }), + ); + }; + process.stdout.write = function (...args) { + output += String(args[0]); + return Reflect.apply(originalWrite, this, args); + }; + try { + const options = await runWithConfig(webFingerCommand, configContext, { + load: () => config, + args: [COMMAND, ...args, "-u", USER_AGENT, resource], + }); + await runWebFinger(options); + } finally { + globalThis.fetch = originalFetch; + process.stdout.write = originalWrite; + } + // Assert after execution: lookup and spinner error handling swallow errors + // thrown inside fetch, so assertions in the mock would not fail the test. + assert.deepEqual( + requests, + allowed + ? [{ + url: "http://127.0.0.1/.well-known/webfinger?resource=" + + encodeURIComponent(resource), + userAgent: USER_AGENT, + }] + : [], + ); + if (allowed) { + assert.ok(output.includes(resource), output); + assert.ok(output.includes("subject"), output); + } else { + assert.strictEqual(output, ""); + } + }); +} + test("Test webFingerCommand - resources only", async () => { const argsWithResourcesOnly = [COMMAND, ...RESOURCES]; setActiveConfig(configContext.id, {});