Unverified Commit 0cfc76fb authored by Hong Minhee's avatar Hong Minhee
Browse files

Add -a/--authorized-fetch option to fedify lookup

parent d6d3d952
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -68,6 +68,7 @@
    "spki",
    "subproperty",
    "superproperty",
    "tempserver",
    "unfollow",
    "unfollowing",
    "urlpattern",
+84 −8
Original line number Diff line number Diff line
import { Command } from "@cliffy/command";
import { lookupObject } from "@fedify/fedify";
import {
  Application,
  CryptographicKey,
  type DocumentLoader,
  generateCryptoKeyPair,
  getAuthenticatedDocumentLoader,
  lookupObject,
  type ResourceDescriptor,
  respondWithObject,
} from "@fedify/fedify";
import { highlight } from "cli-highlight";
import ora from "ora";
import { spawnTemporaryServer, type TemporaryServer } from "./tempserver.ts";

export const command = new Command()
  .arguments("<url:string>")
@@ -9,6 +20,7 @@ export const command = new Command()
      "The argument can be either a URL or an actor handle " +
      "(e.g., @username@domain).",
  )
  .option("-a, --authorized-fetch", "Sign the request with an one-time key.")
  .option("-c, --compact", "Compact the fetched JSON-LD document.", {
    conflicts: ["expand"],
  })
@@ -16,14 +28,78 @@ export const command = new Command()
    conflicts: ["compact"],
  })
  .action(async (options, url: string) => {
    const object = await lookupObject(url);
    const spinner = ora({
      text: "Looking up the object...",
      discardStdin: false,
    }).start();
    let server: TemporaryServer | undefined = undefined;
    let loader: DocumentLoader | undefined = undefined;
    if (options.authorizedFetch) {
      spinner.text = "Generating a one-time key pair...";
      const key = await generateCryptoKeyPair();
      spinner.text = "Spinning up a temporary ActivityPub server...";
      server = await spawnTemporaryServer((req) => {
        if (new URL(req.url).pathname == "/.well-known/webfinger") {
          const jrd: ResourceDescriptor = {
            subject: `acct:${server!.url.hostname}@${server!.url.hostname}`,
            aliases: [server!.url.href],
            links: [
              {
                rel: "self",
                href: server!.url.href,
                type: "application/activity+json",
              },
            ],
          };
          return new Response(JSON.stringify(jrd), {
            headers: { "Content-Type": "application/jrd+json" },
          });
        }
        return respondWithObject(
          new Application({
            id: server?.url,
            preferredUsername: server?.url?.hostname,
            publicKey: new CryptographicKey({
              id: new URL("#main-key", server?.url),
              owner: server?.url,
              publicKey: key.publicKey,
            }),
            manuallyApprovesFollowers: true,
            inbox: new URL("/inbox", server?.url),
            outbox: new URL("/outbox", server?.url),
          }),
        );
      });
      loader = getAuthenticatedDocumentLoader({
        keyId: new URL("#main-key", server.url),
        privateKey: key.privateKey,
      });
    }
    try {
      spinner.text = "Looking up the object...";
      const object = await lookupObject(url, { documentLoader: loader });
      spinner.succeed();
      if (object == null) {
        console.error("Failed to fetch the object.");
        if (loader == null) {
          console.error(
            "It may be a private object.  Try with -a/--authorized-fetch.",
          );
        }
        Deno.exit(1);
      }
      if (options.compact) {
      printJson(await object?.toJsonLd());
        printJson(await object.toJsonLd());
      } else if (options.expand) {
      printJson(await object?.toJsonLd({ expand: true }));
        printJson(await object.toJsonLd({ expand: true }));
      } else {
        console.log(object);
      }
    } catch (_) {
      spinner.fail();
    } finally {
      await server?.close();
    }
  });

function printJson(json: unknown): void {

cli/tempserver.ts

0 → 100644
+38 −0
Original line number Diff line number Diff line
import { openTunnel } from "@hongminhee/localtunnel";
import { getLogger } from "@logtape/logtape";

const logger = getLogger(["fedify", "cli", "tempserver"]);

export interface TemporaryServer {
  url: URL;
  close(): Promise<void>;
}

export function spawnTemporaryServer(
  handler: Deno.ServeHandler,
): Promise<TemporaryServer> {
  return new Promise((resolve) => {
    const ac = new AbortController();
    const server = Deno.serve({
      handler,
      port: 0,
      signal: ac.signal,
      onListen({ port }) {
        logger.debug("Temporary server is listening on port {port}.", { port });
        openTunnel({ port }).then((tun) => {
          logger.debug(
            "Temporary server is tunneled to {url}.",
            { url: tun.url.href },
          );
          resolve({
            url: tun.url,
            async close() {
              await server.shutdown();
              await tun.close();
            },
          });
        });
      },
    });
  });
}
+4 −1
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@
  "imports": {
    "@cfworker/json-schema": "npm:@cfworker/json-schema@^1.12.8",
    "@cliffy/command": "https://deno.land/x/cliffy@v1.0.0-rc.4/command/mod.ts",
    "@cliffy/prompt": "https://deno.land/x/cliffy@v1.0.0-rc.4/prompt/mod.ts",
    "@david/which-runtime": "jsr:@david/which-runtime@^0.2.0",
    "@deno/dnt": "jsr:@deno/dnt@^0.41.1",
    "@fedify/fedify": "./mod.ts",
@@ -29,6 +30,7 @@
    "@fedify/fedify/x/fresh": "./x/fresh.ts",
    "@fedify/fedify/x/hono": "./x/hono.ts",
    "@hongminhee/aitertools": "jsr:@hongminhee/aitertools@^0.6.0",
    "@hongminhee/localtunnel": "jsr:@hongminhee/localtunnel@^0.1.0",
    "@logtape/logtape": "jsr:@logtape/logtape@^0.2.2",
    "@phensley/language-tag": "npm:@phensley/language-tag@^1.8.0",
    "@std/assert": "jsr:@std/assert@^0.220.1",
@@ -51,6 +53,7 @@
    "fast-check": "npm:fast-check@^3.17.0",
    "jsonld": "npm:jsonld@^8.3.2",
    "mock_fetch": "https://deno.land/x/mock_fetch@0.3.0/mod.ts",
    "ora": "npm:ora@^8.0.1",
    "uri-template-router": "npm:uri-template-router@^0.0.16",
    "url-template": "npm:url-template@^3.1.1"
  },
@@ -68,7 +71,7 @@
    "cache": "deno task codegen && deno cache mod.ts",
    "check": "deno task codegen && deno fmt --check && deno lint && deno check */*.ts",
    "codegen": "deno run --allow-read --allow-write --check codegen/main.ts vocab/ ../runtime/ > vocab/vocab.ts && deno fmt vocab/vocab.ts && deno cache vocab/vocab.ts && deno check vocab/vocab.ts",
    "cli": "deno task codegen && deno run --allow-read --allow-net --allow-env cli/mod.ts",
    "cli": "deno task codegen && deno run --allow-read --allow-net --allow-env --allow-run cli/mod.ts",
    "test-without-codegen": "deno test --check --doc --allow-read --allow-write --unstable-kv --trace-leaks",
    "test": "deno task codegen && deno task test-without-codegen",
    "coverage": "rm -rf coverage/ && deno task test --coverage && deno coverage --html coverage",