mirror of
https://gitlab.bertha.cloud/partitio/Nextcloud-Partitio/gonextcloud
synced 2024-11-16 11:16:24 +00:00
586 lines
15 KiB
Go
586 lines
15 KiB
Go
package grequests
|
||
|
||
import (
|
||
"bytes"
|
||
"crypto/tls"
|
||
"encoding/json"
|
||
"encoding/xml"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"mime"
|
||
"mime/multipart"
|
||
"net"
|
||
"net/http"
|
||
"net/http/cookiejar"
|
||
"net/textproto"
|
||
"net/url"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/google/go-querystring/query"
|
||
|
||
"context"
|
||
|
||
"golang.org/x/net/publicsuffix"
|
||
)
|
||
|
||
// RequestOptions is the location that of where the data
|
||
type RequestOptions struct {
|
||
|
||
// Data is a map of key values that will eventually convert into the
|
||
// query string of a GET request or the body of a POST request.
|
||
Data map[string]string
|
||
|
||
// Params is a map of query strings that may be used within a GET request
|
||
Params map[string]string
|
||
|
||
// QueryStruct is a struct that encapsulates a set of URL query params
|
||
// this paramter is mutually exclusive with `Params map[string]string` (they cannot be combined)
|
||
// for more information please see https://godoc.org/github.com/google/go-querystring/query
|
||
QueryStruct interface{}
|
||
|
||
// Files is where you can include files to upload. The use of this data
|
||
// structure is limited to POST requests
|
||
Files []FileUpload
|
||
|
||
// JSON can be used when you wish to send JSON within the request body
|
||
JSON interface{}
|
||
|
||
// XML can be used if you wish to send XML within the request body
|
||
XML interface{}
|
||
|
||
// Headers if you want to add custom HTTP headers to the request,
|
||
// this is your friend
|
||
Headers map[string]string
|
||
|
||
// InsecureSkipVerify is a flag that specifies if we should validate the
|
||
// server's TLS certificate. It should be noted that Go's TLS verify mechanism
|
||
// doesn't validate if a certificate has been revoked
|
||
InsecureSkipVerify bool
|
||
|
||
// DisableCompression will disable gzip compression on requests
|
||
DisableCompression bool
|
||
|
||
// UserAgent allows you to set an arbitrary custom user agent
|
||
UserAgent string
|
||
|
||
// Host allows you to set an arbitrary custom host
|
||
Host string
|
||
|
||
// Auth allows you to specify a user name and password that you wish to
|
||
// use when requesting the URL. It will use basic HTTP authentication
|
||
// formatting the username and password in base64 the format is:
|
||
// []string{username, password}
|
||
Auth []string
|
||
|
||
// IsAjax is a flag that can be set to make the request appear
|
||
// to be generated by browser Javascript
|
||
IsAjax bool
|
||
|
||
// Cookies is an array of `http.Cookie` that allows you to attach
|
||
// cookies to your request
|
||
Cookies []*http.Cookie
|
||
|
||
// UseCookieJar will create a custom HTTP client that will
|
||
// process and store HTTP cookies when they are sent down
|
||
UseCookieJar bool
|
||
|
||
// Proxies is a map in the following format
|
||
// *protocol* => proxy address e.g http => http://127.0.0.1:8080
|
||
Proxies map[string]*url.URL
|
||
|
||
// TLSHandshakeTimeout specifies the maximum amount of time waiting to
|
||
// wait for a TLS handshake. Zero means no timeout.
|
||
TLSHandshakeTimeout time.Duration
|
||
|
||
// DialTimeout is the maximum amount of time a dial will wait for
|
||
// a connect to complete.
|
||
DialTimeout time.Duration
|
||
|
||
// KeepAlive specifies the keep-alive period for an active
|
||
// network connection. If zero, keep-alive are not enabled.
|
||
DialKeepAlive time.Duration
|
||
|
||
// RequestTimeout is the maximum amount of time a whole request(include dial / request / redirect)
|
||
// will wait.
|
||
RequestTimeout time.Duration
|
||
|
||
// HTTPClient can be provided if you wish to supply a custom HTTP client
|
||
// this is useful if you want to use an OAUTH client with your request.
|
||
HTTPClient *http.Client
|
||
|
||
// SensitiveHTTPHeaders is a map of sensitive HTTP headers that a user
|
||
// doesn't want passed on a redirect.
|
||
SensitiveHTTPHeaders map[string]struct{}
|
||
|
||
// RedirectLimit is the acceptable amount of redirects that we should expect
|
||
// before returning an error be default this is set to 30. You can change this
|
||
// globally by modifying the `RedirectLimit` variable.
|
||
RedirectLimit int
|
||
|
||
// RequestBody allows you to put anything matching an `io.Reader` into the request
|
||
// this option will take precedence over any other request option specified
|
||
RequestBody io.Reader
|
||
|
||
// CookieJar allows you to specify a special cookiejar to use with your request.
|
||
// this option will take precedence over the `UseCookieJar` option above.
|
||
CookieJar http.CookieJar
|
||
|
||
// Context can be used to maintain state between requests https://golang.org/pkg/context/#Context
|
||
Context context.Context
|
||
}
|
||
|
||
func doRegularRequest(requestVerb, url string, ro *RequestOptions) (*Response, error) {
|
||
return buildResponse(buildRequest(requestVerb, url, ro, nil))
|
||
}
|
||
|
||
func doSessionRequest(requestVerb, url string, ro *RequestOptions, httpClient *http.Client) (*Response, error) {
|
||
return buildResponse(buildRequest(requestVerb, url, ro, httpClient))
|
||
}
|
||
|
||
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
|
||
|
||
func escapeQuotes(s string) string {
|
||
return quoteEscaper.Replace(s)
|
||
}
|
||
|
||
// buildRequest is where most of the magic happens for request processing
|
||
func buildRequest(httpMethod, url string, ro *RequestOptions, httpClient *http.Client) (*http.Response, error) {
|
||
if ro == nil {
|
||
ro = &RequestOptions{}
|
||
}
|
||
|
||
if ro.CookieJar != nil {
|
||
ro.UseCookieJar = true
|
||
}
|
||
|
||
// Create our own HTTP client
|
||
|
||
if httpClient == nil {
|
||
httpClient = BuildHTTPClient(*ro)
|
||
}
|
||
|
||
var err error // we don't want to shadow url so we won't use :=
|
||
switch {
|
||
case len(ro.Params) != 0:
|
||
if url, err = buildURLParams(url, ro.Params); err != nil {
|
||
return nil, err
|
||
}
|
||
case ro.QueryStruct != nil:
|
||
if url, err = buildURLStruct(url, ro.QueryStruct); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
// Build the request
|
||
req, err := buildHTTPRequest(httpMethod, url, ro)
|
||
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Do we need to add any HTTP headers or Basic Auth?
|
||
addHTTPHeaders(ro, req)
|
||
addCookies(ro, req)
|
||
|
||
addRedirectFunctionality(httpClient, ro)
|
||
|
||
if ro.Context != nil {
|
||
req = req.WithContext(ro.Context)
|
||
}
|
||
|
||
return httpClient.Do(req)
|
||
}
|
||
|
||
func buildHTTPRequest(httpMethod, userURL string, ro *RequestOptions) (*http.Request, error) {
|
||
if ro.RequestBody != nil {
|
||
return http.NewRequest(httpMethod, userURL, ro.RequestBody)
|
||
}
|
||
|
||
if ro.JSON != nil {
|
||
return createBasicJSONRequest(httpMethod, userURL, ro)
|
||
}
|
||
|
||
if ro.XML != nil {
|
||
return createBasicXMLRequest(httpMethod, userURL, ro)
|
||
}
|
||
|
||
if ro.Files != nil {
|
||
return createFileUploadRequest(httpMethod, userURL, ro)
|
||
}
|
||
|
||
if ro.Data != nil {
|
||
return createBasicRequest(httpMethod, userURL, ro)
|
||
}
|
||
|
||
return http.NewRequest(httpMethod, userURL, nil)
|
||
}
|
||
|
||
func createFileUploadRequest(httpMethod, userURL string, ro *RequestOptions) (*http.Request, error) {
|
||
if httpMethod == "POST" {
|
||
return createMultiPartPostRequest(httpMethod, userURL, ro)
|
||
}
|
||
|
||
// This may be a PUT or PATCH request so we will just put the raw
|
||
// io.ReadCloser in the request body
|
||
// and guess the MIME type from the file name
|
||
|
||
// At the moment, we will only support 1 file upload as a time
|
||
// when uploading using PUT or PATCH
|
||
|
||
req, err := http.NewRequest(httpMethod, userURL, ro.Files[0].FileContents)
|
||
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
req.Header.Set("Content-Type", mime.TypeByExtension(ro.Files[0].FileName))
|
||
|
||
return req, nil
|
||
|
||
}
|
||
|
||
func createBasicXMLRequest(httpMethod, userURL string, ro *RequestOptions) (*http.Request, error) {
|
||
var reader io.Reader
|
||
|
||
switch ro.XML.(type) {
|
||
case string:
|
||
reader = strings.NewReader(ro.XML.(string))
|
||
case []byte:
|
||
reader = bytes.NewReader(ro.XML.([]byte))
|
||
default:
|
||
byteSlice, err := xml.Marshal(ro.XML)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
reader = bytes.NewReader(byteSlice)
|
||
}
|
||
|
||
req, err := http.NewRequest(httpMethod, userURL, reader)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
req.Header.Set("Content-Type", "application/xml")
|
||
|
||
return req, nil
|
||
|
||
}
|
||
func createMultiPartPostRequest(httpMethod, userURL string, ro *RequestOptions) (*http.Request, error) {
|
||
requestBody := &bytes.Buffer{}
|
||
|
||
multipartWriter := multipart.NewWriter(requestBody)
|
||
|
||
for i, f := range ro.Files {
|
||
|
||
if f.FileContents == nil {
|
||
return nil, errors.New("grequests: Pointer FileContents cannot be nil")
|
||
}
|
||
|
||
fieldName := f.FieldName
|
||
|
||
if fieldName == "" {
|
||
if len(ro.Files) > 1 {
|
||
fieldName = strings.Join([]string{"file", strconv.Itoa(i + 1)}, "")
|
||
} else {
|
||
fieldName = "file"
|
||
}
|
||
}
|
||
|
||
var writer io.Writer
|
||
var err error
|
||
|
||
if f.FileMime != "" {
|
||
h := make(textproto.MIMEHeader)
|
||
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, escapeQuotes(fieldName), escapeQuotes(f.FileName)))
|
||
h.Set("Content-Type", f.FileMime)
|
||
writer, err = multipartWriter.CreatePart(h)
|
||
} else {
|
||
writer, err = multipartWriter.CreateFormFile(fieldName, f.FileName)
|
||
}
|
||
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if _, err = io.Copy(writer, f.FileContents); err != nil && err != io.EOF {
|
||
return nil, err
|
||
}
|
||
|
||
f.FileContents.Close()
|
||
|
||
}
|
||
|
||
// Populate the other parts of the form (if there are any)
|
||
for key, value := range ro.Data {
|
||
multipartWriter.WriteField(key, value)
|
||
}
|
||
|
||
if err := multipartWriter.Close(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
req, err := http.NewRequest(httpMethod, userURL, requestBody)
|
||
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
req.Header.Add("Content-Type", multipartWriter.FormDataContentType())
|
||
|
||
return req, err
|
||
}
|
||
|
||
func createBasicJSONRequest(httpMethod, userURL string, ro *RequestOptions) (*http.Request, error) {
|
||
|
||
var reader io.Reader
|
||
switch ro.JSON.(type) {
|
||
case string:
|
||
reader = strings.NewReader(ro.JSON.(string))
|
||
case []byte:
|
||
reader = bytes.NewReader(ro.JSON.([]byte))
|
||
default:
|
||
byteSlice, err := json.Marshal(ro.JSON)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
reader = bytes.NewReader(byteSlice)
|
||
}
|
||
|
||
req, err := http.NewRequest(httpMethod, userURL, reader)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
req.Header.Set("Content-Type", "application/json")
|
||
|
||
return req, nil
|
||
|
||
}
|
||
func createBasicRequest(httpMethod, userURL string, ro *RequestOptions) (*http.Request, error) {
|
||
|
||
req, err := http.NewRequest(httpMethod, userURL, strings.NewReader(encodePostValues(ro.Data)))
|
||
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// The content type must be set to a regular form
|
||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||
|
||
return req, nil
|
||
}
|
||
|
||
func encodePostValues(postValues map[string]string) string {
|
||
urlValues := &url.Values{}
|
||
|
||
for key, value := range postValues {
|
||
urlValues.Set(key, value)
|
||
}
|
||
|
||
return urlValues.Encode() // This will sort all of the string values
|
||
}
|
||
|
||
// proxySettings will default to the default proxy settings if none are provided
|
||
// if settings are provided – they will override the environment variables
|
||
func (ro RequestOptions) proxySettings(req *http.Request) (*url.URL, error) {
|
||
// No proxies – lets use the default
|
||
if len(ro.Proxies) == 0 {
|
||
return http.ProxyFromEnvironment(req)
|
||
}
|
||
|
||
// There was a proxy specified – do we support the protocol?
|
||
if _, ok := ro.Proxies[req.URL.Scheme]; ok {
|
||
return ro.Proxies[req.URL.Scheme], nil
|
||
}
|
||
|
||
// Proxies were specified but not for any protocol that we use
|
||
return http.ProxyFromEnvironment(req)
|
||
|
||
}
|
||
|
||
// dontUseDefaultClient will tell the "client creator" if a custom client is needed
|
||
// it checks the following items (and will create a custom client of these are)
|
||
// true
|
||
// 1. Do we want to accept invalid SSL certificates?
|
||
// 2. Do we want to disable compression?
|
||
// 3. Do we want a custom proxy?
|
||
// 4. Do we want to change the default timeout for TLS Handshake?
|
||
// 5. Do we want to change the default request timeout?
|
||
// 6. Do we want to change the default connection timeout?
|
||
// 7. Do you want to use the http.Client's cookieJar?
|
||
func (ro RequestOptions) dontUseDefaultClient() bool {
|
||
switch {
|
||
case ro.InsecureSkipVerify == true:
|
||
case ro.DisableCompression == true:
|
||
case len(ro.Proxies) != 0:
|
||
case ro.TLSHandshakeTimeout != 0:
|
||
case ro.DialTimeout != 0:
|
||
case ro.DialKeepAlive != 0:
|
||
case len(ro.Cookies) != 0:
|
||
case ro.UseCookieJar != false:
|
||
case ro.RequestTimeout != 0:
|
||
default:
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
// BuildHTTPClient is a function that will return a custom HTTP client based on the request options provided
|
||
// the check is in UseDefaultClient
|
||
func BuildHTTPClient(ro RequestOptions) *http.Client {
|
||
|
||
if ro.HTTPClient != nil {
|
||
return ro.HTTPClient
|
||
}
|
||
|
||
// Does the user want to change the defaults?
|
||
if !ro.dontUseDefaultClient() {
|
||
return http.DefaultClient
|
||
}
|
||
|
||
// Using the user config for tls timeout or default
|
||
if ro.TLSHandshakeTimeout == 0 {
|
||
ro.TLSHandshakeTimeout = tlsHandshakeTimeout
|
||
}
|
||
|
||
// Using the user config for dial timeout or default
|
||
if ro.DialTimeout == 0 {
|
||
ro.DialTimeout = dialTimeout
|
||
}
|
||
|
||
// Using the user config for dial keep alive or default
|
||
if ro.DialKeepAlive == 0 {
|
||
ro.DialKeepAlive = dialKeepAlive
|
||
}
|
||
|
||
if ro.RequestTimeout == 0 {
|
||
ro.RequestTimeout = requestTimeout
|
||
}
|
||
|
||
var cookieJar http.CookieJar
|
||
|
||
if ro.UseCookieJar {
|
||
if ro.CookieJar != nil {
|
||
cookieJar = ro.CookieJar
|
||
} else {
|
||
// The function does not return an error ever... so we are just ignoring it
|
||
cookieJar, _ = cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
|
||
}
|
||
}
|
||
|
||
return &http.Client{
|
||
Jar: cookieJar,
|
||
Transport: createHTTPTransport(ro),
|
||
Timeout: ro.RequestTimeout,
|
||
}
|
||
}
|
||
|
||
func createHTTPTransport(ro RequestOptions) *http.Transport {
|
||
ourHTTPTransport := &http.Transport{
|
||
// These are borrowed from the default transporter
|
||
Proxy: ro.proxySettings,
|
||
Dial: (&net.Dialer{
|
||
Timeout: ro.DialTimeout,
|
||
KeepAlive: ro.DialKeepAlive,
|
||
}).Dial,
|
||
TLSHandshakeTimeout: ro.TLSHandshakeTimeout,
|
||
|
||
// Here comes the user settings
|
||
TLSClientConfig: &tls.Config{InsecureSkipVerify: ro.InsecureSkipVerify},
|
||
DisableCompression: ro.DisableCompression,
|
||
}
|
||
EnsureTransporterFinalized(ourHTTPTransport)
|
||
return ourHTTPTransport
|
||
}
|
||
|
||
// buildURLParams returns a URL with all of the params
|
||
// Note: This function will override current URL params if they contradict what is provided in the map
|
||
// That is what the "magic" is on the last line
|
||
func buildURLParams(userURL string, params map[string]string) (string, error) {
|
||
parsedURL, err := url.Parse(userURL)
|
||
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
parsedQuery, err := url.ParseQuery(parsedURL.RawQuery)
|
||
|
||
if err != nil {
|
||
return "", nil
|
||
}
|
||
|
||
for key, value := range params {
|
||
parsedQuery.Set(key, value)
|
||
}
|
||
|
||
return addQueryParams(parsedURL, parsedQuery), nil
|
||
}
|
||
|
||
// addHTTPHeaders adds any additional HTTP headers that need to be added are added here including:
|
||
// 1. Custom User agent
|
||
// 2. Authorization Headers
|
||
// 3. Any other header requested
|
||
func addHTTPHeaders(ro *RequestOptions, req *http.Request) {
|
||
for key, value := range ro.Headers {
|
||
req.Header.Set(key, value)
|
||
}
|
||
|
||
if ro.UserAgent != "" {
|
||
req.Header.Set("User-Agent", ro.UserAgent)
|
||
} else {
|
||
req.Header.Set("User-Agent", localUserAgent)
|
||
}
|
||
|
||
if ro.Host != "" {
|
||
req.Host = ro.Host
|
||
}
|
||
|
||
if ro.Auth != nil {
|
||
req.SetBasicAuth(ro.Auth[0], ro.Auth[1])
|
||
}
|
||
|
||
if ro.IsAjax == true {
|
||
req.Header.Set("X-Requested-With", "XMLHttpRequest")
|
||
}
|
||
}
|
||
|
||
func addCookies(ro *RequestOptions, req *http.Request) {
|
||
for _, c := range ro.Cookies {
|
||
req.AddCookie(c)
|
||
}
|
||
}
|
||
|
||
func addQueryParams(parsedURL *url.URL, parsedQuery url.Values) string {
|
||
return strings.Join([]string{strings.Replace(parsedURL.String(), "?"+parsedURL.RawQuery, "", -1), parsedQuery.Encode()}, "?")
|
||
}
|
||
|
||
func buildURLStruct(userURL string, URLStruct interface{}) (string, error) {
|
||
parsedURL, err := url.Parse(userURL)
|
||
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
parsedQuery, err := url.ParseQuery(parsedURL.RawQuery)
|
||
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
queryStruct, err := query.Values(URLStruct)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
for key, value := range queryStruct {
|
||
for _, v := range value {
|
||
parsedQuery.Add(key, v)
|
||
}
|
||
}
|
||
|
||
return addQueryParams(parsedURL, parsedQuery), nil
|
||
}
|