github.com/go-playground/pkg/v5@v5.29.1/net/http/helpers.go (about)

     1  package httpext
     2  
     3  import (
     4  	"compress/gzip"
     5  	"encoding/json"
     6  	"encoding/xml"
     7  	"errors"
     8  	"io"
     9  	"mime"
    10  	"net"
    11  	"net/http"
    12  	"net/url"
    13  	"path/filepath"
    14  	"strings"
    15  
    16  	bytesext "github.com/go-playground/pkg/v5/bytes"
    17  	ioext "github.com/go-playground/pkg/v5/io"
    18  )
    19  
    20  // QueryParamsOption represents the options for including query parameters during Decode helper functions
    21  type QueryParamsOption uint8
    22  
    23  // QueryParamsOption's
    24  const (
    25  	QueryParams QueryParamsOption = iota
    26  	NoQueryParams
    27  )
    28  
    29  var (
    30  	xmlHeaderBytes = []byte(xml.Header)
    31  )
    32  
    33  func detectContentType(filename string) string {
    34  	ext := strings.ToLower(filepath.Ext(filename))
    35  	if t := mime.TypeByExtension(ext); t != "" {
    36  		return t
    37  	}
    38  	switch ext {
    39  	case ".md":
    40  		return TextMarkdown
    41  	default:
    42  		return ApplicationOctetStream
    43  	}
    44  }
    45  
    46  // AcceptedLanguages returns an array of accepted languages denoted by
    47  // the Accept-Language header sent by the browser
    48  func AcceptedLanguages(r *http.Request) (languages []string) {
    49  	accepted := r.Header.Get(AcceptedLanguage)
    50  	if accepted == "" {
    51  		return
    52  	}
    53  	options := strings.Split(accepted, ",")
    54  	l := len(options)
    55  	languages = make([]string, l)
    56  
    57  	for i := 0; i < l; i++ {
    58  		locale := strings.SplitN(options[i], ";", 2)
    59  		languages[i] = strings.Trim(locale[0], " ")
    60  	}
    61  	return
    62  }
    63  
    64  // Attachment is a helper method for returning an attachment file
    65  // to be downloaded, if you with to open inline see function Inline
    66  func Attachment(w http.ResponseWriter, r io.Reader, filename string) (err error) {
    67  	w.Header().Set(ContentDisposition, "attachment;filename="+filename)
    68  	w.Header().Set(ContentType, detectContentType(filename))
    69  	w.WriteHeader(http.StatusOK)
    70  	_, err = io.Copy(w, r)
    71  	return
    72  }
    73  
    74  // Inline is a helper method for returning a file inline to
    75  // be rendered/opened by the browser
    76  func Inline(w http.ResponseWriter, r io.Reader, filename string) (err error) {
    77  	w.Header().Set(ContentDisposition, "inline;filename="+filename)
    78  	w.Header().Set(ContentType, detectContentType(filename))
    79  	w.WriteHeader(http.StatusOK)
    80  	_, err = io.Copy(w, r)
    81  	return
    82  }
    83  
    84  // ClientIP implements the best effort algorithm to return the real client IP, it parses
    85  // X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.
    86  func ClientIP(r *http.Request) (clientIP string) {
    87  	values := r.Header[XRealIP]
    88  	if len(values) > 0 {
    89  		clientIP = strings.TrimSpace(values[0])
    90  		if clientIP != "" {
    91  			return
    92  		}
    93  	}
    94  	if values = r.Header[XForwardedFor]; len(values) > 0 {
    95  		clientIP = values[0]
    96  		if index := strings.IndexByte(clientIP, ','); index >= 0 {
    97  			clientIP = clientIP[0:index]
    98  		}
    99  		clientIP = strings.TrimSpace(clientIP)
   100  		if clientIP != "" {
   101  			return
   102  		}
   103  	}
   104  	clientIP, _, _ = net.SplitHostPort(strings.TrimSpace(r.RemoteAddr))
   105  	return
   106  }
   107  
   108  // JSONStream uses json.Encoder to stream the JSON response body.
   109  //
   110  // This differs from the JSON helper which unmarshalls into memory first allowing the capture of JSON encoding errors.
   111  func JSONStream(w http.ResponseWriter, status int, i interface{}) error {
   112  	w.Header().Set(ContentType, ApplicationJSON)
   113  	w.WriteHeader(status)
   114  	return json.NewEncoder(w).Encode(i)
   115  }
   116  
   117  // JSON marshals provided interface + returns JSON + status code
   118  func JSON(w http.ResponseWriter, status int, i interface{}) error {
   119  	b, err := json.Marshal(i)
   120  	if err != nil {
   121  		return err
   122  	}
   123  	w.Header().Set(ContentType, ApplicationJSON)
   124  	w.WriteHeader(status)
   125  	_, err = w.Write(b)
   126  	return err
   127  }
   128  
   129  // JSONBytes returns provided JSON response with status code
   130  func JSONBytes(w http.ResponseWriter, status int, b []byte) (err error) {
   131  	w.Header().Set(ContentType, ApplicationJSON)
   132  	w.WriteHeader(status)
   133  	_, err = w.Write(b)
   134  	return err
   135  }
   136  
   137  // JSONP sends a JSONP response with status code and uses `callback` to construct
   138  // the JSONP payload.
   139  func JSONP(w http.ResponseWriter, status int, i interface{}, callback string) error {
   140  	b, err := json.Marshal(i)
   141  	if err != nil {
   142  		return err
   143  	}
   144  	w.Header().Set(ContentType, ApplicationJSON)
   145  	w.WriteHeader(status)
   146  	if _, err = w.Write([]byte(callback + "(")); err == nil {
   147  		if _, err = w.Write(b); err == nil {
   148  			_, err = w.Write([]byte(");"))
   149  		}
   150  	}
   151  	return err
   152  }
   153  
   154  // XML marshals provided interface + returns XML + status code
   155  func XML(w http.ResponseWriter, status int, i interface{}) error {
   156  	b, err := xml.Marshal(i)
   157  	if err != nil {
   158  		return err
   159  	}
   160  	w.Header().Set(ContentType, ApplicationXML)
   161  	w.WriteHeader(status)
   162  	if _, err = w.Write(xmlHeaderBytes); err == nil {
   163  		_, err = w.Write(b)
   164  	}
   165  	return err
   166  }
   167  
   168  // XMLBytes returns provided XML response with status code
   169  func XMLBytes(w http.ResponseWriter, status int, b []byte) (err error) {
   170  	w.Header().Set(ContentType, ApplicationXML)
   171  	w.WriteHeader(status)
   172  	if _, err = w.Write(xmlHeaderBytes); err == nil {
   173  		_, err = w.Write(b)
   174  	}
   175  	return
   176  }
   177  
   178  // DecodeForm parses the requests form data into the provided struct.
   179  //
   180  // The Content-Type and http method are not checked.
   181  //
   182  // NOTE: when QueryParamsOption=QueryParams the query params will be parsed and included eg. route /user?test=true 'test'
   183  // is added to parsed Form.
   184  func DecodeForm(r *http.Request, qp QueryParamsOption, v interface{}) (err error) {
   185  	if err = r.ParseForm(); err == nil {
   186  		switch qp {
   187  		case QueryParams:
   188  			err = DefaultFormDecoder.Decode(v, r.Form)
   189  		case NoQueryParams:
   190  			err = DefaultFormDecoder.Decode(v, r.PostForm)
   191  		}
   192  	}
   193  	return
   194  }
   195  
   196  // DecodeMultipartForm parses the requests form data into the provided struct.
   197  //
   198  // The Content-Type and http method are not checked.
   199  //
   200  // NOTE: when includeQueryParams=true query params will be parsed and included eg. route /user?test=true 'test'
   201  // is added to parsed MultipartForm.
   202  func DecodeMultipartForm(r *http.Request, qp QueryParamsOption, maxMemory int64, v interface{}) (err error) {
   203  	if err = r.ParseMultipartForm(maxMemory); err == nil {
   204  		switch qp {
   205  		case QueryParams:
   206  			err = DefaultFormDecoder.Decode(v, r.Form)
   207  		case NoQueryParams:
   208  			err = DefaultFormDecoder.Decode(v, r.MultipartForm.Value)
   209  		}
   210  	}
   211  	return
   212  }
   213  
   214  // DecodeJSON decodes the request body into the provided struct and limits the request size via
   215  // an ioext.LimitReader using the maxBytes param.
   216  //
   217  // The Content-Type e.g. "application/json" and http method are not checked.
   218  //
   219  // NOTE: when includeQueryParams=true query params will be parsed and included eg. route /user?test=true 'test'
   220  // is added to parsed JSON and replaces any values that may have been present
   221  func DecodeJSON(r *http.Request, qp QueryParamsOption, maxMemory int64, v interface{}) (err error) {
   222  	var values url.Values
   223  	if qp == QueryParams {
   224  		values = r.URL.Query()
   225  	}
   226  	return decodeJSON(r.Header, r.Body, qp, values, maxMemory, v)
   227  }
   228  
   229  func decodeJSON(headers http.Header, body io.Reader, qp QueryParamsOption, values url.Values, maxMemory int64, v interface{}) (err error) {
   230  	if encoding := headers.Get(ContentEncoding); encoding == Gzip {
   231  		var gzr *gzip.Reader
   232  		gzr, err = gzip.NewReader(body)
   233  		if err != nil {
   234  			return
   235  		}
   236  		defer func() {
   237  			_ = gzr.Close()
   238  		}()
   239  		body = gzr
   240  	}
   241  	err = json.NewDecoder(ioext.LimitReader(body, maxMemory)).Decode(v)
   242  	if qp == QueryParams && err == nil {
   243  		err = decodeQueryParams(values, v)
   244  	}
   245  	return
   246  }
   247  
   248  // DecodeXML decodes the request body into the provided struct and limits the request size via
   249  // an ioext.LimitReader using the maxBytes param.
   250  //
   251  // The Content-Type e.g. "application/xml" and http method are not checked.
   252  //
   253  // NOTE: when includeQueryParams=true query params will be parsed and included eg. route /user?test=true 'test'
   254  // is added to parsed XML and replaces any values that may have been present
   255  func DecodeXML(r *http.Request, qp QueryParamsOption, maxMemory int64, v interface{}) (err error) {
   256  	var values url.Values
   257  	if qp == QueryParams {
   258  		values = r.URL.Query()
   259  	}
   260  	return decodeXML(r.Header, r.Body, qp, values, maxMemory, v)
   261  }
   262  
   263  func decodeXML(headers http.Header, body io.Reader, qp QueryParamsOption, values url.Values, maxMemory int64, v interface{}) (err error) {
   264  	if encoding := headers.Get(ContentEncoding); encoding == Gzip {
   265  		var gzr *gzip.Reader
   266  		gzr, err = gzip.NewReader(body)
   267  		if err != nil {
   268  			return
   269  		}
   270  		defer func() {
   271  			_ = gzr.Close()
   272  		}()
   273  		body = gzr
   274  	}
   275  	err = xml.NewDecoder(ioext.LimitReader(body, maxMemory)).Decode(v)
   276  	if qp == QueryParams && err == nil {
   277  		err = decodeQueryParams(values, v)
   278  	}
   279  	return
   280  }
   281  
   282  // DecodeQueryParams takes the URL Query params flag.
   283  func DecodeQueryParams(r *http.Request, v interface{}) (err error) {
   284  	return decodeQueryParams(r.URL.Query(), v)
   285  }
   286  
   287  func decodeQueryParams(values url.Values, v interface{}) (err error) {
   288  	err = DefaultFormDecoder.Decode(v, values)
   289  	return
   290  }
   291  
   292  const (
   293  	nakedApplicationJSON string = "application/json"
   294  	nakedApplicationXML  string = "application/xml"
   295  )
   296  
   297  // Decode takes the request and attempts to discover its content type via
   298  // the http headers and then decode the request body into the provided struct.
   299  // Example if header was "application/json" would decode using
   300  // json.NewDecoder(ioext.LimitReader(r.Body, maxBytes)).Decode(v).
   301  //
   302  // This default to parsing query params if includeQueryParams=true and no other content type matches.
   303  //
   304  // NOTE: when includeQueryParams=true query params will be parsed and included eg. route /user?test=true 'test'
   305  // is added to parsed XML and replaces any values that may have been present
   306  func Decode(r *http.Request, qp QueryParamsOption, maxMemory int64, v interface{}) (err error) {
   307  	typ := r.Header.Get(ContentType)
   308  	if idx := strings.Index(typ, ";"); idx != -1 {
   309  		typ = typ[:idx]
   310  	}
   311  	switch typ {
   312  	case nakedApplicationJSON:
   313  		err = DecodeJSON(r, qp, maxMemory, v)
   314  	case nakedApplicationXML:
   315  		err = DecodeXML(r, qp, maxMemory, v)
   316  	case ApplicationForm:
   317  		err = DecodeForm(r, qp, v)
   318  	case MultipartForm:
   319  		err = DecodeMultipartForm(r, qp, maxMemory, v)
   320  	default:
   321  		if qp == QueryParams {
   322  			err = DecodeQueryParams(r, v)
   323  		}
   324  	}
   325  	return
   326  }
   327  
   328  // DecodeResponseAny takes the response and attempts to discover its content type via
   329  // the http headers and then decode the request body into the provided type.
   330  //
   331  // Example if header was "application/json" would decode using
   332  // json.NewDecoder(ioext.LimitReader(r.Body, maxBytes)).Decode(v).
   333  func DecodeResponseAny(r *http.Response, maxMemory bytesext.Bytes, v interface{}) (err error) {
   334  	typ := r.Header.Get(ContentType)
   335  	if idx := strings.Index(typ, ";"); idx != -1 {
   336  		typ = typ[:idx]
   337  	}
   338  	switch typ {
   339  	case nakedApplicationJSON:
   340  		err = decodeJSON(r.Header, r.Body, NoQueryParams, nil, maxMemory, v)
   341  	case nakedApplicationXML:
   342  		err = decodeXML(r.Header, r.Body, NoQueryParams, nil, maxMemory, v)
   343  	default:
   344  		err = errors.New("unsupported content type")
   345  	}
   346  	return
   347  }