11package main
22
33import (
4+ crypto_ed25519 "crypto/ed25519"
5+ "crypto/x509"
46 "encoding/json"
7+ "encoding/pem"
58 "fmt"
9+ "io"
610 "net/url"
11+ "os"
12+ "time"
713
14+ logging "github.com/ipfs/go-log/v2"
815 "github.com/ipni/go-libipni/maurl"
916 "github.com/ipni/go-libipni/metadata"
1017 "github.com/libp2p/go-libp2p/core/crypto"
1118 "github.com/libp2p/go-libp2p/core/peer"
1219 "github.com/multiformats/go-multiaddr"
13- "github.com/redis/go-redis/v9"
20+ goredis "github.com/redis/go-redis/v9"
21+ "github.com/storacha/go-libstoracha/principalresolver"
1422 "github.com/storacha/go-ucanto/did"
1523 "github.com/storacha/go-ucanto/principal"
1624 ed25519 "github.com/storacha/go-ucanto/principal/ed25519/signer"
1725 "github.com/storacha/go-ucanto/principal/signer"
1826 userver "github.com/storacha/go-ucanto/server"
27+ "github.com/storacha/go-ucanto/validator"
28+ "github.com/urfave/cli/v2"
29+
1930 "github.com/storacha/indexing-service/pkg/construct"
2031 "github.com/storacha/indexing-service/pkg/presets"
21- "github.com/storacha/indexing-service/pkg/principalresolver "
32+ "github.com/storacha/indexing-service/pkg/redis "
2233 "github.com/storacha/indexing-service/pkg/server"
23- "github.com/urfave/cli/v2"
2434)
2535
2636var serverCmd = & cli.Command {
@@ -42,6 +52,11 @@ var serverCmd = &cli.Command{
4252 Aliases : []string {"pk" },
4353 Usage : "base64 encoded private key identity for the server" ,
4454 },
55+ & cli.StringFlag {
56+ Name : "key-file" ,
57+ Aliases : []string {"kf" },
58+ Usage : "path to PEM-encoded Ed25519 private key file" ,
59+ },
4560 & cli.StringFlag {
4661 Name : "did" ,
4762 Usage : "DID of the server (only needs to be set if different from what is derived from the private key i.e. a did:web DID)" ,
@@ -83,41 +98,99 @@ var serverCmd = &cli.Command{
8398 Name : "ipni-format-endpoint" ,
8499 Usage : "HTTP endpoint of the IPNI node to use for format announcements (enables endpoint mimicking IPNI)." ,
85100 },
101+ & cli.StringSliceFlag {
102+ Name : "public-url" ,
103+ EnvVars : []string {"PUBLIC_URL" },
104+ Usage : "Public URL(s) where the indexing service can be reached (for claim fetching). Can be specified multiple times or comma-separated in env var." ,
105+ },
106+ & cli.StringSliceFlag {
107+ Name : "resolve-did-web" ,
108+ EnvVars : []string {"RESOLVE_DID_WEB" },
109+ Usage : "did:web DIDs to resolve via HTTP (fetches /.well-known/did.json). Can be specified multiple times or comma-separated in env var." ,
110+ },
111+ & cli.BoolFlag {
112+ Name : "insecure-did-resolution" ,
113+ EnvVars : []string {"INSECURE_DID_RESOLUTION" },
114+ Usage : "Use HTTP instead of HTTPS for did:web resolution (for local development), used with --resolve-did-web" ,
115+ },
86116 },
87117 Action : func (cCtx * cli.Context ) error {
118+ if cCtx .IsSet ("private-key" ) && cCtx .IsSet ("key-file" ) {
119+ return fmt .Errorf ("private-key and key-file are mutually exclusive" )
120+ }
121+
88122 addr := fmt .Sprintf (":%d" , cCtx .Int ("port" ))
89123 var id principal.Signer
90124 var err error
91125 var opts []server.Option
92126
93- if ! cCtx .IsSet ("private-key" ) {
127+ if cCtx .IsSet ("key-file" ) {
128+ // load from PEM file
129+ id , err = signerFromPEMFile (cCtx .String ("key-file" ))
130+ if err != nil {
131+ return fmt .Errorf ("loading key from PEM file: %w" , err )
132+ }
133+ } else if cCtx .IsSet ("private-key" ) {
134+ id , err = ed25519 .Parse (cCtx .String ("private-key" ))
135+ if err != nil {
136+ return fmt .Errorf ("parsing server private key: %w" , err )
137+ }
138+ } else {
94139 // generate a new private key if one is not provided
95140 id , err = ed25519 .Generate ()
96141 if err != nil {
97142 return fmt .Errorf ("generating server private key: %w" , err )
98143 }
99- } else {
100- id , err = ed25519 .Parse (cCtx .String ("private-key" ))
144+ }
145+
146+ // wrap with custom DID if specified
147+ if cCtx .String ("did" ) != "" {
148+ customDID , err := did .Parse (cCtx .String ("did" ))
101149 if err != nil {
102- return fmt .Errorf ("parsing server private key : %w" , err )
150+ return fmt .Errorf ("parsing server DID : %w" , err )
103151 }
104- if cCtx .String ("did" ) != "" {
105- did , err := did .Parse (cCtx .String ("did" ))
106- if err != nil {
107- return fmt .Errorf ("parsing server DID: %w" , err )
108- }
109- id , err = signer .Wrap (id , did )
110- if err != nil {
111- return fmt .Errorf ("wrapping server DID: %w" , err )
112- }
152+ id , err = signer .Wrap (id , customDID )
153+ if err != nil {
154+ return fmt .Errorf ("wrapping server DID: %w" , err )
113155 }
114156 }
115157
116158 opts = append (opts , server .WithIdentity (id ))
117159
118- presolv , err := principalresolver .New (presets .PrincipalMapping )
119- if err != nil {
120- return fmt .Errorf ("creating principal resolver: %w" , err )
160+ // Create principal resolver
161+ var presolv validator.PrincipalResolver
162+ resolveDIDs := cCtx .StringSlice ("resolve-did-web" )
163+ if len (resolveDIDs ) > 0 {
164+ // Use HTTP-based resolution for specified DIDs
165+ var webDIDs []did.DID
166+ for _ , d := range resolveDIDs {
167+ parsed , err := did .Parse (d )
168+ if err != nil {
169+ return fmt .Errorf ("parsing resolve-did-web DID %s: %w" , d , err )
170+ }
171+ webDIDs = append (webDIDs , parsed )
172+ }
173+
174+ var rslvOpts []principalresolver.Option
175+ if cCtx .Bool ("insecure-did-resolution" ) {
176+ rslvOpts = append (rslvOpts , principalresolver .InsecureResolution ())
177+ }
178+
179+ httpResolver , err := principalresolver .NewHTTPResolver (webDIDs , rslvOpts ... )
180+ if err != nil {
181+ return fmt .Errorf ("creating HTTP principal resolver: %w" , err )
182+ }
183+ presolv , err = principalresolver .NewCachedResolver (httpResolver , 24 * time .Hour )
184+ if err != nil {
185+ return fmt .Errorf ("creating cached resolver: %w" , err )
186+ }
187+ } else {
188+ // Fall back to static mapping from presets
189+ staticResolver , err := principalresolver .NewMapResolver (presets .PrincipalMapping )
190+ if err != nil {
191+ return fmt .Errorf ("creating principal resolver: %w" , err )
192+ }
193+ presolv = staticResolver
121194 }
122195 opts = append (
123196 opts ,
@@ -133,19 +206,16 @@ var serverCmd = &cli.Command{
133206 opts = append (opts , ipniSrvOpts ... )
134207 var sc construct.ServiceConfig
135208 sc .ID = id
136- sc .ProvidersRedis = redis.ClusterOptions {
137- Addrs : []string {cCtx .String ("redis-url" )},
138- Password : cCtx .String ("redis-passwd" ),
139- }
140- sc .ClaimsRedis = redis.ClusterOptions {
141- Addrs : []string {cCtx .String ("redis-url" )},
142- Password : cCtx .String ("redis-passwd" ),
143- }
144- sc .IndexesRedis = redis.ClusterOptions {
145- Addrs : []string {cCtx .String ("redis-url" )},
209+ sc .IPNIFindURL = cCtx .String ("ipni-endpoint" )
210+ sc .PublicURL = cCtx .StringSlice ("public-url" )
211+
212+ // Create standalone Redis client for local development
213+ redisOpts := & goredis.Options {
214+ Addr : cCtx .String ("redis-url" ),
146215 Password : cCtx .String ("redis-passwd" ),
147216 }
148- sc .IPNIFindURL = cCtx .String ("ipni-endpoint" )
217+ redisClient := goredis .NewClient (redisOpts )
218+ clientAdapter := redis .NewClientAdapter (redisClient )
149219
150220 if cCtx .String ("ipni-fallback-endpoints" ) != "" {
151221 var urls []string
@@ -173,7 +243,13 @@ var serverCmd = &cli.Command{
173243 }
174244 sc .PrivateKey = privKey
175245
176- indexer , err := construct .Construct (sc )
246+ logging .SetAllLoggers (logging .LevelInfo )
247+ indexer , err := construct .Construct (sc ,
248+ construct .WithProvidersClient (clientAdapter ),
249+ construct .WithNoProvidersClient (redisClient ),
250+ construct .WithClaimsClient (redisClient ),
251+ construct .WithIndexesClient (redisClient ),
252+ )
177253 if err != nil {
178254 return err
179255 }
@@ -210,3 +286,48 @@ func ipniOpts(ipniFormatPeerID string, ipniFormatEndpoint string) ([]server.Opti
210286 server .WithIPNI (peer.AddrInfo {ID : peerID , Addrs : []multiaddr.Multiaddr {ma }}, metadata .Default .New (metadata.IpfsGatewayHttp {})),
211287 }, nil
212288}
289+
290+ // signerFromPEMFile loads an Ed25519 private key from a PEM file.
291+ func signerFromPEMFile (path string ) (principal.Signer , error ) {
292+ f , err := os .Open (path )
293+ if err != nil {
294+ return nil , err
295+ }
296+ defer f .Close ()
297+
298+ pemData , err := io .ReadAll (f )
299+ if err != nil {
300+ return nil , fmt .Errorf ("reading private key: %w" , err )
301+ }
302+
303+ var privateKey * crypto_ed25519.PrivateKey
304+ rest := pemData
305+
306+ for {
307+ block , remaining := pem .Decode (rest )
308+ if block == nil {
309+ break
310+ }
311+ rest = remaining
312+
313+ if block .Type == "PRIVATE KEY" {
314+ parsedKey , err := x509 .ParsePKCS8PrivateKey (block .Bytes )
315+ if err != nil {
316+ return nil , fmt .Errorf ("failed to parse PKCS#8 private key: %w" , err )
317+ }
318+
319+ key , ok := parsedKey .(crypto_ed25519.PrivateKey )
320+ if ! ok {
321+ return nil , fmt .Errorf ("the parsed key is not an ED25519 private key" )
322+ }
323+ privateKey = & key
324+ break
325+ }
326+ }
327+
328+ if privateKey == nil {
329+ return nil , fmt .Errorf ("could not find a PRIVATE KEY block in the PEM file" )
330+ }
331+
332+ return ed25519 .FromRaw (* privateKey )
333+ }
0 commit comments