name.go
1 package coreapi 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "strings" 8 "time" 9 10 "github.com/ipfs/boxo/ipns" 11 keystore "github.com/ipfs/boxo/keystore" 12 "github.com/ipfs/boxo/namesys" 13 "github.com/ipfs/kubo/tracing" 14 "go.opentelemetry.io/otel/attribute" 15 "go.opentelemetry.io/otel/trace" 16 17 "github.com/ipfs/boxo/path" 18 coreiface "github.com/ipfs/kubo/core/coreiface" 19 caopts "github.com/ipfs/kubo/core/coreiface/options" 20 ci "github.com/libp2p/go-libp2p/core/crypto" 21 peer "github.com/libp2p/go-libp2p/core/peer" 22 ) 23 24 type NameAPI CoreAPI 25 26 // Publish announces new IPNS name and returns the new IPNS entry. 27 func (api *NameAPI) Publish(ctx context.Context, p path.Path, opts ...caopts.NamePublishOption) (ipns.Name, error) { 28 ctx, span := tracing.Span(ctx, "CoreAPI.NameAPI", "Publish", trace.WithAttributes(attribute.String("path", p.String()))) 29 defer span.End() 30 31 if err := api.checkPublishAllowed(); err != nil { 32 return ipns.Name{}, err 33 } 34 35 options, err := caopts.NamePublishOptions(opts...) 36 if err != nil { 37 return ipns.Name{}, err 38 } 39 span.SetAttributes( 40 attribute.Bool("allowoffline", options.AllowOffline), 41 attribute.String("key", options.Key), 42 attribute.Float64("validtime", options.ValidTime.Seconds()), 43 ) 44 if options.TTL != nil { 45 span.SetAttributes(attribute.Float64("ttl", options.TTL.Seconds())) 46 } 47 48 // Handle different publishing modes 49 if options.AllowDelegated { 50 // AllowDelegated mode: check if delegated publishers are configured 51 cfg, err := api.repo.Config() 52 if err != nil { 53 return ipns.Name{}, fmt.Errorf("failed to read config: %w", err) 54 } 55 delegatedPublishers := cfg.DelegatedPublishersWithAutoConf() 56 if len(delegatedPublishers) == 0 { 57 return ipns.Name{}, errors.New("no delegated publishers configured: add Ipns.DelegatedPublishers or use --allow-offline for local-only publishing") 58 } 59 // For allow-delegated mode, we only require that we have delegated publishers configured 60 // The node doesn't need P2P connectivity since we're using HTTP publishing 61 } else { 62 // Normal mode: check online status with allow-offline flag 63 err = api.checkOnline(options.AllowOffline) 64 if err != nil { 65 return ipns.Name{}, err 66 } 67 } 68 69 k, err := keylookup(api.privateKey, api.repo.Keystore(), options.Key) 70 if err != nil { 71 return ipns.Name{}, err 72 } 73 74 eol := time.Now().Add(options.ValidTime) 75 76 publishOptions := []namesys.PublishOption{ 77 namesys.PublishWithEOL(eol), 78 namesys.PublishWithIPNSOption(ipns.WithV1Compatibility(options.CompatibleWithV1)), 79 } 80 81 if options.TTL != nil { 82 publishOptions = append(publishOptions, namesys.PublishWithTTL(*options.TTL)) 83 } 84 85 if options.Sequence != nil { 86 publishOptions = append(publishOptions, namesys.PublishWithSequence(*options.Sequence)) 87 } 88 89 err = api.namesys.Publish(ctx, k, p, publishOptions...) 90 if err != nil { 91 return ipns.Name{}, err 92 } 93 94 pid, err := peer.IDFromPrivateKey(k) 95 if err != nil { 96 return ipns.Name{}, err 97 } 98 99 return ipns.NameFromPeer(pid), nil 100 } 101 102 func (api *NameAPI) Search(ctx context.Context, name string, opts ...caopts.NameResolveOption) (<-chan coreiface.IpnsResult, error) { 103 ctx, span := tracing.Span(ctx, "CoreAPI.NameAPI", "Search", trace.WithAttributes(attribute.String("name", name))) 104 defer span.End() 105 106 options, err := caopts.NameResolveOptions(opts...) 107 if err != nil { 108 return nil, err 109 } 110 111 span.SetAttributes(attribute.Bool("cache", options.Cache)) 112 113 err = api.checkOnline(true) 114 if err != nil { 115 return nil, err 116 } 117 118 var resolver namesys.Resolver = api.namesys 119 if !options.Cache { 120 resolver, err = namesys.NewNameSystem(api.routing, 121 namesys.WithDatastore(api.repo.Datastore()), 122 namesys.WithDNSResolver(api.dnsResolver)) 123 if err != nil { 124 return nil, err 125 } 126 } 127 128 if !strings.HasPrefix(name, "/ipns/") { 129 name = "/ipns/" + name 130 } 131 132 p, err := path.NewPath(name) 133 if err != nil { 134 return nil, err 135 } 136 137 out := make(chan coreiface.IpnsResult) 138 go func() { 139 defer close(out) 140 for res := range resolver.ResolveAsync(ctx, p, options.ResolveOpts...) { 141 select { 142 case out <- coreiface.IpnsResult{Path: res.Path, Err: res.Err}: 143 case <-ctx.Done(): 144 return 145 } 146 } 147 }() 148 149 return out, nil 150 } 151 152 // Resolve attempts to resolve the newest version of the specified name and 153 // returns its path. 154 func (api *NameAPI) Resolve(ctx context.Context, name string, opts ...caopts.NameResolveOption) (path.Path, error) { 155 ctx, span := tracing.Span(ctx, "CoreAPI.NameAPI", "Resolve", trace.WithAttributes(attribute.String("name", name))) 156 defer span.End() 157 158 ctx, cancel := context.WithCancel(ctx) 159 defer cancel() 160 161 results, err := api.Search(ctx, name, opts...) 162 if err != nil { 163 return nil, err 164 } 165 166 err = coreiface.ErrResolveFailed 167 var p path.Path 168 169 for res := range results { 170 p, err = res.Path, res.Err 171 if err != nil { 172 break 173 } 174 } 175 176 return p, err 177 } 178 179 func keylookup(self ci.PrivKey, kstore keystore.Keystore, k string) (ci.PrivKey, error) { 180 //////////////////// 181 // Lookup by name // 182 //////////////////// 183 184 // First, lookup self. 185 if k == "self" { 186 return self, nil 187 } 188 189 // Then, look in the keystore. 190 res, err := kstore.Get(k) 191 if res != nil { 192 return res, nil 193 } 194 195 if err != nil && err != keystore.ErrNoSuchKey { 196 return nil, err 197 } 198 199 keys, err := kstore.List() 200 if err != nil { 201 return nil, err 202 } 203 204 ////////////////// 205 // Lookup by ID // 206 ////////////////// 207 targetPid, err := peer.Decode(k) 208 if err != nil { 209 return nil, keystore.ErrNoSuchKey 210 } 211 212 // First, check self. 213 pid, err := peer.IDFromPrivateKey(self) 214 if err != nil { 215 return nil, fmt.Errorf("failed to determine peer ID for private key: %w", err) 216 } 217 if pid == targetPid { 218 return self, nil 219 } 220 221 // Then, look in the keystore. 222 for _, key := range keys { 223 privKey, err := kstore.Get(key) 224 if err != nil { 225 return nil, err 226 } 227 228 pid, err := peer.IDFromPrivateKey(privKey) 229 if err != nil { 230 return nil, err 231 } 232 233 if targetPid == pid { 234 return privKey, nil 235 } 236 } 237 238 return nil, errors.New("no key by the given name or PeerID was found") 239 }