parse.go 6.03 KB
Newer Older
1 2 3
package http

import (
Steven Allen's avatar
Steven Allen committed
4
	"encoding/base32"
5
	"fmt"
6
	"io/ioutil"
Steven Allen's avatar
Steven Allen committed
7
	"math/rand"
8
	"mime"
9
	"net/http"
10
	"strconv"
11 12
	"strings"

Hector Sanjuan's avatar
Hector Sanjuan committed
13
	cmds "github.com/ipfs/go-ipfs-cmds"
14

Hector Sanjuan's avatar
Hector Sanjuan committed
15
	files "github.com/ipfs/go-ipfs-files"
Steven Allen's avatar
Steven Allen committed
16
	logging "github.com/ipfs/go-log"
17 18
)

19
// parseRequest parses the data in a http.Request and returns a command Request object
Steven Allen's avatar
Steven Allen committed
20
func parseRequest(r *http.Request, root *cmds.Command) (*cmds.Request, error) {
21 22
	if r.URL.Path[0] == '/' {
		r.URL.Path = r.URL.Path[1:]
23 24
	}

25 26 27 28 29
	var (
		stringArgs []string
		pth        = strings.Split(r.URL.Path, "/")
		getPath    = pth[:len(pth)-1]
	)
30

31
	cmdPath, err := root.Resolve(getPath)
32
	if err != nil {
Matt Bell's avatar
Matt Bell committed
33
		// 404 if there is no command at that path
34
		return nil, ErrNotFound
rht's avatar
rht committed
35 36
	}

37 38 39 40 41 42 43
	for _, c := range cmdPath {
		if c.NoRemote {
			return nil, ErrNotFound
		}
	}

	cmd := cmdPath[len(cmdPath)-1]
keks's avatar
cleanup  
keks committed
44
	sub := cmd.Subcommands[pth[len(pth)-1]]
Jan Winkelmann's avatar
Jan Winkelmann committed
45 46

	if sub == nil {
47
		if cmd.Run == nil {
48 49 50
			return nil, ErrNotFound
		}

Matt Bell's avatar
Matt Bell committed
51 52
		// if the last string in the path isn't a subcommand, use it as an argument
		// e.g. /objects/Qabc12345 (we are passing "Qabc12345" to the "objects" command)
rht's avatar
rht committed
53 54
		stringArgs = append(stringArgs, pth[len(pth)-1])
		pth = pth[:len(pth)-1]
55 56
	} else {
		cmd = sub
57 58
	}

59 60 61 62
	if cmd.NoRemote {
		return nil, ErrNotFound
	}

63
	opts := make(map[string]interface{})
64 65 66 67
	optDefs, err := root.GetOptions(pth)
	if err != nil {
		return nil, err
	}
68 69 70 71 72 73 74 75 76 77 78

	query := r.URL.Query()
	// Note: len(v) is guaranteed by the above function to always be greater than 0
	for k, v := range query {
		if k == "arg" {
			stringArgs = append(stringArgs, v...)
		} else {
			optDef, ok := optDefs[k]
			if !ok {
				opts[k] = v[0]
				continue
79 80
			}

81 82 83 84 85 86 87 88 89 90 91 92 93
			name := optDef.Names()[0]
			opts[name] = v

			switch optType := optDef.Type(); optType {
			case cmds.Strings:
				opts[name] = v
			case cmds.Bool, cmds.Int, cmds.Int64, cmds.Uint, cmds.Uint64, cmds.Float, cmds.String:
				if len(v) > 1 {
					return nil, fmt.Errorf("expected key %s to have only a single value, received %v", name, v)
				}
				opts[name] = v[0]
			default:
				return nil, fmt.Errorf("unsupported option type. key: %s, type: %v", k, optType)
94 95 96 97
			}
		}
	}
	// default to setting encoding to JSON
98 99
	if _, ok := opts[cmds.EncLong]; !ok {
		opts[cmds.EncLong] = cmds.JSON
100 101
	}

102
	// count required argument definitions
103
	numRequired := 0
104 105
	for _, argDef := range cmd.Arguments {
		if argDef.Required {
106
			numRequired++
107 108 109 110 111 112
		}
	}

	// count the number of provided argument values
	valCount := len(stringArgs)

113
	args := make([]string, valCount)
114 115

	valIndex := 0
116
	requiredFile := ""
117 118
	for _, argDef := range cmd.Arguments {
		// skip optional argument definitions if there aren't sufficient remaining values
119
		if valCount-valIndex <= numRequired && !argDef.Required {
120
			continue
121
		} else if argDef.Required {
122
			numRequired--
123 124
		}

Steven Allen's avatar
Steven Allen committed
125
		if argDef.Type == cmds.ArgString {
126
			if argDef.Variadic {
127
				for _, s := range stringArgs {
128 129
					args[valIndex] = s
					valIndex++
130
				}
Matt Bell's avatar
Matt Bell committed
131
				valCount -= len(stringArgs)
132 133

			} else if len(stringArgs) > 0 {
134
				args[valIndex] = stringArgs[0]
135
				stringArgs = stringArgs[1:]
136
				valIndex++
137 138 139

			} else {
				break
140
			}
Steven Allen's avatar
Steven Allen committed
141
		} else if argDef.Type == cmds.ArgFile && argDef.Required && len(requiredFile) == 0 {
142
			requiredFile = argDef.Name
143 144
		}
	}
145

146 147
	// create cmds.File from multipart/form-data contents
	contentType := r.Header.Get(contentTypeHeader)
148 149
	mediatype, _, _ := mime.ParseMediaType(contentType)

Łukasz Magiera's avatar
Łukasz Magiera committed
150
	var f files.Directory
151
	if mediatype == "multipart/form-data" {
152
		reader, err := r.MultipartReader()
153 154 155
		if err != nil {
			return nil, err
		}
156

Łukasz Magiera's avatar
Łukasz Magiera committed
157 158 159
		f, err = files.NewFileFromPartReader(reader, mediatype)
		if err != nil {
			return nil, err
160
		}
161 162
	}

163 164
	// if there is a required filearg, error if no files were provided
	if len(requiredFile) > 0 && f == nil {
Hector Sanjuan's avatar
Hector Sanjuan committed
165
		return nil, fmt.Errorf("file argument '%s' is required", requiredFile)
166 167
	}

Steven Allen's avatar
Steven Allen committed
168
	ctx := logging.ContextWithLoggable(r.Context(), uuidLoggable())
169
	req, err := cmds.NewRequest(ctx, pth, opts, args, f, root)
170 171 172
	if err != nil {
		return nil, err
	}
173 174 175 176 177 178

	err = cmd.CheckArguments(req)
	if err != nil {
		return nil, err
	}

179 180
	err = req.FillDefaults()
	return req, err
181 182
}

183 184 185 186 187 188
// parseResponse decodes a http.Response to create a cmds.Response
func parseResponse(httpRes *http.Response, req *cmds.Request) (cmds.Response, error) {
	res := &Response{
		res: httpRes,
		req: req,
		rr:  &responseReader{httpRes},
189 190
	}

191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
	lengthHeader := httpRes.Header.Get(extraContentLengthHeader)
	if len(lengthHeader) > 0 {
		length, err := strconv.ParseUint(lengthHeader, 10, 64)
		if err != nil {
			return nil, err
		}
		res.length = length
	}

	contentType := httpRes.Header.Get(contentTypeHeader)
	contentType = strings.Split(contentType, ";")[0]

	encType, found := MIMEEncodings[contentType]
	if found {
		makeDec, ok := cmds.Decoders[encType]
		if ok {
			res.dec = makeDec(res.rr)
208
		} else if encType != "text" {
209
			log.Errorf("could not find decoder for encoding %q", encType)
210
		} // else we have an io.Reader, which is okay
211 212
	} else {
		log.Errorf("could not guess encoding from content type %q", contentType)
213 214 215 216
	}

	// If we ran into an error
	if httpRes.StatusCode >= http.StatusBadRequest {
Steven Allen's avatar
Steven Allen committed
217
		e := &cmds.Error{}
218 219 220 221 222

		switch {
		case httpRes.StatusCode == http.StatusNotFound:
			// handle 404s
			e.Message = "Command not found."
Steven Allen's avatar
Steven Allen committed
223
			e.Code = cmds.ErrClient
224 225 226 227 228 229 230
		case contentType == plainText:
			// handle non-marshalled errors
			mes, err := ioutil.ReadAll(res.rr)
			if err != nil {
				return nil, err
			}
			e.Message = string(mes)
Steven Allen's avatar
Steven Allen committed
231 232 233 234 235 236 237 238 239 240
			switch httpRes.StatusCode {
			case http.StatusNotFound, http.StatusBadRequest:
				e.Code = cmds.ErrClient
			case http.StatusTooManyRequests:
				e.Code = cmds.ErrRateLimited
			case http.StatusForbidden:
				e.Code = cmds.ErrForbidden
			default:
				e.Code = cmds.ErrNormal
			}
241 242
		case res.dec == nil:
			return nil, fmt.Errorf("unknown error content type: %s", contentType)
243
		default:
244
			// handle errors from value
245 246
			err := res.dec.Decode(e)
			if err != nil {
keks's avatar
vet  
keks committed
247
				log.Errorf("error parsing error %q", err.Error())
248
			}
249 250
		}

251
		return nil, e
252 253 254
	}

	return res, nil
255
}
Steven Allen's avatar
Steven Allen committed
256 257 258 259 260 261 262 263 264

func uuidLoggable() logging.Loggable {
	ids := make([]byte, 16)
	rand.Read(ids)

	return logging.Metadata{
		"requestId": base32.HexEncoding.EncodeToString(ids),
	}
}