1// Copyright (c) 2015-2019 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
2// resty source code and usage is governed by a MIT style
3// license that can be found in the LICENSE file.
4
5package resty
6
7import (
8	"encoding/base64"
9	"encoding/json"
10	"encoding/xml"
11	"fmt"
12	"io"
13	"net"
14	"net/url"
15	"reflect"
16	"strings"
17)
18
19// SRVRecord holds the data to query the SRV record for the following service
20type SRVRecord struct {
21	Service string
22	Domain  string
23}
24
25// SetHeader method is to set a single header field and its value in the current request.
26// Example: To set `Content-Type` and `Accept` as `application/json`.
27// 		resty.R().
28//			SetHeader("Content-Type", "application/json").
29//			SetHeader("Accept", "application/json")
30//
31// Also you can override header value, which was set at client instance level.
32//
33func (r *Request) SetHeader(header, value string) *Request {
34	r.Header.Set(header, value)
35	return r
36}
37
38// SetHeaders method sets multiple headers field and its values at one go in the current request.
39// Example: To set `Content-Type` and `Accept` as `application/json`
40//
41// 		resty.R().
42//			SetHeaders(map[string]string{
43//				"Content-Type": "application/json",
44//				"Accept": "application/json",
45//			})
46// Also you can override header value, which was set at client instance level.
47//
48func (r *Request) SetHeaders(headers map[string]string) *Request {
49	for h, v := range headers {
50		r.SetHeader(h, v)
51	}
52
53	return r
54}
55
56// SetQueryParam method sets single parameter and its value in the current request.
57// It will be formed as query string for the request.
58// Example: `search=kitchen%20papers&size=large` in the URL after `?` mark.
59// 		resty.R().
60//			SetQueryParam("search", "kitchen papers").
61//			SetQueryParam("size", "large")
62// Also you can override query params value, which was set at client instance level
63//
64func (r *Request) SetQueryParam(param, value string) *Request {
65	r.QueryParam.Set(param, value)
66	return r
67}
68
69// SetQueryParams method sets multiple parameters and its values at one go in the current request.
70// It will be formed as query string for the request.
71// Example: `search=kitchen%20papers&size=large` in the URL after `?` mark.
72// 		resty.R().
73//			SetQueryParams(map[string]string{
74//				"search": "kitchen papers",
75//				"size": "large",
76//			})
77// Also you can override query params value, which was set at client instance level
78//
79func (r *Request) SetQueryParams(params map[string]string) *Request {
80	for p, v := range params {
81		r.SetQueryParam(p, v)
82	}
83
84	return r
85}
86
87// SetMultiValueQueryParams method appends multiple parameters with multi-value
88// at one go in the current request. It will be formed as query string for the request.
89// Example: `status=pending&status=approved&status=open` in the URL after `?` mark.
90// 		resty.R().
91//			SetMultiValueQueryParams(url.Values{
92//				"status": []string{"pending", "approved", "open"},
93//			})
94// Also you can override query params value, which was set at client instance level
95//
96func (r *Request) SetMultiValueQueryParams(params url.Values) *Request {
97	for p, v := range params {
98		for _, pv := range v {
99			r.QueryParam.Add(p, pv)
100		}
101	}
102
103	return r
104}
105
106// SetQueryString method provides ability to use string as an input to set URL query string for the request.
107//
108// Using String as an input
109// 		resty.R().
110//			SetQueryString("productId=232&template=fresh-sample&cat=resty&source=google&kw=buy a lot more")
111//
112func (r *Request) SetQueryString(query string) *Request {
113	params, err := url.ParseQuery(strings.TrimSpace(query))
114	if err == nil {
115		for p, v := range params {
116			for _, pv := range v {
117				r.QueryParam.Add(p, pv)
118			}
119		}
120	} else {
121		r.client.Log.Printf("ERROR %v", err)
122	}
123	return r
124}
125
126// SetFormData method sets Form parameters and their values in the current request.
127// It's applicable only HTTP method `POST` and `PUT` and requests content type would be set as
128// `application/x-www-form-urlencoded`.
129// 		resty.R().
130// 			SetFormData(map[string]string{
131//				"access_token": "BC594900-518B-4F7E-AC75-BD37F019E08F",
132//				"user_id": "3455454545",
133//			})
134// Also you can override form data value, which was set at client instance level
135//
136func (r *Request) SetFormData(data map[string]string) *Request {
137	for k, v := range data {
138		r.FormData.Set(k, v)
139	}
140
141	return r
142}
143
144// SetMultiValueFormData method appends multiple form parameters with multi-value
145// at one go in the current request.
146// 		resty.R().
147//			SetMultiValueFormData(url.Values{
148//				"search_criteria": []string{"book", "glass", "pencil"},
149//			})
150// Also you can override form data value, which was set at client instance level
151//
152func (r *Request) SetMultiValueFormData(params url.Values) *Request {
153	for k, v := range params {
154		for _, kv := range v {
155			r.FormData.Add(k, kv)
156		}
157	}
158
159	return r
160}
161
162// SetBody method sets the request body for the request. It supports various realtime needs as easy.
163// We can say its quite handy or powerful. Supported request body data types is `string`,
164// `[]byte`, `struct`, `map`, `slice` and `io.Reader`. Body value can be pointer or non-pointer.
165// Automatic marshalling for JSON and XML content type, if it is `struct`, `map`, or `slice`.
166//
167// Note: `io.Reader` is processed as bufferless mode while sending request.
168//
169// Example:
170//
171// Struct as a body input, based on content type, it will be marshalled.
172//		resty.R().
173//			SetBody(User{
174//				Username: "jeeva@myjeeva.com",
175//				Password: "welcome2resty",
176//			})
177//
178// Map as a body input, based on content type, it will be marshalled.
179//		resty.R().
180//			SetBody(map[string]interface{}{
181//				"username": "jeeva@myjeeva.com",
182//				"password": "welcome2resty",
183//				"address": &Address{
184//					Address1: "1111 This is my street",
185//					Address2: "Apt 201",
186//					City: "My City",
187//					State: "My State",
188//					ZipCode: 00000,
189//				},
190//			})
191//
192// String as a body input. Suitable for any need as a string input.
193//		resty.R().
194//			SetBody(`{
195//				"username": "jeeva@getrightcare.com",
196//				"password": "admin"
197//			}`)
198//
199// []byte as a body input. Suitable for raw request such as file upload, serialize & deserialize, etc.
200// 		resty.R().
201//			SetBody([]byte("This is my raw request, sent as-is"))
202//
203func (r *Request) SetBody(body interface{}) *Request {
204	r.Body = body
205	return r
206}
207
208// SetResult method is to register the response `Result` object for automatic unmarshalling in the RESTful mode
209// if response status code is between 200 and 299 and content type either JSON or XML.
210//
211// Note: Result object can be pointer or non-pointer.
212//		resty.R().SetResult(&AuthToken{})
213//		// OR
214//		resty.R().SetResult(AuthToken{})
215//
216// Accessing a result value
217//		response.Result().(*AuthToken)
218//
219func (r *Request) SetResult(res interface{}) *Request {
220	r.Result = getPointer(res)
221	return r
222}
223
224// SetError method is to register the request `Error` object for automatic unmarshalling in the RESTful mode
225// if response status code is greater than 399 and content type either JSON or XML.
226//
227// Note: Error object can be pointer or non-pointer.
228// 		resty.R().SetError(&AuthError{})
229//		// OR
230//		resty.R().SetError(AuthError{})
231//
232// Accessing a error value
233//		response.Error().(*AuthError)
234//
235func (r *Request) SetError(err interface{}) *Request {
236	r.Error = getPointer(err)
237	return r
238}
239
240// SetFile method is to set single file field name and its path for multipart upload.
241//	resty.R().
242//		SetFile("my_file", "/Users/jeeva/Gas Bill - Sep.pdf")
243//
244func (r *Request) SetFile(param, filePath string) *Request {
245	r.isMultiPart = true
246	r.FormData.Set("@"+param, filePath)
247	return r
248}
249
250// SetFiles method is to set multiple file field name and its path for multipart upload.
251//	resty.R().
252//		SetFiles(map[string]string{
253//				"my_file1": "/Users/jeeva/Gas Bill - Sep.pdf",
254//				"my_file2": "/Users/jeeva/Electricity Bill - Sep.pdf",
255//				"my_file3": "/Users/jeeva/Water Bill - Sep.pdf",
256//			})
257//
258func (r *Request) SetFiles(files map[string]string) *Request {
259	r.isMultiPart = true
260
261	for f, fp := range files {
262		r.FormData.Set("@"+f, fp)
263	}
264
265	return r
266}
267
268// SetFileReader method is to set single file using io.Reader for multipart upload.
269//	resty.R().
270//		SetFileReader("profile_img", "my-profile-img.png", bytes.NewReader(profileImgBytes)).
271//		SetFileReader("notes", "user-notes.txt", bytes.NewReader(notesBytes))
272//
273func (r *Request) SetFileReader(param, fileName string, reader io.Reader) *Request {
274	r.isMultiPart = true
275	r.multipartFiles = append(r.multipartFiles, &File{
276		Name:      fileName,
277		ParamName: param,
278		Reader:    reader,
279	})
280	return r
281}
282
283// SetMultipartField method is to set custom data using io.Reader for multipart upload.
284func (r *Request) SetMultipartField(param, fileName, contentType string, reader io.Reader) *Request {
285	r.isMultiPart = true
286	r.multipartFields = append(r.multipartFields, &MultipartField{
287		Param:       param,
288		FileName:    fileName,
289		ContentType: contentType,
290		Reader:      reader,
291	})
292	return r
293}
294
295// SetMultipartFields method is to set multiple data fields using io.Reader for multipart upload.
296// Example:
297// 	resty.R().SetMultipartFields(
298// 		&resty.MultipartField{
299//			Param:       "uploadManifest1",
300//			FileName:    "upload-file-1.json",
301//			ContentType: "application/json",
302//			Reader:      strings.NewReader(`{"input": {"name": "Uploaded document 1", "_filename" : ["file1.txt"]}}`),
303//		},
304//		&resty.MultipartField{
305//			Param:       "uploadManifest2",
306//			FileName:    "upload-file-2.json",
307//			ContentType: "application/json",
308//			Reader:      strings.NewReader(`{"input": {"name": "Uploaded document 2", "_filename" : ["file2.txt"]}}`),
309//		})
310//
311// If you have slice already, then simply call-
312// 	resty.R().SetMultipartFields(fields...)
313func (r *Request) SetMultipartFields(fields ...*MultipartField) *Request {
314	r.isMultiPart = true
315	r.multipartFields = append(r.multipartFields, fields...)
316	return r
317}
318
319// SetContentLength method sets the HTTP header `Content-Length` value for current request.
320// By default go-resty won't set `Content-Length`. Also you have an option to enable for every
321// request. See `resty.SetContentLength`
322// 		resty.R().SetContentLength(true)
323//
324func (r *Request) SetContentLength(l bool) *Request {
325	r.setContentLength = true
326	return r
327}
328
329// SetBasicAuth method sets the basic authentication header in the current HTTP request.
330// For Header example:
331//		Authorization: Basic <base64-encoded-value>
332//
333// To set the header for username "go-resty" and password "welcome"
334// 		resty.R().SetBasicAuth("go-resty", "welcome")
335//
336// This method overrides the credentials set by method `resty.SetBasicAuth`.
337//
338func (r *Request) SetBasicAuth(username, password string) *Request {
339	r.UserInfo = &User{Username: username, Password: password}
340	return r
341}
342
343// SetAuthToken method sets bearer auth token header in the current HTTP request. Header example:
344// 		Authorization: Bearer <auth-token-value-comes-here>
345//
346// Example: To set auth token BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F
347//
348// 		resty.R().SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F")
349//
350// This method overrides the Auth token set by method `resty.SetAuthToken`.
351//
352func (r *Request) SetAuthToken(token string) *Request {
353	r.Token = token
354	return r
355}
356
357// SetOutput method sets the output file for current HTTP request. Current HTTP response will be
358// saved into given file. It is similar to `curl -o` flag. Absolute path or relative path can be used.
359// If is it relative path then output file goes under the output directory, as mentioned
360// in the `Client.SetOutputDirectory`.
361// 		resty.R().
362// 			SetOutput("/Users/jeeva/Downloads/ReplyWithHeader-v5.1-beta.zip").
363// 			Get("http://bit.ly/1LouEKr")
364//
365// Note: In this scenario `Response.Body` might be nil.
366func (r *Request) SetOutput(file string) *Request {
367	r.outputFile = file
368	r.isSaveResponse = true
369	return r
370}
371
372// SetSRV method sets the details to query the service SRV record and execute the
373// request.
374// 		resty.R().
375//			SetSRV(SRVRecord{"web", "testservice.com"}).
376//			Get("/get")
377func (r *Request) SetSRV(srv *SRVRecord) *Request {
378	r.SRV = srv
379	return r
380}
381
382// SetDoNotParseResponse method instructs `Resty` not to parse the response body automatically.
383// Resty exposes the raw response body as `io.ReadCloser`. Also do not forget to close the body,
384// otherwise you might get into connection leaks, no connection reuse.
385//
386// Please Note: Response middlewares are not applicable, if you use this option. Basically you have
387// taken over the control of response parsing from `Resty`.
388func (r *Request) SetDoNotParseResponse(parse bool) *Request {
389	r.notParseResponse = parse
390	return r
391}
392
393// SetPathParams method sets multiple URL path key-value pairs at one go in the
394// resty current request instance.
395// 		resty.R().SetPathParams(map[string]string{
396// 		   "userId": "sample@sample.com",
397// 		   "subAccountId": "100002",
398// 		})
399//
400// 		Result:
401// 		   URL - /v1/users/{userId}/{subAccountId}/details
402// 		   Composed URL - /v1/users/sample@sample.com/100002/details
403// It replace the value of the key while composing request URL. Also you can
404// override Path Params value, which was set at client instance level.
405func (r *Request) SetPathParams(params map[string]string) *Request {
406	for p, v := range params {
407		r.pathParams[p] = v
408	}
409	return r
410}
411
412// ExpectContentType method allows to provide fallback `Content-Type` for automatic unmarshalling
413// when `Content-Type` response header is unavailable.
414func (r *Request) ExpectContentType(contentType string) *Request {
415	r.fallbackContentType = contentType
416	return r
417}
418
419// SetJSONEscapeHTML method is to enable/disable the HTML escape on JSON marshal.
420//
421// NOTE: This option only applicable to standard JSON Marshaller.
422func (r *Request) SetJSONEscapeHTML(b bool) *Request {
423	r.jsonEscapeHTML = b
424	return r
425}
426
427//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
428// HTTP verb method starts here
429//___________________________________
430
431// Get method does GET HTTP request. It's defined in section 4.3.1 of RFC7231.
432func (r *Request) Get(url string) (*Response, error) {
433	return r.Execute(MethodGet, url)
434}
435
436// Head method does HEAD HTTP request. It's defined in section 4.3.2 of RFC7231.
437func (r *Request) Head(url string) (*Response, error) {
438	return r.Execute(MethodHead, url)
439}
440
441// Post method does POST HTTP request. It's defined in section 4.3.3 of RFC7231.
442func (r *Request) Post(url string) (*Response, error) {
443	return r.Execute(MethodPost, url)
444}
445
446// Put method does PUT HTTP request. It's defined in section 4.3.4 of RFC7231.
447func (r *Request) Put(url string) (*Response, error) {
448	return r.Execute(MethodPut, url)
449}
450
451// Delete method does DELETE HTTP request. It's defined in section 4.3.5 of RFC7231.
452func (r *Request) Delete(url string) (*Response, error) {
453	return r.Execute(MethodDelete, url)
454}
455
456// Options method does OPTIONS HTTP request. It's defined in section 4.3.7 of RFC7231.
457func (r *Request) Options(url string) (*Response, error) {
458	return r.Execute(MethodOptions, url)
459}
460
461// Patch method does PATCH HTTP request. It's defined in section 2 of RFC5789.
462func (r *Request) Patch(url string) (*Response, error) {
463	return r.Execute(MethodPatch, url)
464}
465
466// Execute method performs the HTTP request with given HTTP method and URL
467// for current `Request`.
468// 		resp, err := resty.R().Execute(resty.GET, "http://httpbin.org/get")
469//
470func (r *Request) Execute(method, url string) (*Response, error) {
471	var addrs []*net.SRV
472	var err error
473
474	if r.isMultiPart && !(method == MethodPost || method == MethodPut) {
475		return nil, fmt.Errorf("multipart content is not allowed in HTTP verb [%v]", method)
476	}
477
478	if r.SRV != nil {
479		_, addrs, err = net.LookupSRV(r.SRV.Service, "tcp", r.SRV.Domain)
480		if err != nil {
481			return nil, err
482		}
483	}
484
485	r.Method = method
486	r.URL = r.selectAddr(addrs, url, 0)
487
488	if r.client.RetryCount == 0 {
489		return r.client.execute(r)
490	}
491
492	var resp *Response
493	attempt := 0
494	_ = Backoff(
495		func() (*Response, error) {
496			attempt++
497
498			r.URL = r.selectAddr(addrs, url, attempt)
499
500			resp, err = r.client.execute(r)
501			if err != nil {
502				r.client.Log.Printf("ERROR %v, Attempt %v", err, attempt)
503				if r.isContextCancelledIfAvailable() {
504					// stop Backoff from retrying request if request has been
505					// canceled by context
506					return resp, nil
507				}
508			}
509
510			return resp, err
511		},
512		Retries(r.client.RetryCount),
513		WaitTime(r.client.RetryWaitTime),
514		MaxWaitTime(r.client.RetryMaxWaitTime),
515		RetryConditions(r.client.RetryConditions),
516	)
517
518	return resp, err
519}
520
521//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
522// Request Unexported methods
523//___________________________________
524
525func (r *Request) fmtBodyString() (body string) {
526	body = "***** NO CONTENT *****"
527	if isPayloadSupported(r.Method, r.client.AllowGetMethodPayload) {
528		if _, ok := r.Body.(io.Reader); ok {
529			body = "***** BODY IS io.Reader *****"
530			return
531		}
532
533		// multipart or form-data
534		if r.isMultiPart || r.isFormData {
535			body = r.bodyBuf.String()
536			return
537		}
538
539		// request body data
540		if r.Body == nil {
541			return
542		}
543		var prtBodyBytes []byte
544		var err error
545
546		contentType := r.Header.Get(hdrContentTypeKey)
547		kind := kindOf(r.Body)
548		if canJSONMarshal(contentType, kind) {
549			prtBodyBytes, err = json.MarshalIndent(&r.Body, "", "   ")
550		} else if IsXMLType(contentType) && (kind == reflect.Struct) {
551			prtBodyBytes, err = xml.MarshalIndent(&r.Body, "", "   ")
552		} else if b, ok := r.Body.(string); ok {
553			if IsJSONType(contentType) {
554				bodyBytes := []byte(b)
555				out := acquireBuffer()
556				defer releaseBuffer(out)
557				if err = json.Indent(out, bodyBytes, "", "   "); err == nil {
558					prtBodyBytes = out.Bytes()
559				}
560			} else {
561				body = b
562				return
563			}
564		} else if b, ok := r.Body.([]byte); ok {
565			body = base64.StdEncoding.EncodeToString(b)
566		}
567
568		if prtBodyBytes != nil && err == nil {
569			body = string(prtBodyBytes)
570		}
571	}
572
573	return
574}
575
576func (r *Request) selectAddr(addrs []*net.SRV, path string, attempt int) string {
577	if addrs == nil {
578		return path
579	}
580
581	idx := attempt % len(addrs)
582	domain := strings.TrimRight(addrs[idx].Target, ".")
583	path = strings.TrimLeft(path, "/")
584
585	return fmt.Sprintf("%s://%s:%d/%s", r.client.scheme, domain, addrs[idx].Port, path)
586}
587