mirror of
https://github.com/ipfs/kubo.git
synced 2025-05-17 06:57:40 +08:00
170 lines
3.7 KiB
Go
170 lines
3.7 KiB
Go
package rpc
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"mime"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
|
|
"github.com/ipfs/boxo/files"
|
|
cmds "github.com/ipfs/go-ipfs-cmds"
|
|
cmdhttp "github.com/ipfs/go-ipfs-cmds/http"
|
|
)
|
|
|
|
type Error = cmds.Error
|
|
|
|
type trailerReader struct {
|
|
resp *http.Response
|
|
}
|
|
|
|
func (r *trailerReader) Read(b []byte) (int, error) {
|
|
n, err := r.resp.Body.Read(b)
|
|
if err != nil {
|
|
if e := r.resp.Trailer.Get(cmdhttp.StreamErrHeader); e != "" {
|
|
err = errors.New(e)
|
|
}
|
|
}
|
|
return n, err
|
|
}
|
|
|
|
func (r *trailerReader) Close() error {
|
|
return r.resp.Body.Close()
|
|
}
|
|
|
|
type Response struct {
|
|
Output io.ReadCloser
|
|
Error *Error
|
|
}
|
|
|
|
func (r *Response) Close() error {
|
|
if r.Output != nil {
|
|
|
|
// drain output (response body)
|
|
_, err1 := io.Copy(io.Discard, r.Output)
|
|
err2 := r.Output.Close()
|
|
if err1 != nil {
|
|
return err1
|
|
}
|
|
return err2
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Cancel aborts running request (without draining request body).
|
|
func (r *Response) Cancel() error {
|
|
if r.Output != nil {
|
|
return r.Output.Close()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Decode reads request body and decodes it as json.
|
|
func (r *Response) decode(dec interface{}) error {
|
|
if r.Error != nil {
|
|
return r.Error
|
|
}
|
|
|
|
err := json.NewDecoder(r.Output).Decode(dec)
|
|
err2 := r.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return err2
|
|
}
|
|
|
|
func (r *Request) Send(c *http.Client) (*Response, error) {
|
|
url := r.getURL()
|
|
req, err := http.NewRequest("POST", url, r.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req = req.WithContext(r.Ctx)
|
|
|
|
// Add any headers that were supplied via the requestBuilder.
|
|
for k, v := range r.Headers {
|
|
req.Header.Add(k, v)
|
|
}
|
|
|
|
if fr, ok := r.Body.(*files.MultiFileReader); ok {
|
|
req.Header.Set("Content-Type", "multipart/form-data; boundary="+fr.Boundary())
|
|
req.Header.Set("Content-Disposition", "form-data; name=\"files\"")
|
|
}
|
|
|
|
resp, err := c.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
contentType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
nresp := new(Response)
|
|
|
|
nresp.Output = &trailerReader{resp}
|
|
if resp.StatusCode >= http.StatusBadRequest {
|
|
e := new(Error)
|
|
switch {
|
|
case resp.StatusCode == http.StatusNotFound:
|
|
e.Message = "command not found"
|
|
case contentType == "text/plain":
|
|
out, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "ipfs-shell: warning! response (%d) read error: %s\n", resp.StatusCode, err)
|
|
}
|
|
e.Message = string(out)
|
|
|
|
// set special status codes.
|
|
switch resp.StatusCode {
|
|
case http.StatusNotFound, http.StatusBadRequest:
|
|
e.Code = cmds.ErrClient
|
|
case http.StatusTooManyRequests:
|
|
e.Code = cmds.ErrRateLimited
|
|
case http.StatusForbidden:
|
|
e.Code = cmds.ErrForbidden
|
|
}
|
|
case contentType == "application/json":
|
|
if err = json.NewDecoder(resp.Body).Decode(e); err != nil {
|
|
fmt.Fprintf(os.Stderr, "ipfs-shell: warning! response (%d) unmarshall error: %s\n", resp.StatusCode, err)
|
|
}
|
|
default:
|
|
// This is a server-side bug (probably).
|
|
e.Code = cmds.ErrImplementation
|
|
fmt.Fprintf(os.Stderr, "ipfs-shell: warning! unhandled response (%d) encoding: %s", resp.StatusCode, contentType)
|
|
out, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "ipfs-shell: response (%d) read error: %s\n", resp.StatusCode, err)
|
|
}
|
|
e.Message = fmt.Sprintf("unknown ipfs-shell error encoding: %q - %q", contentType, out)
|
|
}
|
|
nresp.Error = e
|
|
nresp.Output = nil
|
|
|
|
// drain body and close
|
|
_, _ = io.Copy(io.Discard, resp.Body)
|
|
_ = resp.Body.Close()
|
|
}
|
|
|
|
return nresp, nil
|
|
}
|
|
|
|
func (r *Request) getURL() string {
|
|
values := make(url.Values)
|
|
for _, arg := range r.Args {
|
|
values.Add("arg", arg)
|
|
}
|
|
for k, v := range r.Opts {
|
|
values.Add(k, v)
|
|
}
|
|
|
|
return fmt.Sprintf("%s/%s?%s", r.ApiBase, r.Command, values.Encode())
|
|
}
|