Repository navigation
feat(reachability): add the setup command with IP certificates #205
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
b1996fa
fix(daemon): read the kubo config from the repo kubo actually runs in
Rinse12 1555fef
feat(reachability): add the pure kubo config transform
Rinse12 379ee1b
fix(daemon): keep env overrides and connect to IPv6 kubo addresses
Rinse12 6ed4c24
fix(reachability): restore null kubo sections on disable
Rinse12 537eff1
feat(reachability): add versioned state and the status command
Rinse12 812bb36
fix(reachability): show only current certificates and validate runtim…
Rinse12 8e6a2f4
feat(reachability): add Caddy download, Caddyfile generation and Pebb…
Rinse12 cb41e25
fix(reachability): count only certificates from the configured CA
Rinse12 82d23d1
Merge branch 'fix/kubo-repo-path' into feat/reachability-setup
Rinse12 654557c
feat(reachability): add the setup command with IP certificates
Rinse12 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| import { spawn } from "child_process"; | ||
| import readline from "readline/promises"; | ||
| import { Command, Flags } from "@oclif/core"; | ||
| import defaults from "../../../common-utils/defaults.js"; | ||
| import { initKuboRepoIfNeeded, resolveKuboBinaryPath } from "../../../ipfs/startIpfs.js"; | ||
| import { detectContainer } from "../../../reachability/discovery.js"; | ||
| import { NeedsAnswerError, realSetupDependencies, runReachabilitySetup, type SetupOptions } from "../../../reachability/setup.js"; | ||
| import { loadKuboConfigFile, PKCLogger, resolveKuboRepoPath, resolveKuboRpcUrl } from "../../../util.js"; | ||
|
|
||
| // Test hooks: Pebble in place of Let's Encrypt, and loopback in place of a public address. | ||
| function testOverrides(env: NodeJS.ProcessEnv): SetupOptions["overrides"] { | ||
| const acmeDirectory = env["BITSOCIAL_TEST_REACHABILITY_ACME_DIRECTORY"]; | ||
| if (!acmeDirectory) return undefined; | ||
| const number = (value: string | undefined) => (value ? Number(value) : undefined); | ||
| const httpsPort = number(env["BITSOCIAL_TEST_REACHABILITY_HTTPS_PORT"]); | ||
| const httpPort = number(env["BITSOCIAL_TEST_REACHABILITY_HTTP_PORT"]); | ||
| const trustedRoots = env["BITSOCIAL_TEST_REACHABILITY_ACME_ROOT"]; | ||
| const publicIps = env["BITSOCIAL_TEST_REACHABILITY_PUBLIC_IPS"]; | ||
| return { | ||
| acmeDirectory, | ||
| ...(trustedRoots ? { acmeTrustedRootsPath: trustedRoots } : {}), | ||
| ...(publicIps ? { publicIps: publicIps.split(",") } : {}), | ||
| ...(httpsPort ? { httpsPort } : {}), | ||
| ...(httpPort ? { httpPort } : {}) | ||
| }; | ||
| } | ||
|
|
||
| const runSudoSetcap = (binaryPath: string) => | ||
| new Promise<boolean>((resolve) => { | ||
| const child = spawn("sudo", ["setcap", "cap_net_bind_service=+ep", binaryPath], { stdio: "inherit" }); | ||
| child.once("error", () => resolve(false)); | ||
| child.once("exit", (code) => resolve(code === 0)); | ||
| }); | ||
|
|
||
| async function confirmOnTerminal(question: string): Promise<boolean> { | ||
| const terminal = readline.createInterface({ input: process.stdin, output: process.stdout }); | ||
| try { | ||
| return /^y(es)?$/i.test((await terminal.question(`${question} [y/N] `)).trim()); | ||
| } finally { | ||
| terminal.close(); | ||
| } | ||
| } | ||
|
|
||
| export default class Setup extends Command { | ||
| static override description = | ||
| "Make this node dialable from browsers: discover public addresses, obtain a certificate, configure kubo and verify. Safe to rerun"; | ||
|
|
||
| static override flags = { | ||
| "pkcOptions.dataPath": Flags.directory({ description: "Data path of the node", required: false }), | ||
| staging: Flags.boolean({ description: "Use the Let's Encrypt staging CA. Its certificates are not trusted by browsers", default: false }), | ||
| "non-interactive": Flags.boolean({ description: "Never ask. Exit non-zero with the reason if an answer would be needed", default: false }) | ||
| }; | ||
|
|
||
| static override examples = ["bitsocial reachability setup", "bitsocial reachability setup --staging", "bitsocial reachability setup --non-interactive"]; | ||
|
|
||
| async run(): Promise<void> { | ||
| const { flags } = await this.parse(Setup); | ||
| const dataPath = flags["pkcOptions.dataPath"] || defaults.PKC_DATA_PATH; | ||
| const log = PKCLogger("bitsocial-cli:reachability:setup"); | ||
| const kuboRpcUrl = await resolveKuboRpcUrl(dataPath, process.env["KUBO_RPC_URL"], defaults.KUBO_RPC_URL); | ||
| const gatewayUrl = defaults.IPFS_GATEWAY_URL; | ||
| // A running daemon holds the repo lock, and `ipfs init` errors out on it, so only init a missing repo. | ||
| if (!(await loadKuboConfigFile(dataPath))) await initKuboRepoIfNeeded(log, dataPath, kuboRpcUrl, gatewayUrl); | ||
|
|
||
| const canAsk = !flags["non-interactive"] && process.stdin.isTTY === true; | ||
| const overrides = testOverrides(process.env); | ||
| try { | ||
| await runReachabilitySetup( | ||
| { | ||
| dataPath, | ||
| kuboRepoPath: resolveKuboRepoPath(dataPath), | ||
| kuboRpcUrl, | ||
| staging: flags.staging, | ||
| nonInteractive: flags["non-interactive"], | ||
| ...(overrides ? { overrides } : {}) | ||
| }, | ||
| { | ||
| ...realSetupDependencies(), | ||
| log: (message) => this.log(message), | ||
| warn: (message) => this.logToStderr(message), | ||
| ...(canAsk ? { confirm: confirmOnTerminal, runSudoSetcap } : {}), | ||
| platform: process.platform, | ||
| inContainer: await detectContainer(), | ||
| isCi: Boolean(process.env["CI"]), | ||
| kuboBinaryPath: await resolveKuboBinaryPath() | ||
| } | ||
| ); | ||
| } catch (error) { | ||
| if (error instanceof NeedsAnswerError) this.error(error.message, { exit: 2 }); | ||
| throw error; | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { Command, Flags } from "@oclif/core"; | ||
| import defaults from "../../../common-utils/defaults.js"; | ||
| import { readReachabilityStatus, type ReachabilityStatus } from "../../../reachability/state.js"; | ||
|
|
||
| export default class Status extends Command { | ||
| static override description = "Show whether browsers can reach this node: mode, announced addresses, certificate expiry and last checks"; | ||
|
|
||
| static override enableJsonFlag = true; | ||
|
|
||
| static override flags = { | ||
| "pkcOptions.dataPath": Flags.directory({ | ||
| description: "Data path of the node", | ||
| required: false | ||
| }) | ||
| }; | ||
|
|
||
| static override examples = ["bitsocial reachability status", "bitsocial reachability status --json"]; | ||
|
|
||
| async run(): Promise<ReachabilityStatus> { | ||
| const { flags } = await this.parse(Status); | ||
| const status = await readReachabilityStatus(flags["pkcOptions.dataPath"] || defaults.PKC_DATA_PATH); | ||
|
|
||
| if (!status.configured) { | ||
| this.log("Reachability is not configured. Browsers cannot reach this node. Run: bitsocial reachability setup"); | ||
| return status; | ||
| } | ||
| this.log(`Enabled: ${status.enabled ? "yes" : "no"}`); | ||
| this.log(`Mode: ${status.mode}${status.staging ? " (Let's Encrypt staging)" : ""}`); | ||
| if (status.domain) this.log(`Domain: ${status.domain}`); | ||
| this.log(`Announced addresses: ${status.addresses.length > 0 ? "" : "none"}`); | ||
| for (const address of status.addresses) this.log(` ${address}`); | ||
| this.log(`Certificates: ${status.certificates.length > 0 ? "" : "none"}`); | ||
| for (const certificate of status.certificates) | ||
| this.log(` ${certificate.names.join(", ")}: expires ${certificate.notAfter} (${certificate.hoursLeft} h left)`); | ||
| if (status.lastChecks?.lastIpCheck) { | ||
| const { at, publicIps } = status.lastChecks.lastIpCheck; | ||
| this.log(`Last IP check: ${at} (${publicIps.join(", ") || "no public IP found"})`); | ||
| } | ||
| for (const [domain, check] of Object.entries(status.lastChecks?.domains ?? {})) { | ||
| const state = check.ok ? "ok" : `failing since ${check.failingSince ?? check.lastCheckAt}`; | ||
| this.log(`Domain check ${domain}: ${state}${check.droppedFromAnnounce ? ", no longer announced" : ""} (checked ${check.lastCheckAt})`); | ||
| } | ||
| if (status.lastChecks?.caddy) this.log(`Caddy: ${status.lastChecks.caddy.running ? "running" : "not running"}`); | ||
| return status; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The commit adds
reachability setupandreachability status, but the generated README contains no entries for either command; its only change is the daemon's Kubo path. This leaves the new user-facing commands and flags absent from the repository's command reference, contrary to the requirement to update documentation when commands or flags change.AGENTS.md reference: AGENTS.md:L70-L81
Useful? React with 👍 / 👎.