api.go
1 package rpc 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "net" 9 "net/http" 10 "os" 11 "path/filepath" 12 "strings" 13 "sync" 14 "time" 15 16 "github.com/blang/semver/v4" 17 "github.com/ipfs/boxo/ipld/merkledag" 18 "github.com/ipfs/go-cid" 19 legacy "github.com/ipfs/go-ipld-legacy" 20 ipfs "github.com/ipfs/kubo" 21 iface "github.com/ipfs/kubo/core/coreiface" 22 caopts "github.com/ipfs/kubo/core/coreiface/options" 23 "github.com/ipfs/kubo/misc/fsutil" 24 dagpb "github.com/ipld/go-codec-dagpb" 25 _ "github.com/ipld/go-ipld-prime/codec/dagcbor" 26 "github.com/ipld/go-ipld-prime/node/basicnode" 27 ma "github.com/multiformats/go-multiaddr" 28 manet "github.com/multiformats/go-multiaddr/net" 29 ) 30 31 const ( 32 DefaultPathName = ".ipfs" 33 DefaultPathRoot = "~/" + DefaultPathName 34 DefaultApiFile = "api" 35 EnvDir = "IPFS_PATH" 36 ) 37 38 // ErrApiNotFound if we fail to find a running daemon. 39 var ErrApiNotFound = errors.New("ipfs api address could not be found") 40 41 // HttpApi implements github.com/ipfs/interface-go-ipfs-core/CoreAPI using 42 // IPFS HTTP API. 43 // 44 // For interface docs see 45 // https://godoc.org/github.com/ipfs/interface-go-ipfs-core#CoreAPI 46 type HttpApi struct { 47 url string 48 httpcli http.Client 49 Headers http.Header 50 applyGlobal func(*requestBuilder) 51 ipldDecoder *legacy.Decoder 52 versionMu sync.Mutex 53 version *semver.Version 54 } 55 56 // NewLocalApi tries to construct new HttpApi instance communicating with local 57 // IPFS daemon 58 // 59 // Daemon api address is pulled from the $IPFS_PATH/api file. 60 // If $IPFS_PATH env var is not present, it defaults to ~/.ipfs. 61 func NewLocalApi() (*HttpApi, error) { 62 baseDir := os.Getenv(EnvDir) 63 if baseDir == "" { 64 baseDir = DefaultPathRoot 65 } 66 67 return NewPathApi(baseDir) 68 } 69 70 // NewPathApi constructs new HttpApi by pulling api address from specified 71 // ipfspath. Api file should be located at $ipfspath/api. 72 func NewPathApi(ipfspath string) (*HttpApi, error) { 73 a, err := ApiAddr(ipfspath) 74 if err != nil { 75 if os.IsNotExist(err) { 76 err = ErrApiNotFound 77 } 78 return nil, err 79 } 80 return NewApi(a) 81 } 82 83 // ApiAddr reads api file in specified ipfs path. 84 func ApiAddr(ipfspath string) (ma.Multiaddr, error) { 85 baseDir, err := fsutil.ExpandHome(ipfspath) 86 if err != nil { 87 return nil, err 88 } 89 90 apiFile := filepath.Join(baseDir, DefaultApiFile) 91 92 api, err := os.ReadFile(apiFile) 93 if err != nil { 94 return nil, err 95 } 96 97 return ma.NewMultiaddr(strings.TrimSpace(string(api))) 98 } 99 100 // NewApi constructs HttpApi with specified endpoint. 101 func NewApi(a ma.Multiaddr) (*HttpApi, error) { 102 transport := &http.Transport{ 103 Proxy: http.ProxyFromEnvironment, 104 DisableKeepAlives: true, 105 } 106 107 network, address, err := manet.DialArgs(a) 108 if err != nil { 109 return nil, err 110 } 111 if network == "unix" { 112 transport.DialContext = func(_ context.Context, _, _ string) (net.Conn, error) { 113 return net.Dial("unix", address) 114 } 115 c := &http.Client{ 116 Transport: transport, 117 } 118 // This will create an API client which 119 // makes requests to `http://unix`. 120 return NewURLApiWithClient(network, c) 121 } 122 123 c := &http.Client{ 124 Transport: transport, 125 } 126 127 return NewApiWithClient(a, c) 128 } 129 130 // NewApiWithClient constructs HttpApi with specified endpoint and custom http client. 131 func NewApiWithClient(a ma.Multiaddr, c *http.Client) (*HttpApi, error) { 132 _, url, err := manet.DialArgs(a) 133 if err != nil { 134 return nil, err 135 } 136 137 if a, err := ma.NewMultiaddr(url); err == nil { 138 _, host, err := manet.DialArgs(a) 139 if err == nil { 140 url = host 141 } 142 } 143 144 proto := "http://" 145 146 // By default, DialArgs is going to provide details suitable for connecting 147 // a socket to, but not really suitable for making an informed choice of http 148 // protocol. For multiaddresses specifying tls and/or https we want to make 149 // a https request instead of a http request. 150 protocols := a.Protocols() 151 for _, p := range protocols { 152 if p.Code == ma.P_HTTPS || p.Code == ma.P_TLS { 153 proto = "https://" 154 break 155 } 156 } 157 158 return NewURLApiWithClient(proto+url, c) 159 } 160 161 func NewURLApiWithClient(url string, c *http.Client) (*HttpApi, error) { 162 decoder := legacy.NewDecoder() 163 // Add support for these codecs to match what is done in the merkledag library 164 // Note: to match prior behavior the go-ipld-prime CBOR decoder is manually included 165 // TODO: allow the codec registry used to be configured by the caller not through a global variable 166 decoder.RegisterCodec(cid.DagProtobuf, dagpb.Type.PBNode, merkledag.ProtoNodeConverter) 167 decoder.RegisterCodec(cid.Raw, basicnode.Prototype.Bytes, merkledag.RawNodeConverter) 168 169 api := &HttpApi{ 170 url: url, 171 httpcli: *c, 172 Headers: make(map[string][]string), 173 applyGlobal: func(*requestBuilder) {}, 174 ipldDecoder: decoder, 175 } 176 177 // We don't support redirects. 178 api.httpcli.CheckRedirect = func(_ *http.Request, _ []*http.Request) error { 179 return fmt.Errorf("unexpected redirect") 180 } 181 182 return api, nil 183 } 184 185 func (api *HttpApi) WithOptions(opts ...caopts.ApiOption) (iface.CoreAPI, error) { 186 options, err := caopts.ApiOptions(opts...) 187 if err != nil { 188 return nil, err 189 } 190 191 subApi := &HttpApi{ 192 url: api.url, 193 httpcli: api.httpcli, 194 Headers: api.Headers, 195 applyGlobal: func(req *requestBuilder) { 196 if options.Offline { 197 req.Option("offline", options.Offline) 198 } 199 }, 200 ipldDecoder: api.ipldDecoder, 201 } 202 203 return subApi, nil 204 } 205 206 func (api *HttpApi) Request(command string, args ...string) RequestBuilder { 207 headers := make(map[string]string) 208 if api.Headers != nil { 209 for k := range api.Headers { 210 headers[k] = api.Headers.Get(k) 211 } 212 } 213 return &requestBuilder{ 214 command: command, 215 args: args, 216 shell: api, 217 headers: headers, 218 } 219 } 220 221 func (api *HttpApi) Unixfs() iface.UnixfsAPI { 222 return (*UnixfsAPI)(api) 223 } 224 225 func (api *HttpApi) Block() iface.BlockAPI { 226 return (*BlockAPI)(api) 227 } 228 229 func (api *HttpApi) Dag() iface.APIDagService { 230 return (*HttpDagServ)(api) 231 } 232 233 func (api *HttpApi) Name() iface.NameAPI { 234 return (*NameAPI)(api) 235 } 236 237 func (api *HttpApi) Key() iface.KeyAPI { 238 return (*KeyAPI)(api) 239 } 240 241 func (api *HttpApi) Pin() iface.PinAPI { 242 return (*PinAPI)(api) 243 } 244 245 func (api *HttpApi) Object() iface.ObjectAPI { 246 return (*ObjectAPI)(api) 247 } 248 249 func (api *HttpApi) Swarm() iface.SwarmAPI { 250 return (*SwarmAPI)(api) 251 } 252 253 func (api *HttpApi) PubSub() iface.PubSubAPI { 254 return (*PubsubAPI)(api) 255 } 256 257 func (api *HttpApi) Routing() iface.RoutingAPI { 258 return (*RoutingAPI)(api) 259 } 260 261 func (api *HttpApi) loadRemoteVersion() (*semver.Version, error) { 262 api.versionMu.Lock() 263 defer api.versionMu.Unlock() 264 265 if api.version == nil { 266 ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second*30)) 267 defer cancel() 268 269 resp, err := api.Request("version").Send(ctx) 270 if err != nil { 271 return nil, err 272 } 273 if resp.Error != nil { 274 return nil, resp.Error 275 } 276 defer resp.Close() 277 var out ipfs.VersionInfo 278 dec := json.NewDecoder(resp.Output) 279 if err := dec.Decode(&out); err != nil { 280 return nil, err 281 } 282 283 remoteVersion, err := semver.New(out.Version) 284 if err != nil { 285 return nil, err 286 } 287 288 api.version = remoteVersion 289 } 290 291 return api.version, nil 292 }