master
go 169 lines 3.71 KB
Raw
1 package rpc
2
3 import (
4 "encoding/json"
5 "errors"
6 "fmt"
7 "io"
8 "mime"
9 "net/http"
10 "net/url"
11 "os"
12
13 "github.com/ipfs/boxo/files"
14 cmds "github.com/ipfs/go-ipfs-cmds"
15 cmdhttp "github.com/ipfs/go-ipfs-cmds/http"
16 )
17
18 type Error = cmds.Error
19
20 type trailerReader struct {
21 resp *http.Response
22 }
23
24 func (r *trailerReader) Read(b []byte) (int, error) {
25 n, err := r.resp.Body.Read(b)
26 if err != nil {
27 if e := r.resp.Trailer.Get(cmdhttp.StreamErrHeader); e != "" {
28 err = errors.New(e)
29 }
30 }
31 return n, err
32 }
33
34 func (r *trailerReader) Close() error {
35 return r.resp.Body.Close()
36 }
37
38 type Response struct {
39 Output io.ReadCloser
40 Error *Error
41 }
42
43 func (r *Response) Close() error {
44 if r.Output != nil {
45
46 // drain output (response body)
47 _, err1 := io.Copy(io.Discard, r.Output)
48 err2 := r.Output.Close()
49 if err1 != nil {
50 return err1
51 }
52 return err2
53 }
54 return nil
55 }
56
57 // Cancel aborts running request (without draining request body).
58 func (r *Response) Cancel() error {
59 if r.Output != nil {
60 return r.Output.Close()
61 }
62
63 return nil
64 }
65
66 // Decode reads request body and decodes it as json.
67 func (r *Response) decode(dec any) error {
68 if r.Error != nil {
69 return r.Error
70 }
71
72 err := json.NewDecoder(r.Output).Decode(dec)
73 err2 := r.Close()
74 if err != nil {
75 return err
76 }
77
78 return err2
79 }
80
81 func (r *Request) Send(c *http.Client) (*Response, error) {
82 url := r.getURL()
83 req, err := http.NewRequest("POST", url, r.Body)
84 if err != nil {
85 return nil, err
86 }
87
88 req = req.WithContext(r.Ctx)
89
90 // Add any headers that were supplied via the requestBuilder.
91 for k, v := range r.Headers {
92 req.Header.Add(k, v)
93 }
94
95 if fr, ok := r.Body.(*files.MultiFileReader); ok {
96 req.Header.Set("Content-Type", "multipart/form-data; boundary="+fr.Boundary())
97 req.Header.Set("Content-Disposition", "form-data; name=\"files\"")
98 }
99
100 resp, err := c.Do(req)
101 if err != nil {
102 return nil, err
103 }
104
105 contentType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
106 if err != nil {
107 return nil, err
108 }
109
110 nresp := new(Response)
111
112 nresp.Output = &trailerReader{resp}
113 if resp.StatusCode >= http.StatusBadRequest {
114 e := new(Error)
115 switch {
116 case resp.StatusCode == http.StatusNotFound:
117 e.Message = "command not found"
118 case contentType == "text/plain":
119 out, err := io.ReadAll(resp.Body)
120 if err != nil {
121 fmt.Fprintf(os.Stderr, "ipfs-shell: warning! response (%d) read error: %s\n", resp.StatusCode, err)
122 }
123 e.Message = string(out)
124
125 // set special status codes.
126 switch resp.StatusCode {
127 case http.StatusNotFound, http.StatusBadRequest:
128 e.Code = cmds.ErrClient
129 case http.StatusTooManyRequests:
130 e.Code = cmds.ErrRateLimited
131 case http.StatusForbidden:
132 e.Code = cmds.ErrForbidden
133 }
134 case contentType == "application/json":
135 if err = json.NewDecoder(resp.Body).Decode(e); err != nil {
136 fmt.Fprintf(os.Stderr, "ipfs-shell: warning! response (%d) unmarshall error: %s\n", resp.StatusCode, err)
137 }
138 default:
139 // This is a server-side bug (probably).
140 e.Code = cmds.ErrImplementation
141 fmt.Fprintf(os.Stderr, "ipfs-shell: warning! unhandled response (%d) encoding: %s", resp.StatusCode, contentType)
142 out, err := io.ReadAll(resp.Body)
143 if err != nil {
144 fmt.Fprintf(os.Stderr, "ipfs-shell: response (%d) read error: %s\n", resp.StatusCode, err)
145 }
146 e.Message = fmt.Sprintf("unknown ipfs-shell error encoding: %q - %q", contentType, out)
147 }
148 nresp.Error = e
149 nresp.Output = nil
150
151 // drain body and close
152 _, _ = io.Copy(io.Discard, resp.Body)
153 _ = resp.Body.Close()
154 }
155
156 return nresp, nil
157 }
158
159 func (r *Request) getURL() string {
160 values := make(url.Values)
161 for _, arg := range r.Args {
162 values.Add("arg", arg)
163 }
164 for k, v := range r.Opts {
165 values.Add(k, v)
166 }
167
168 return fmt.Sprintf("%s/%s?%s", r.ApiBase, r.Command, values.Encode())
169 }