1// Copyright (c) 2015-2021 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	"bytes"
9	"encoding/xml"
10	"fmt"
11	"io"
12	"log"
13	"mime/multipart"
14	"net/http"
15	"net/textproto"
16	"os"
17	"path/filepath"
18	"reflect"
19	"runtime"
20	"sort"
21	"strings"
22)
23
24//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
25// Logger interface
26//_______________________________________________________________________
27
28// Logger interface is to abstract the logging from Resty. Gives control to
29// the Resty users, choice of the logger.
30type Logger interface {
31	Errorf(format string, v ...interface{})
32	Warnf(format string, v ...interface{})
33	Debugf(format string, v ...interface{})
34}
35
36func createLogger() *logger {
37	l := &logger{l: log.New(os.Stderr, "", log.Ldate|log.Lmicroseconds)}
38	return l
39}
40
41var _ Logger = (*logger)(nil)
42
43type logger struct {
44	l *log.Logger
45}
46
47func (l *logger) Errorf(format string, v ...interface{}) {
48	l.output("ERROR RESTY "+format, v...)
49}
50
51func (l *logger) Warnf(format string, v ...interface{}) {
52	l.output("WARN RESTY "+format, v...)
53}
54
55func (l *logger) Debugf(format string, v ...interface{}) {
56	l.output("DEBUG RESTY "+format, v...)
57}
58
59func (l *logger) output(format string, v ...interface{}) {
60	if len(v) == 0 {
61		l.l.Print(format)
62		return
63	}
64	l.l.Printf(format, v...)
65}
66
67//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
68// Package Helper methods
69//_______________________________________________________________________
70
71// IsStringEmpty method tells whether given string is empty or not
72func IsStringEmpty(str string) bool {
73	return len(strings.TrimSpace(str)) == 0
74}
75
76// DetectContentType method is used to figure out `Request.Body` content type for request header
77func DetectContentType(body interface{}) string {
78	contentType := plainTextType
79	kind := kindOf(body)
80	switch kind {
81	case reflect.Struct, reflect.Map:
82		contentType = jsonContentType
83	case reflect.String:
84		contentType = plainTextType
85	default:
86		if b, ok := body.([]byte); ok {
87			contentType = http.DetectContentType(b)
88		} else if kind == reflect.Slice {
89			contentType = jsonContentType
90		}
91	}
92
93	return contentType
94}
95
96// IsJSONType method is to check JSON content type or not
97func IsJSONType(ct string) bool {
98	return jsonCheck.MatchString(ct)
99}
100
101// IsXMLType method is to check XML content type or not
102func IsXMLType(ct string) bool {
103	return xmlCheck.MatchString(ct)
104}
105
106// Unmarshalc content into object from JSON or XML
107func Unmarshalc(c *Client, ct string, b []byte, d interface{}) (err error) {
108	if IsJSONType(ct) {
109		err = c.JSONUnmarshal(b, d)
110	} else if IsXMLType(ct) {
111		err = xml.Unmarshal(b, d)
112	}
113
114	return
115}
116
117//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
118// RequestLog and ResponseLog type
119//_______________________________________________________________________
120
121// RequestLog struct is used to collected information from resty request
122// instance for debug logging. It sent to request log callback before resty
123// actually logs the information.
124type RequestLog struct {
125	Header http.Header
126	Body   string
127}
128
129// ResponseLog struct is used to collected information from resty response
130// instance for debug logging. It sent to response log callback before resty
131// actually logs the information.
132type ResponseLog struct {
133	Header http.Header
134	Body   string
135}
136
137//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
138// Package Unexported methods
139//_______________________________________________________________________
140
141// way to disable the HTML escape as opt-in
142func jsonMarshal(c *Client, r *Request, d interface{}) ([]byte, error) {
143	if !r.jsonEscapeHTML {
144		return noescapeJSONMarshal(d)
145	} else if !c.jsonEscapeHTML {
146		return noescapeJSONMarshal(d)
147	}
148	return c.JSONMarshal(d)
149}
150
151func firstNonEmpty(v ...string) string {
152	for _, s := range v {
153		if !IsStringEmpty(s) {
154			return s
155		}
156	}
157	return ""
158}
159
160var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
161
162func escapeQuotes(s string) string {
163	return quoteEscaper.Replace(s)
164}
165
166func createMultipartHeader(param, fileName, contentType string) textproto.MIMEHeader {
167	hdr := make(textproto.MIMEHeader)
168
169	var contentDispositionValue string
170	if IsStringEmpty(fileName) {
171		contentDispositionValue = fmt.Sprintf(`form-data; name="%s"`, param)
172	} else {
173		contentDispositionValue = fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
174			param, escapeQuotes(fileName))
175	}
176	hdr.Set("Content-Disposition", contentDispositionValue)
177
178	if !IsStringEmpty(contentType) {
179		hdr.Set(hdrContentTypeKey, contentType)
180	}
181	return hdr
182}
183
184func addMultipartFormField(w *multipart.Writer, mf *MultipartField) error {
185	partWriter, err := w.CreatePart(createMultipartHeader(mf.Param, mf.FileName, mf.ContentType))
186	if err != nil {
187		return err
188	}
189
190	_, err = io.Copy(partWriter, mf.Reader)
191	return err
192}
193
194func writeMultipartFormFile(w *multipart.Writer, fieldName, fileName string, r io.Reader) error {
195	// Auto detect actual multipart content type
196	cbuf := make([]byte, 512)
197	size, err := r.Read(cbuf)
198	if err != nil {
199		return err
200	}
201
202	partWriter, err := w.CreatePart(createMultipartHeader(fieldName, fileName, http.DetectContentType(cbuf)))
203	if err != nil {
204		return err
205	}
206
207	if _, err = partWriter.Write(cbuf[:size]); err != nil {
208		return err
209	}
210
211	_, err = io.Copy(partWriter, r)
212	return err
213}
214
215func addFile(w *multipart.Writer, fieldName, path string) error {
216	file, err := os.Open(path)
217	if err != nil {
218		return err
219	}
220	defer closeq(file)
221	return writeMultipartFormFile(w, fieldName, filepath.Base(path), file)
222}
223
224func addFileReader(w *multipart.Writer, f *File) error {
225	return writeMultipartFormFile(w, f.ParamName, f.Name, f.Reader)
226}
227
228func getPointer(v interface{}) interface{} {
229	vv := valueOf(v)
230	if vv.Kind() == reflect.Ptr {
231		return v
232	}
233	return reflect.New(vv.Type()).Interface()
234}
235
236func isPayloadSupported(m string, allowMethodGet bool) bool {
237	return !(m == MethodHead || m == MethodOptions || (m == MethodGet && !allowMethodGet))
238}
239
240func typeOf(i interface{}) reflect.Type {
241	return indirect(valueOf(i)).Type()
242}
243
244func valueOf(i interface{}) reflect.Value {
245	return reflect.ValueOf(i)
246}
247
248func indirect(v reflect.Value) reflect.Value {
249	return reflect.Indirect(v)
250}
251
252func kindOf(v interface{}) reflect.Kind {
253	return typeOf(v).Kind()
254}
255
256func createDirectory(dir string) (err error) {
257	if _, err = os.Stat(dir); err != nil {
258		if os.IsNotExist(err) {
259			if err = os.MkdirAll(dir, 0755); err != nil {
260				return
261			}
262		}
263	}
264	return
265}
266
267func canJSONMarshal(contentType string, kind reflect.Kind) bool {
268	return IsJSONType(contentType) && (kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice)
269}
270
271func functionName(i interface{}) string {
272	return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
273}
274
275func acquireBuffer() *bytes.Buffer {
276	return bufPool.Get().(*bytes.Buffer)
277}
278
279func releaseBuffer(buf *bytes.Buffer) {
280	if buf != nil {
281		buf.Reset()
282		bufPool.Put(buf)
283	}
284}
285
286func closeq(v interface{}) {
287	if c, ok := v.(io.Closer); ok {
288		silently(c.Close())
289	}
290}
291
292func silently(_ ...interface{}) {}
293
294func composeHeaders(c *Client, r *Request, hdrs http.Header) string {
295	str := make([]string, 0, len(hdrs))
296	for _, k := range sortHeaderKeys(hdrs) {
297		var v string
298		if k == "Cookie" {
299			cv := strings.TrimSpace(strings.Join(hdrs[k], ", "))
300			if c.GetClient().Jar != nil {
301				for _, c := range c.GetClient().Jar.Cookies(r.RawRequest.URL) {
302					if cv != "" {
303						cv = cv + "; " + c.String()
304					} else {
305						cv = c.String()
306					}
307				}
308			}
309			v = strings.TrimSpace(fmt.Sprintf("%25s: %s", k, cv))
310		} else {
311			v = strings.TrimSpace(fmt.Sprintf("%25s: %s", k, strings.Join(hdrs[k], ", ")))
312		}
313		if v != "" {
314			str = append(str, "\t"+v)
315		}
316	}
317	return strings.Join(str, "\n")
318}
319
320func sortHeaderKeys(hdrs http.Header) []string {
321	keys := make([]string, 0, len(hdrs))
322	for key := range hdrs {
323		keys = append(keys, key)
324	}
325	sort.Strings(keys)
326	return keys
327}
328
329func copyHeaders(hdrs http.Header) http.Header {
330	nh := http.Header{}
331	for k, v := range hdrs {
332		nh[k] = v
333	}
334	return nh
335}
336
337type noRetryErr struct {
338	err error
339}
340
341func (e *noRetryErr) Error() string {
342	return e.err.Error()
343}
344
345func wrapNoRetryErr(err error) error {
346	if err != nil {
347		err = &noRetryErr{err: err}
348	}
349	return err
350}
351
352func unwrapNoRetryErr(err error) error {
353	if e, ok := err.(*noRetryErr); ok {
354		err = e.err
355	}
356	return err
357}
358