Skip to content

Commit b39816c

Browse files
committed
feat: standalone Redis client and HTTP did:web resolution
- Add standalone Redis client support for non-cluster deployments - Add PEM file key loading via --key-file flag - Add HTTP-based did:web resolution via --resolve-did-web flag - Add --public-url and --insecure-did-resolution flags for configuration - Add debug logging for claim URL fetching
1 parent 1dffab5 commit b39816c

File tree

4 files changed

+160
-31
lines changed

4 files changed

+160
-31
lines changed

cmd/server.go

Lines changed: 152 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,36 @@
11
package main
22

33
import (
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

2636
var 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+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ require (
145145
github.com/opencontainers/go-digest v1.0.0 // indirect
146146
github.com/opencontainers/image-spec v1.1.1 // indirect
147147
github.com/opentracing/opentracing-go v1.2.0 // indirect
148+
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
148149
github.com/pkg/errors v0.9.1 // indirect
149150
github.com/pmezard/go-difflib v1.0.0 // indirect
150151
github.com/polydawn/refmt v0.89.1-0.20231129105047-37766d95467a // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,8 @@ github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgr
550550
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
551551
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
552552
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
553+
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
554+
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
553555
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
554556
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
555557
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=

pkg/service/service.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,13 @@ func (is *IndexingService) jobHandler(mhCtx context.Context, j job, spawn func(j
160160

161161
// fetch (from cache or url) the actual content claim
162162
claimCid := hasClaimCid.GetClaim()
163+
log.Infow("query: fetching claim", "claimCid", claimCid, "providerId", result.Provider.ID, "numAddrs", len(result.Provider.Addrs))
164+
for i, addr := range result.Provider.Addrs {
165+
log.Infow("query: provider address", "index", i, "addr", addr.String())
166+
}
163167
url, err := fetchClaimURL(*result.Provider, claimCid)
164168
if err != nil {
169+
log.Errorw("query: failed to build claim URL", "error", err)
165170
telemetry.Error(s, err, "building claim URL")
166171
return err
167172
}

0 commit comments

Comments
 (0)