client.go 4.94 KB
Newer Older
1 2 3
package http

import (
Jan Winkelmann's avatar
Jan Winkelmann committed
4
	"context"
5
	"fmt"
6
	"io"
7
	"net"
8
	"net/http"
9
	"net/url"
10 11
	"strings"

Steven Allen's avatar
Steven Allen committed
12
	"github.com/ipfs/go-ipfs-cmds"
13 14

	"github.com/ipfs/go-ipfs-files"
15 16
)

17
const (
18 19 20
	ApiUrlFormat = "%s%s/%s?%s"
)

Jeromy's avatar
Jeromy committed
21 22 23 24
var OptionSkipMap = map[string]bool{
	"api": true,
}

25 26
// Client is the commands HTTP client interface.
type Client interface {
27
	Send(req *cmds.Request) (cmds.Response, error)
28 29 30 31
}

type client struct {
	serverAddress string
rht's avatar
rht committed
32
	httpClient    *http.Client
33 34
	ua            string
	apiPrefix     string
35
}
36

37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
type ClientOpt func(*client)

func ClientWithUserAgent(ua string) ClientOpt {
	return func(c *client) {
		c.ua = ua
	}
}

func ClientWithAPIPrefix(apiPrefix string) ClientOpt {
	return func(c *client) {
		c.apiPrefix = apiPrefix
	}
}

func NewClient(address string, opts ...ClientOpt) Client {
	if !strings.HasPrefix(address, "http://") {
		address = "http://" + address
	}

	c := &client{
57
		serverAddress: address,
rht's avatar
rht committed
58
		httpClient:    http.DefaultClient,
59 60 61 62 63
		ua:            "go-ipfs-cmds/http",
	}

	for _, opt := range opts {
		opt(c)
64
	}
65 66

	return c
67
}
68

69
func (c *client) Execute(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) error {
70 71 72 73 74 75 76 77 78 79
	cmd := req.Command

	if cmd.PreRun != nil {
		err := cmd.PreRun(req, env)
		if err != nil {
			return err
		}
	}

	res, err := c.Send(req)
Brian Tiger Chow's avatar
Brian Tiger Chow committed
80
	if err != nil {
81
		if isConnRefused(err) {
Devin's avatar
Devin committed
82
			err = fmt.Errorf("cannot connect to the api. Is the daemon running? To run as a standalone CLI command remove the api file in `$IPFS_PATH/api`")
83 84
		}
		return err
Brian Tiger Chow's avatar
Brian Tiger Chow committed
85 86
	}

keks's avatar
keks committed
87 88 89 90
	if cmd.PostRun != nil {
		if typer, ok := re.(interface {
			Type() cmds.PostRunType
		}); ok && cmd.PostRun[typer.Type()] != nil {
keks's avatar
keks committed
91
			err := cmd.PostRun[typer.Type()](res, re)
keks's avatar
keks committed
92 93 94 95 96 97
			closeErr := re.CloseWithError(err)
			if closeErr == cmds.ErrClosingClosedEmitter {
				// ignore double close errors
				return nil
			}

Steven Allen's avatar
Steven Allen committed
98
			return closeErr
keks's avatar
keks committed
99 100 101
		}
	}

102 103 104
	return cmds.Copy(re, res)
}

105
func (c *client) toHTTPRequest(req *cmds.Request) (*http.Request, error) {
106
	query, err := getQuery(req)
107 108 109 110
	if err != nil {
		return nil, err
	}

111
	var fileReader *files.MultiFileReader
Łukasz Magiera's avatar
Łukasz Magiera committed
112 113 114
	var reader io.Reader // in case we have no body to send we need to provide
	// untyped nil to http.NewRequest

115 116 117
	if bodyArgs := req.BodyArgs(); bodyArgs != nil {
		// In the end, this wraps a file reader in a file reader.
		// However, such is life.
Łukasz Magiera's avatar
Łukasz Magiera committed
118 119
		fileReader = files.NewMultiFileReader(files.NewMapDirectory(map[string]files.Node{
			"stdin": files.NewReaderFile(bodyArgs),
120 121 122
		}), true)
		reader = fileReader
	} else if req.Files != nil {
123
		fileReader = files.NewMultiFileReader(req.Files, true)
124
		reader = fileReader
125 126
	}

127 128
	path := strings.Join(req.Path, "/")
	url := fmt.Sprintf(ApiUrlFormat, c.serverAddress, c.apiPrefix, path, query)
129

130
	httpReq, err := http.NewRequest("POST", url, reader)
131 132 133 134 135 136
	if err != nil {
		return nil, err
	}

	// TODO extract string consts?
	if fileReader != nil {
137
		httpReq.Header.Set(contentTypeHeader, "multipart/form-data; boundary="+fileReader.Boundary())
138
	} else {
139
		httpReq.Header.Set(contentTypeHeader, applicationOctetStream)
140
	}
141 142
	httpReq.Header.Set(uaHeader, c.ua)

143
	httpReq = httpReq.WithContext(req.Context)
rht's avatar
rht committed
144
	httpReq.Close = true
145

146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
	return httpReq, nil
}

func (c *client) Send(req *cmds.Request) (cmds.Response, error) {
	if req.Context == nil {
		log.Warningf("no context set in request")
		req.Context = context.Background()
	}

	// save user-provided encoding
	previousUserProvidedEncoding, found := req.Options[cmds.EncLong].(string)

	// override with json to send to server
	req.SetOption(cmds.EncLong, cmds.JSON)

	// stream channel output
	req.SetOption(cmds.ChanOpt, true)

	// build http request
	httpReq, err := c.toHTTPRequest(req)
	if err != nil {
		return nil, err
	}

	// send http request
rht's avatar
rht committed
171 172 173 174
	httpRes, err := c.httpClient.Do(httpReq)
	if err != nil {
		return nil, err
	}
175

176
	// parse using the overridden JSON encoding in request
177
	res, err := parseResponse(httpRes, req)
rht's avatar
rht committed
178
	if err != nil {
rht's avatar
rht committed
179
		return nil, err
180
	}
rht's avatar
rht committed
181

182
	// reset request encoding to what it was before
rht's avatar
rht committed
183 184 185 186
	if found && len(previousUserProvidedEncoding) > 0 {
		// reset to user provided encoding after sending request
		// NB: if user has provided an encoding but it is the empty string,
		// still leave it as JSON.
187
		req.SetOption(cmds.EncLong, previousUserProvidedEncoding)
rht's avatar
rht committed
188 189 190
	}

	return res, nil
191 192
}

193
func getQuery(req *cmds.Request) (string, error) {
194
	query := url.Values{}
195 196

	for k, v := range req.Options {
Jeromy's avatar
Jeromy committed
197
		if OptionSkipMap[k] {
Jeromy's avatar
Jeromy committed
198 199
			continue
		}
200
		str := fmt.Sprintf("%v", v)
201
		query.Set(k, str)
202
	}
203

204 205
	args := req.Arguments
	argDefs := req.Command.Arguments
206

207 208 209 210 211
	argDefIndex := 0

	for _, arg := range args {
		argDef := argDefs[argDefIndex]
		// skip ArgFiles
Steven Allen's avatar
Steven Allen committed
212
		for argDef.Type == cmds.ArgFile {
213 214
			argDefIndex++
			argDef = argDefs[argDefIndex]
215 216
		}

217 218 219 220
		query.Add("arg", arg)

		if len(argDefs) > argDefIndex+1 {
			argDefIndex++
221
		}
222
	}
223

224
	return query.Encode(), nil
225
}
226 227 228 229 230 231 232 233 234 235 236 237 238 239

func isConnRefused(err error) bool {
	// unwrap url errors from http calls
	if urlerr, ok := err.(*url.Error); ok {
		err = urlerr.Err
	}

	netoperr, ok := err.(*net.OpError)
	if !ok {
		return false
	}

	return netoperr.Op == "dial"
}