/ client / rpc / api.go
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  }