parse.go 5.58 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"

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

	"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

Jan Winkelmann's avatar
Jan Winkelmann committed
31
	cmd, err := root.Get(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
	}

keks's avatar
cleanup  
keks committed
37
	sub := cmd.Subcommands[pth[len(pth)-1]]
Jan Winkelmann's avatar
Jan Winkelmann committed
38 39

	if sub == nil {
40
		if cmd.Run == nil {
41 42 43
			return nil, ErrNotFound
		}

Matt Bell's avatar
Matt Bell committed
44 45
		// 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
46 47
		stringArgs = append(stringArgs, pth[len(pth)-1])
		pth = pth[:len(pth)-1]
48 49
	} else {
		cmd = sub
50 51
	}

52
	opts, stringArgs2 := parseOptions(r)
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
	optDefs, err := root.GetOptions(pth)
	if err != nil {
		return nil, err
	}
	for k, v := range opts {
		if optDef, ok := optDefs[k]; ok {
			name := optDef.Names()[0]
			if k != name {
				opts[name] = v
				delete(opts, k)
			}
		}
	}
	// default to setting encoding to JSON
	if _, ok := opts[cmds.EncLong]; !ok {
		opts[cmds.EncLong] = cmds.JSON
	}

71 72
	stringArgs = append(stringArgs, stringArgs2...)

73
	// count required argument definitions
74
	numRequired := 0
75 76
	for _, argDef := range cmd.Arguments {
		if argDef.Required {
77
			numRequired++
78 79 80 81 82 83
		}
	}

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

84
	args := make([]string, valCount)
85 86

	valIndex := 0
87
	requiredFile := ""
88 89
	for _, argDef := range cmd.Arguments {
		// skip optional argument definitions if there aren't sufficient remaining values
90
		if valCount-valIndex <= numRequired && !argDef.Required {
91
			continue
92
		} else if argDef.Required {
93
			numRequired--
94 95
		}

Steven Allen's avatar
Steven Allen committed
96
		if argDef.Type == cmds.ArgString {
97
			if argDef.Variadic {
98
				for _, s := range stringArgs {
99 100
					args[valIndex] = s
					valIndex++
101
				}
Matt Bell's avatar
Matt Bell committed
102
				valCount -= len(stringArgs)
103 104

			} else if len(stringArgs) > 0 {
105
				args[valIndex] = stringArgs[0]
106
				stringArgs = stringArgs[1:]
107
				valIndex++
108 109 110

			} else {
				break
111
			}
Steven Allen's avatar
Steven Allen committed
112
		} else if argDef.Type == cmds.ArgFile && argDef.Required && len(requiredFile) == 0 {
113
			requiredFile = argDef.Name
114 115
		}
	}
116

117 118
	// create cmds.File from multipart/form-data contents
	contentType := r.Header.Get(contentTypeHeader)
119 120
	mediatype, _, _ := mime.ParseMediaType(contentType)

Łukasz Magiera's avatar
Łukasz Magiera committed
121
	var f files.Directory
122
	if mediatype == "multipart/form-data" {
123
		reader, err := r.MultipartReader()
124 125 126
		if err != nil {
			return nil, err
		}
127

Łukasz Magiera's avatar
Łukasz Magiera committed
128 129 130
		f, err = files.NewFileFromPartReader(reader, mediatype)
		if err != nil {
			return nil, err
131
		}
132 133
	}

134 135 136 137 138
	// if there is a required filearg, error if no files were provided
	if len(requiredFile) > 0 && f == nil {
		return nil, fmt.Errorf("File argument '%s' is required", requiredFile)
	}

Steven Allen's avatar
Steven Allen committed
139
	ctx := logging.ContextWithLoggable(r.Context(), uuidLoggable())
140
	req, err := cmds.NewRequest(ctx, pth, opts, args, f, root)
141 142 143
	if err != nil {
		return nil, err
	}
144 145 146 147 148 149

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

150 151
	err = req.FillDefaults()
	return req, err
152 153
}

154
func parseOptions(r *http.Request) (map[string]interface{}, []string) {
155
	opts := make(map[string]interface{})
156
	var args []string
157 158 159

	query := r.URL.Query()
	for k, v := range query {
Matt Bell's avatar
Matt Bell committed
160
		if k == "arg" {
161
			args = v
Matt Bell's avatar
Matt Bell committed
162
		} else {
163

Matt Bell's avatar
Matt Bell committed
164 165
			opts[k] = v[0]
		}
166 167
	}

168 169 170 171 172 173 174 175 176
	return opts, args
}

// 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},
177 178
	}

179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
	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)
196
		} else if encType != "text" {
197
			log.Errorf("could not find decoder for encoding %q", encType)
198
		} // else we have an io.Reader, which is okay
199 200
	} else {
		log.Errorf("could not guess encoding from content type %q", contentType)
201 202 203 204
	}

	// If we ran into an error
	if httpRes.StatusCode >= http.StatusBadRequest {
Steven Allen's avatar
Steven Allen committed
205
		e := &cmds.Error{}
206 207 208 209 210

		switch {
		case httpRes.StatusCode == http.StatusNotFound:
			// handle 404s
			e.Message = "Command not found."
Steven Allen's avatar
Steven Allen committed
211
			e.Code = cmds.ErrClient
212 213 214 215 216 217 218
		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
219 220 221 222 223 224 225 226 227 228
			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
			}
229 230
		case res.dec == nil:
			return nil, fmt.Errorf("unknown error content type: %s", contentType)
231
		default:
232
			// handle errors from value
233 234
			err := res.dec.Decode(e)
			if err != nil {
keks's avatar
vet  
keks committed
235
				log.Errorf("error parsing error %q", err.Error())
236
			}
237 238
		}

239
		return nil, e
240 241 242
	}

	return res, nil
243
}
Steven Allen's avatar
Steven Allen committed
244 245 246 247 248 249 250 251 252

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

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