client.go 5.24 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 34 35 36 37 38 39 40 41 42 43 44 45 46
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
	}
}

47 48 49 50 51 52 53 54 55
// 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
	}
}

56
func NewClient(address string, opts ...ClientOpt) cmds.Executor {
57 58 59 60 61
	if !strings.HasPrefix(address, "http://") {
		address = "http://" + address
	}

	c := &client{
62
		serverAddress: address,
rht's avatar
rht committed
63
		httpClient:    http.DefaultClient,
64 65 66 67 68
		ua:            "go-ipfs-cmds/http",
	}

	for _, opt := range opts {
		opt(c)
69
	}
70 71

	return c
72
}
73

74
func (c *client) Execute(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) error {
75 76
	cmd := req.Command

77 78 79 80 81
	err := cmd.CheckArguments(req)
	if err != nil {
		return err
	}

82 83 84 85 86 87 88
	if cmd.PreRun != nil {
		err := cmd.PreRun(req, env)
		if err != nil {
			return err
		}
	}

89
	res, err := c.send(req)
Brian Tiger Chow's avatar
Brian Tiger Chow committed
90
	if err != nil {
91
		if isConnRefused(err) {
92 93 94 95
			if c.fallback != nil {
				// XXX: this runs the PreRun twice
				return c.fallback.Execute(req, re, env)
			}
Devin's avatar
Devin committed
96
			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`")
97 98
		}
		return err
Brian Tiger Chow's avatar
Brian Tiger Chow committed
99 100
	}

keks's avatar
keks committed
101 102 103 104
	if cmd.PostRun != nil {
		if typer, ok := re.(interface {
			Type() cmds.PostRunType
		}); ok && cmd.PostRun[typer.Type()] != nil {
keks's avatar
keks committed
105
			err := cmd.PostRun[typer.Type()](res, re)
keks's avatar
keks committed
106 107 108 109 110 111
			closeErr := re.CloseWithError(err)
			if closeErr == cmds.ErrClosingClosedEmitter {
				// ignore double close errors
				return nil
			}

Steven Allen's avatar
Steven Allen committed
112
			return closeErr
keks's avatar
keks committed
113 114 115
		}
	}

116 117 118
	return cmds.Copy(re, res)
}

119
func (c *client) toHTTPRequest(req *cmds.Request) (*http.Request, error) {
120
	query, err := getQuery(req)
121 122 123 124
	if err != nil {
		return nil, err
	}

125
	var fileReader *files.MultiFileReader
Łukasz Magiera's avatar
Łukasz Magiera committed
126 127 128
	var reader io.Reader // in case we have no body to send we need to provide
	// untyped nil to http.NewRequest

129 130 131
	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
132 133
		fileReader = files.NewMultiFileReader(files.NewMapDirectory(map[string]files.Node{
			"stdin": files.NewReaderFile(bodyArgs),
134 135 136
		}), true)
		reader = fileReader
	} else if req.Files != nil {
137
		fileReader = files.NewMultiFileReader(req.Files, true)
138
		reader = fileReader
139 140
	}

141 142
	path := strings.Join(req.Path, "/")
	url := fmt.Sprintf(ApiUrlFormat, c.serverAddress, c.apiPrefix, path, query)
143

144
	httpReq, err := http.NewRequest("POST", url, reader)
145 146 147 148 149 150
	if err != nil {
		return nil, err
	}

	// TODO extract string consts?
	if fileReader != nil {
151
		httpReq.Header.Set(contentTypeHeader, "multipart/form-data; boundary="+fileReader.Boundary())
152
	} else {
153
		httpReq.Header.Set(contentTypeHeader, applicationOctetStream)
154
	}
155 156
	httpReq.Header.Set(uaHeader, c.ua)

157
	httpReq = httpReq.WithContext(req.Context)
rht's avatar
rht committed
158
	httpReq.Close = true
159

160 161 162
	return httpReq, nil
}

163
func (c *client) send(req *cmds.Request) (cmds.Response, error) {
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
	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
185 186 187 188
	httpRes, err := c.httpClient.Do(httpReq)
	if err != nil {
		return nil, err
	}
189

190
	// parse using the overridden JSON encoding in request
191
	res, err := parseResponse(httpRes, req)
rht's avatar
rht committed
192
	if err != nil {
rht's avatar
rht committed
193
		return nil, err
194
	}
rht's avatar
rht committed
195

196
	// reset request encoding to what it was before
rht's avatar
rht committed
197 198 199 200
	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.
201
		req.SetOption(cmds.EncLong, previousUserProvidedEncoding)
rht's avatar
rht committed
202 203 204
	}

	return res, nil
205 206
}

207
func getQuery(req *cmds.Request) (string, error) {
208
	query := url.Values{}
209 210

	for k, v := range req.Options {
Jeromy's avatar
Jeromy committed
211
		if OptionSkipMap[k] {
Jeromy's avatar
Jeromy committed
212 213
			continue
		}
214
		str := fmt.Sprintf("%v", v)
215
		query.Set(k, str)
216
	}
217

218 219
	args := req.Arguments
	argDefs := req.Command.Arguments
220

221 222 223 224 225
	argDefIndex := 0

	for _, arg := range args {
		argDef := argDefs[argDefIndex]
		// skip ArgFiles
Steven Allen's avatar
Steven Allen committed
226
		for argDef.Type == cmds.ArgFile {
227 228
			argDefIndex++
			argDef = argDefs[argDefIndex]
229 230
		}

231 232 233 234
		query.Add("arg", arg)

		if len(argDefs) > argDefIndex+1 {
			argDefIndex++
235
		}
236
	}
237

238
	return query.Encode(), nil
239
}
240 241 242 243 244 245 246 247 248 249 250 251 252 253

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"
}