master
go 292 lines 6.88 KB
Raw
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 }