@@ -17,6 +17,7 @@ import (
1717 "iter"
1818 "log/slog"
1919 "net/http"
20+ "os"
2021 "runtime/debug"
2122 "strings"
2223 "time"
@@ -41,12 +42,30 @@ type Client struct {
4142// ClientConfig is the configuration for a [Client].
4243type ClientConfig struct {
4344 // MonitoringPrefix is the c2sp.org/static-ct-api monitoring prefix.
45+ //
46+ // If the MonitoringPrefix has schema "file://", Client will read tiles from
47+ // the local filesystem, and most other settings will be ignored.
48+ //
49+ // If it has schema "gzip+file://", the data tiles are expected to be
50+ // gzip-compressed.
51+ //
52+ // If it has schema "archive+file://", Client will read tiles from a set of
53+ // [archival zip files] in the specified directory.
54+ //
55+ // [archival zip files]:
56+ // https://github.com/geomys/ct-archive/blob/main/README.md#archival-format
4457 MonitoringPrefix string
4558
4659 // PublicKey is the public key of the log, used to verify checkpoints in
4760 // [Client.Checkpoint] and SCTs in [Client.CheckInclusion].
4861 PublicKey crypto.PublicKey
4962
63+ // AllowRFC6962ArchivalLeafs indicates whether to accept leafs archived from
64+ // RFC 6962 logs, which lack the LeafIndex extension.
65+ //
66+ // See [LogEntry.RFC6962ArchivalLeaf] for details.
67+ AllowRFC6962ArchivalLeafs bool
68+
5069 // HTTPClient is the HTTP client used to fetch tiles. If nil, a client is
5170 // created with default timeouts and settings.
5271 //
@@ -84,7 +103,35 @@ type ClientConfig struct {
84103
85104// NewClient creates a new [Client].
86105func NewClient (config * ClientConfig ) (* Client , error ) {
87- if config == nil || config .UserAgent == "" {
106+ if schema , path , ok := strings .Cut (config .MonitoringPrefix , "://" ); ok &&
107+ (schema == "file" || schema == "archive+file" || schema == "gzip+file" ) {
108+ if config .Cache != "" {
109+ return nil , errors .New ("sunlight: permanent cache cannot be used with file://" )
110+ }
111+ root , err := os .OpenRoot (path )
112+ if err != nil {
113+ return nil , fmt .Errorf ("sunlight: failed to open file:// monitoring prefix: %w" , err )
114+ }
115+ tileFS := root .FS ()
116+ if schema == "archive+file" {
117+ tileFS = torchwood .NewTileArchiveFS (root .FS ())
118+ }
119+ options := []torchwood.TileFSOption {torchwood .WithTileFSTilePath (TilePath )}
120+ if schema == "gzip+file" {
121+ options = append (options , torchwood .WithGzipDataTiles ())
122+ }
123+ tileReader , err := torchwood .NewTileFS (tileFS , options ... )
124+ if err != nil {
125+ return nil , fmt .Errorf ("sunlight: failed to create file:// tile reader: %w" , err )
126+ }
127+ client , err := torchwood .NewClient (tileReader , torchwood .WithCutEntry (cutEntry ))
128+ if err != nil {
129+ return nil , fmt .Errorf ("sunlight: failed to create file:// client: %w" , err )
130+ }
131+ return & Client {c : client , r : tileReader , cc : config }, nil
132+ }
133+
134+ if config .UserAgent == "" {
88135 return nil , errors .New ("sunlight: missing UserAgent" )
89136 }
90137 if ! strings .Contains (config .UserAgent , "@" ) &&
@@ -142,7 +189,7 @@ func cutEntry(tile []byte) (entry []byte, rh tlog.Hash, rest []byte, err error)
142189 // This implementation is terribly inefficient, parsing the whole entry just
143190 // to re-serialize and throw it away. If this function shows up in profiles,
144191 // let me know and I'll improve it.
145- e , rest , err := ReadTileLeaf (tile )
192+ e , rest , err := ReadTileLeafMaybeArchival (tile )
146193 if err != nil {
147194 return nil , tlog.Hash {}, nil , err
148195 }
@@ -179,7 +226,16 @@ func (c *Client) Entries(ctx context.Context, tree tlog.Tree, start int64) iter.
179226 c .err = nil
180227 return func (yield func (int64 , * LogEntry ) bool ) {
181228 for i , e := range c .c .Entries (ctx , tree , start ) {
182- entry , rest , err := ReadTileLeaf (e )
229+ var (
230+ entry * LogEntry
231+ rest []byte
232+ err error
233+ )
234+ if c .cc .AllowRFC6962ArchivalLeafs {
235+ entry , rest , err = ReadTileLeafMaybeArchival (e )
236+ } else {
237+ entry , rest , err = ReadTileLeaf (e )
238+ }
183239 if err != nil {
184240 c .err = err
185241 return
@@ -204,14 +260,22 @@ func (c *Client) Entry(ctx context.Context, tree tlog.Tree, index int64) (*LogEn
204260 if err != nil {
205261 return nil , nil , err
206262 }
207- entry , rest , err := ReadTileLeaf (e )
263+ var (
264+ entry * LogEntry
265+ rest []byte
266+ )
267+ if c .cc .AllowRFC6962ArchivalLeafs {
268+ entry , rest , err = ReadTileLeafMaybeArchival (e )
269+ } else {
270+ entry , rest , err = ReadTileLeaf (e )
271+ }
208272 if err != nil {
209273 return nil , nil , fmt .Errorf ("sunlight: failed to parse log entry %d: %w" , index , err )
210274 }
211275 if len (rest ) > 0 {
212276 return nil , nil , fmt .Errorf ("sunlight: unexpected trailing data in entry %d" , index )
213277 }
214- if entry .LeafIndex != index {
278+ if ! entry . RFC6962ArchivalLeaf && entry .LeafIndex != index {
215279 return nil , nil , fmt .Errorf ("sunlight: log entry index %d does not match requested index %d" , entry .LeafIndex , index )
216280 }
217281 return entry , proof , nil
0 commit comments