client.go 5.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
type client struct {
	serverAddress string
rht's avatar
rht committed
27
	httpClient    *http.Client
28 29
	ua            string
	apiPrefix     string
30
	fallback      cmds.Executor
31
}
32

33
// ClientOpt is an option that can be passed to the HTTP client constructor.
34 35
type ClientOpt func(*client)

36
// ClientWithUserAgent specifies the HTTP user agent for the client.
37 38 39 40 41 42
func ClientWithUserAgent(ua string) ClientOpt {
	return func(c *client) {
		c.ua = ua
	}
}

43 44 45 46 47 48 49 50 51
// ClientWithHTTPClient specifies a custom http.Client. Defaults to
// http.DefaultClient.
func ClientWithHTTPClient(hc *http.Client) ClientOpt {
	return func(c *client) {
		c.httpClient = hc
	}
}

// ClientWithAPIPrefix specifies an API URL prefix.
52 53 54 55 56 57
func ClientWithAPIPrefix(apiPrefix string) ClientOpt {
	return func(c *client) {
		c.apiPrefix = apiPrefix
	}
}

58 59 60 61 62 63 64 65 66
// ClientWithFallback adds a fallback executor to the client.
//
// Note: This may run the PreRun function twice.
func ClientWithFallback(exe cmds.Executor) ClientOpt {
	return func(c *client) {
		c.fallback = exe
	}
}

67
// NewClient constructs a new HTTP-backed command executor.
68
func NewClient(address string, opts ...ClientOpt) cmds.Executor {
69 70 71 72 73
	if !strings.HasPrefix(address, "http://") {
		address = "http://" + address
	}

	c := &client{
74
		serverAddress: address,
rht's avatar
rht committed
75
		httpClient:    http.DefaultClient,
76 77 78 79 80
		ua:            "go-ipfs-cmds/http",
	}

	for _, opt := range opts {
		opt(c)
81
	}
82 83

	return c
84
}
85

86
func (c *client) Execute(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) error {
87 88
	cmd := req.Command

89 90 91 92 93
	err := cmd.CheckArguments(req)
	if err != nil {
		return err
	}

94 95 96 97 98 99 100
	if cmd.PreRun != nil {
		err := cmd.PreRun(req, env)
		if err != nil {
			return err
		}
	}

101
	res, err := c.send(req)
Brian Tiger Chow's avatar
Brian Tiger Chow committed
102
	if err != nil {
103 104 105 106 107 108 109 110
		// Unwrap any URL errors. We don't really need to expose the
		// underlying HTTP nonsense to the user.
		if urlerr, ok := err.(*url.Error); ok {
			err = urlerr.Err
		}

		if netoperr, ok := err.(*net.OpError); ok && netoperr.Op == "dial" {
			// Connection refused.
111 112 113 114
			if c.fallback != nil {
				// XXX: this runs the PreRun twice
				return c.fallback.Execute(req, re, env)
			}
Devin's avatar
Devin committed
115
			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`")
116 117
		}
		return err
Brian Tiger Chow's avatar
Brian Tiger Chow committed
118 119
	}

keks's avatar
keks committed
120 121 122 123
	if cmd.PostRun != nil {
		if typer, ok := re.(interface {
			Type() cmds.PostRunType
		}); ok && cmd.PostRun[typer.Type()] != nil {
keks's avatar
keks committed
124
			err := cmd.PostRun[typer.Type()](res, re)
keks's avatar
keks committed
125 126 127 128 129 130
			closeErr := re.CloseWithError(err)
			if closeErr == cmds.ErrClosingClosedEmitter {
				// ignore double close errors
				return nil
			}

Steven Allen's avatar
Steven Allen committed
131
			return closeErr
keks's avatar
keks committed
132 133 134
		}
	}

135 136 137
	return cmds.Copy(re, res)
}

138
func (c *client) toHTTPRequest(req *cmds.Request) (*http.Request, error) {
139
	query, err := getQuery(req)
140 141 142 143
	if err != nil {
		return nil, err
	}

144
	var fileReader *files.MultiFileReader
Łukasz Magiera's avatar
Łukasz Magiera committed
145 146 147
	var reader io.Reader // in case we have no body to send we need to provide
	// untyped nil to http.NewRequest

148 149 150
	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
151 152
		fileReader = files.NewMultiFileReader(files.NewMapDirectory(map[string]files.Node{
			"stdin": files.NewReaderFile(bodyArgs),
153 154 155
		}), true)
		reader = fileReader
	} else if req.Files != nil {
156
		fileReader = files.NewMultiFileReader(req.Files, true)
157
		reader = fileReader
158 159
	}

160 161
	path := strings.Join(req.Path, "/")
	url := fmt.Sprintf(ApiUrlFormat, c.serverAddress, c.apiPrefix, path, query)
162

163
	httpReq, err := http.NewRequest("POST", url, reader)
164 165 166 167 168 169
	if err != nil {
		return nil, err
	}

	// TODO extract string consts?
	if fileReader != nil {
170
		httpReq.Header.Set(contentTypeHeader, "multipart/form-data; boundary="+fileReader.Boundary())
171
	} else {
172
		httpReq.Header.Set(contentTypeHeader, applicationOctetStream)
173
	}
174 175
	httpReq.Header.Set(uaHeader, c.ua)

176
	httpReq = httpReq.WithContext(req.Context)
rht's avatar
rht committed
177
	httpReq.Close = true
178

179 180 181
	return httpReq, nil
}

182
func (c *client) send(req *cmds.Request) (cmds.Response, error) {
183
	if req.Context == nil {
184
		log.Warnf("no context set in request")
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
		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
204 205 206 207
	httpRes, err := c.httpClient.Do(httpReq)
	if err != nil {
		return nil, err
	}
208

209
	// parse using the overridden JSON encoding in request
210
	res, err := parseResponse(httpRes, req)
rht's avatar
rht committed
211
	if err != nil {
rht's avatar
rht committed
212
		return nil, err
213
	}
rht's avatar
rht committed
214

215
	// reset request encoding to what it was before
rht's avatar
rht committed
216 217 218 219
	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.
220
		req.SetOption(cmds.EncLong, previousUserProvidedEncoding)
rht's avatar
rht committed
221 222 223
	}

	return res, nil
224 225
}

226
func getQuery(req *cmds.Request) (string, error) {
227
	query := url.Values{}
228 229

	for k, v := range req.Options {
Jeromy's avatar
Jeromy committed
230
		if OptionSkipMap[k] {
Jeromy's avatar
Jeromy committed
231 232
			continue
		}
233

234 235 236
		switch val := v.(type) {
		case []string:
			for _, o := range val {
237 238
				query.Add(k, o)
			}
239 240 241 242 243
		case bool, int, int64, uint, uint64, float64, string:
			str := fmt.Sprintf("%v", v)
			query.Set(k, str)
		default:
			return "", fmt.Errorf("unsupported query parameter type. key: %s, value: %v", k, v)
244
		}
245
	}
246

247 248
	args := req.Arguments
	argDefs := req.Command.Arguments
249

250 251 252 253 254
	argDefIndex := 0

	for _, arg := range args {
		argDef := argDefs[argDefIndex]
		// skip ArgFiles
Steven Allen's avatar
Steven Allen committed
255
		for argDef.Type == cmds.ArgFile {
256 257
			argDefIndex++
			argDef = argDefs[argDefIndex]
258 259
		}

260 261 262 263
		query.Add("arg", arg)

		if len(argDefs) > argDefIndex+1 {
			argDefIndex++
264
		}
265
	}
266

267
	return query.Encode(), nil
268
}