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

13
	cmds "gitlab.dms3.io/dms3/go-dms3-cmds"
14

15
	files "gitlab.dms3.io/dms3/go-dms3-files"
tavit ohanian's avatar
tavit ohanian committed
16
	loggables "gitlab.dms3.io/p2p/go-p2p-loggables"
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
	}

tavit ohanian's avatar
tavit ohanian committed
168 169
	uuid := uuidLoggable()
	req, err := cmds.NewRequest(r.Context(), pth, opts, args, f, root)
170
	if err != nil {
tavit ohanian's avatar
tavit ohanian committed
171
		log.Errorf("failed to create request for %s", uuid)
172 173
		return nil, err
	}
174 175 176 177 178 179

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

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

184 185 186 187 188 189
// 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},
190 191
	}

192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
	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)
209
		} else if encType != "text" {
210
			log.Errorf("could not find decoder for encoding %q", encType)
211
		} // else we have an io.Reader, which is okay
212 213
	} else {
		log.Errorf("could not guess encoding from content type %q", contentType)
214 215 216 217
	}

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

		switch {
		case httpRes.StatusCode == http.StatusNotFound:
			// handle 404s
			e.Message = "Command not found."
Steven Allen's avatar
Steven Allen committed
224
			e.Code = cmds.ErrClient
225 226 227 228 229 230 231
		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
232 233 234 235 236 237 238 239 240 241
			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
			}
242 243
		case res.dec == nil:
			return nil, fmt.Errorf("unknown error content type: %s", contentType)
244
		default:
245
			// handle errors from value
246 247
			err := res.dec.Decode(e)
			if err != nil {
keks's avatar
vet  
keks committed
248
				log.Errorf("error parsing error %q", err.Error())
249
			}
250 251
		}

252
		return nil, e
253 254 255
	}

	return res, nil
256
}
Steven Allen's avatar
Steven Allen committed
257

tavit ohanian's avatar
tavit ohanian committed
258 259
func uuidLoggable() loggables.DeferredMap {
	m := loggables.DeferredMap{}
Steven Allen's avatar
Steven Allen committed
260 261 262
	ids := make([]byte, 16)
	rand.Read(ids)

tavit ohanian's avatar
tavit ohanian committed
263 264
	m["requestId"] = base32.HexEncoding.EncodeToString(ids)
	return m
Steven Allen's avatar
Steven Allen committed
265
}