// Command ctfetch fetches and dumps a specific leaf entry from a given Static CT log. // It can also dump the whole contents of the tile, if the -dumpall flag is specified. // (C) Copyright 2026 Pim van Pelt package main import ( "encoding/json" "flag" "fmt" "os" "strconv" "strings" "ctfetch/internal/utils" "filippo.io/sunlight" "golang.org/x/mod/sumdb/tlog" ) func main() { dumpAll := flag.Bool("dumpall", false, "dump all entries in the tile") flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage: %s [--dumpall] \n", os.Args[0]) fmt.Fprintf(os.Stderr, "Example: %s https://halloumi2026h1.mon.ct.ipng.ch 457683896\n", os.Args[0]) fmt.Fprintf(os.Stderr, "\nFlags:\n") flag.PrintDefaults() } flag.Parse() if flag.NArg() != 2 { flag.Usage() os.Exit(1) } logURL := strings.TrimSuffix(flag.Arg(0), "/") leafIndex, err := strconv.ParseInt(flag.Arg(1), 10, 64) if err != nil { fatal("invalid leaf index: %v", err) } // Convert leaf index to tile coordinates tile := tlog.TileForIndex(sunlight.TileHeight, tlog.StoredHashIndex(0, leafIndex)) tile.L = -1 // Data tiles are at level -1 // Get the tile path (both partial and full versions) partialPath := sunlight.TilePath(tile) // For full tile path, we need to remove the .p/W suffix if present fullTile := tile fullTile.W = sunlight.TileWidth fullPath := sunlight.TilePath(fullTile) positionInTile := leafIndex % sunlight.TileWidth fmt.Fprintf(os.Stderr, "Leaf Index: %d\n", leafIndex) fmt.Fprintf(os.Stderr, "Position in tile: %d\n", positionInTile) fmt.Fprintf(os.Stderr, "Partial tile path: %s\n", partialPath) fmt.Fprintf(os.Stderr, "Full tile path: %s\n", fullPath) // Try to fetch the tile (partial first, then full) var tileData []byte var fetchedPath string // Try partial tile first partialURL := logURL + "/" + partialPath fmt.Fprintf(os.Stderr, "Trying: %s\n", partialURL) tileData, err = utils.FetchURL(partialURL) if err == nil { fetchedPath = partialPath fmt.Fprintf(os.Stderr, "Successfully fetched partial tile\n") } else { // Fall back to full tile fullURL := logURL + "/" + fullPath fmt.Fprintf(os.Stderr, "Partial tile failed, trying: %s\n", fullURL) tileData, err = utils.FetchURL(fullURL) if err != nil { fatal("failed to fetch tile: %v", err) } fetchedPath = fullPath fmt.Fprintf(os.Stderr, "Successfully fetched full tile\n") } // Decompress if needed tileData, err = utils.Decompress(tileData) if err != nil { fatal("failed to decompress tile: %v", err) } fmt.Fprintf(os.Stderr, "Tile size: %d bytes\n", len(tileData)) fmt.Fprintf(os.Stderr, "Fetched path: %s\n\n", fetchedPath) if *dumpAll { // Dump all entries in the tile result, err := utils.DumpAllEntries(tileData) if err != nil { fatal("%v", err) } printJSON(result) } else { // Dump only the specific entry at the position entry, err := utils.DumpEntryAtPosition(tileData, int(positionInTile), leafIndex) if err != nil { fatal("%v", err) } printJSON(entry) } } func printJSON(v interface{}) { data, err := json.MarshalIndent(v, "", " ") if err != nil { fatal("failed to marshal JSON: %v", err) } fmt.Println(string(data)) } func fatal(format string, args ...any) { fmt.Fprintf(os.Stderr, "Error: "+format+"\n", args...) os.Exit(1) }