Fold tiledump into ctfetch. Add +sct, +issuer and +ctlog flags to print additional info
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
// 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.
|
||||
// Command ctfetch fetches and dumps entries from a Static CT log.
|
||||
//
|
||||
// Two modes:
|
||||
//
|
||||
// ctfetch [flags] <log-url> <leaf-index> [+sct] [+issuer] [+ctlog] fetch one entry by leaf index
|
||||
// ctfetch [flags] <tile-url-or-file> [+sct] [+issuer] [+ctlog] dump all entries in a tile
|
||||
//
|
||||
// (C) Copyright 2026 Pim van Pelt <pim@ipng.ch>
|
||||
package main
|
||||
|
||||
@@ -18,34 +23,85 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
dumpAll := flag.Bool("dumpall", false, "dump all entries in the tile")
|
||||
logsListURL := flag.String("logs-list-url", "https://www.gstatic.com/ct/log_list/v3/all_logs_list.json", "URL of the CT log list JSON")
|
||||
monitoringURL := flag.String("monitoring-url", "", "log root URL for issuer lookups when input is a file")
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, "Usage: %s [--dumpall] <log-url> <leaf-index>\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, "Usage:\n")
|
||||
fmt.Fprintf(os.Stderr, " %s [flags] <log-url> <leaf-index> [+sct] [+issuer] [+ctlog] fetch one entry\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, " %s [flags] <tile-url-or-file> [+sct] [+issuer] [+ctlog] dump all entries in a tile\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, "\nExamples:\n")
|
||||
fmt.Fprintf(os.Stderr, " %s https://halloumi2026h1.mon.ct.ipng.ch 457683896 +sct +issuer +ctlog\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, " %s https://halloumi2026h1.mon.ct.ipng.ch/tile/data/x002/x460/135 +sct\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, " %s --monitoring-url https://halloumi2026h1.mon.ct.ipng.ch tile.bin +issuer\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, "\nFlags:\n")
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
flag.Parse()
|
||||
|
||||
if flag.NArg() != 2 {
|
||||
if flag.NArg() < 1 {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logURL := strings.TrimSuffix(flag.Arg(0), "/")
|
||||
leafIndex, err := strconv.ParseInt(flag.Arg(1), 10, 64)
|
||||
// Determine mode: if second positional arg parses as an integer → leaf-index mode.
|
||||
_, secondIsInt := func() (int64, bool) {
|
||||
if flag.NArg() < 2 {
|
||||
return 0, false
|
||||
}
|
||||
v, err := strconv.ParseInt(flag.Arg(1), 10, 64)
|
||||
return v, err == nil
|
||||
}()
|
||||
|
||||
var modifiers []string
|
||||
if secondIsInt {
|
||||
modifiers = flag.Args()[2:]
|
||||
} else {
|
||||
modifiers = flag.Args()[1:]
|
||||
}
|
||||
|
||||
opts := utils.Options{}
|
||||
for _, arg := range modifiers {
|
||||
switch arg {
|
||||
case "+sct":
|
||||
opts.ShowSCT = true
|
||||
case "+issuer":
|
||||
opts.ShowIssuer = true
|
||||
case "+ctlog":
|
||||
opts.ShowCTLog = true
|
||||
default:
|
||||
fatal("unknown argument %q (expected +sct, +issuer, or +ctlog)", arg)
|
||||
}
|
||||
}
|
||||
|
||||
if opts.ShowCTLog && *logsListURL != "" {
|
||||
ctlogs, err := utils.FetchCTLogList(*logsListURL)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "WARNING: could not fetch CT log list: %v\n", err)
|
||||
} else {
|
||||
opts.CTLogs = ctlogs
|
||||
}
|
||||
}
|
||||
|
||||
if secondIsInt {
|
||||
runLeafIndex(flag.Arg(0), flag.Arg(1), opts)
|
||||
} else {
|
||||
runTileDump(flag.Arg(0), *monitoringURL, opts)
|
||||
}
|
||||
}
|
||||
|
||||
func runLeafIndex(logURL, indexStr string, opts utils.Options) {
|
||||
logURL = strings.TrimSuffix(logURL, "/")
|
||||
opts.LogURL = logURL
|
||||
|
||||
leafIndex, err := strconv.ParseInt(indexStr, 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
|
||||
tile.L = -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)
|
||||
@@ -57,53 +113,82 @@ func main() {
|
||||
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
|
||||
tileData, err := utils.FetchTile(partialURL)
|
||||
if err != nil {
|
||||
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")
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "Successfully fetched partial 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\n", len(tileData))
|
||||
|
||||
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)
|
||||
entry, err := utils.DumpEntryAtPosition(tileData, int(positionInTile), leafIndex, opts)
|
||||
if err != nil {
|
||||
fatal("%v", err)
|
||||
}
|
||||
printJSON(entry)
|
||||
}
|
||||
|
||||
func runTileDump(arg, monitoringURL string, opts utils.Options) {
|
||||
var tileData []byte
|
||||
var err error
|
||||
|
||||
if strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") {
|
||||
// Derive log root from tile URL for issuer lookups.
|
||||
if opts.ShowIssuer {
|
||||
if idx := strings.Index(arg, "/tile/"); idx != -1 {
|
||||
opts.LogURL = strings.TrimSuffix(arg[:idx], "/")
|
||||
} else if monitoringURL != "" {
|
||||
opts.LogURL = strings.TrimSuffix(monitoringURL, "/")
|
||||
} else {
|
||||
fatal("+issuer requires a log root URL; none could be derived from %q and --monitoring-url is not set", arg)
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Fetching: %s\n", arg)
|
||||
tileData, err = utils.FetchTile(arg)
|
||||
if err != nil {
|
||||
fatal("failed to fetch tile: %v", err)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Fetched %d bytes\n", len(tileData))
|
||||
} else {
|
||||
// File input.
|
||||
if opts.ShowIssuer {
|
||||
if monitoringURL != "" {
|
||||
opts.LogURL = strings.TrimSuffix(monitoringURL, "/")
|
||||
} else {
|
||||
fatal("+issuer requires --monitoring-url when input is a file")
|
||||
}
|
||||
}
|
||||
tileData, err = os.ReadFile(arg)
|
||||
if err != nil {
|
||||
fatal("failed to read file: %v", err)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Read %d bytes from %s\n", len(tileData), arg)
|
||||
}
|
||||
|
||||
tileData, err = utils.Decompress(tileData)
|
||||
if err != nil {
|
||||
fatal("failed to decompress tile: %v", err)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Tile size: %d bytes\n\n", len(tileData))
|
||||
|
||||
result, err := utils.DumpAllEntries(tileData, opts)
|
||||
if err != nil {
|
||||
fatal("%v", err)
|
||||
}
|
||||
printJSON(result)
|
||||
}
|
||||
|
||||
func printJSON(v interface{}) {
|
||||
@@ -111,7 +196,6 @@ func printJSON(v interface{}) {
|
||||
if err != nil {
|
||||
fatal("failed to marshal JSON: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
// Command tiledump reads a CT log tile file and dumps all entries.
|
||||
// (C) Copyright 2026 Pim van Pelt <pim@ipng.ch>
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"ctfetch/internal/utils"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Fprintf(os.Stderr, "Usage: %s <tile-file-or-url>\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, "Examples:\n")
|
||||
fmt.Fprintf(os.Stderr, " %s tile.data\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, " %s https://halloumi2026h1.mon.ct.ipng.ch/tile/data/x002/x460/135\n", os.Args[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
arg := os.Args[1]
|
||||
|
||||
var tileData []byte
|
||||
var err error
|
||||
|
||||
// Check if argument is a URL
|
||||
if strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") {
|
||||
// Fetch from URL
|
||||
fmt.Fprintf(os.Stderr, "Fetching: %s\n", arg)
|
||||
tileData, err = utils.FetchURL(arg)
|
||||
if err != nil {
|
||||
// If it's a 404 and the URL is for a partial tile, try the full tile
|
||||
if err.Error() == "HTTP 404" && strings.Contains(arg, ".p/") {
|
||||
fullTileURL := arg[:strings.Index(arg, ".p/")]
|
||||
fmt.Fprintf(os.Stderr, "Partial tile not found, trying full tile: %s\n", fullTileURL)
|
||||
tileData, err = utils.FetchURL(fullTileURL)
|
||||
if err != nil {
|
||||
fatal("failed to fetch full tile: %v", err)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Fetched %d bytes from full tile\n", len(tileData))
|
||||
} else {
|
||||
fatal("failed to fetch URL: %v", err)
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "Fetched %d bytes\n", len(tileData))
|
||||
}
|
||||
} else {
|
||||
// Read from file
|
||||
tileData, err = os.ReadFile(arg)
|
||||
if err != nil {
|
||||
fatal("failed to read file: %v", err)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Read %d bytes from %s\n", len(tileData), arg)
|
||||
}
|
||||
|
||||
// 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\n", len(tileData))
|
||||
|
||||
// Dump all entries
|
||||
result, err := utils.DumpAllEntries(tileData)
|
||||
if err != nil {
|
||||
fatal("%v", err)
|
||||
}
|
||||
printJSON(result)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user