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 }