|
|
package starnet
|
|
|
|
|
|
import (
|
|
|
"bytes"
|
|
|
"context"
|
|
|
"crypto/tls"
|
|
|
"encoding/json"
|
|
|
"fmt"
|
|
|
"io"
|
|
|
"mime/multipart"
|
|
|
"net"
|
|
|
"net/http"
|
|
|
"net/url"
|
|
|
"os"
|
|
|
"strconv"
|
|
|
"strings"
|
|
|
"time"
|
|
|
)
|
|
|
|
|
|
const (
|
|
|
HEADER_FORM_URLENCODE = `application/x-www-form-urlencoded`
|
|
|
HEADER_FORM_DATA = `multipart/form-data`
|
|
|
HEADER_JSON = `application/json`
|
|
|
HEADER_PLAIN = `text/plain`
|
|
|
)
|
|
|
|
|
|
var (
|
|
|
DefaultDialTimeout = 5 * time.Second
|
|
|
DefaultTimeout = 10 * time.Second
|
|
|
DefaultFetchRespBody = false
|
|
|
)
|
|
|
|
|
|
func UrlEncodeRaw(str string) string {
|
|
|
strs := strings.Replace(url.QueryEscape(str), "+", "%20", -1)
|
|
|
return strs
|
|
|
}
|
|
|
|
|
|
func UrlEncode(str string) string {
|
|
|
return url.QueryEscape(str)
|
|
|
}
|
|
|
|
|
|
func UrlDecode(str string) (string, error) {
|
|
|
return url.QueryUnescape(str)
|
|
|
}
|
|
|
|
|
|
func BuildQuery(queryData map[string]string) string {
|
|
|
query := url.Values{}
|
|
|
for k, v := range queryData {
|
|
|
query.Add(k, v)
|
|
|
}
|
|
|
return query.Encode()
|
|
|
}
|
|
|
|
|
|
// BuildPostForm takes a map of string keys and values, converts it into a URL-encoded query string,
|
|
|
// and then converts that string into a byte slice. This function is useful for preparing data for HTTP POST requests,
|
|
|
// where the server expects the request body to be URL-encoded form data.
|
|
|
//
|
|
|
// Parameters:
|
|
|
// queryMap: A map where the key-value pairs represent the form data to be sent in the HTTP POST request.
|
|
|
//
|
|
|
// Returns:
|
|
|
// A byte slice representing the URL-encoded form data.
|
|
|
func BuildPostForm(queryMap map[string]string) []byte {
|
|
|
return []byte(BuildQuery(queryMap))
|
|
|
}
|
|
|
|
|
|
func Get(uri string, opts ...RequestOpt) (*Response, error) {
|
|
|
return NewSimpleRequest(uri, "GET", opts...).Do()
|
|
|
}
|
|
|
|
|
|
func Post(uri string, opts ...RequestOpt) (*Response, error) {
|
|
|
return NewSimpleRequest(uri, "POST", opts...).Do()
|
|
|
}
|
|
|
|
|
|
func Options(uri string, opts ...RequestOpt) (*Response, error) {
|
|
|
return NewSimpleRequest(uri, "OPTIONS", opts...).Do()
|
|
|
}
|
|
|
|
|
|
func Put(uri string, opts ...RequestOpt) (*Response, error) {
|
|
|
return NewSimpleRequest(uri, "PUT", opts...).Do()
|
|
|
}
|
|
|
|
|
|
func Delete(uri string, opts ...RequestOpt) (*Response, error) {
|
|
|
return NewSimpleRequest(uri, "DELETE", opts...).Do()
|
|
|
}
|
|
|
|
|
|
func Head(uri string, opts ...RequestOpt) (*Response, error) {
|
|
|
return NewSimpleRequest(uri, "HEAD", opts...).Do()
|
|
|
}
|
|
|
|
|
|
func Patch(uri string, opts ...RequestOpt) (*Response, error) {
|
|
|
return NewSimpleRequest(uri, "PATCH", opts...).Do()
|
|
|
}
|
|
|
|
|
|
func Trace(uri string, opts ...RequestOpt) (*Response, error) {
|
|
|
return NewSimpleRequest(uri, "TRACE", opts...).Do()
|
|
|
}
|
|
|
|
|
|
func Connect(uri string, opts ...RequestOpt) (*Response, error) {
|
|
|
return NewSimpleRequest(uri, "CONNECT", opts...).Do()
|
|
|
}
|
|
|
|
|
|
type Request struct {
|
|
|
ctx context.Context
|
|
|
uri string
|
|
|
method string
|
|
|
errInfo error
|
|
|
RequestOpts
|
|
|
}
|
|
|
|
|
|
func (r *Request) Method() string {
|
|
|
return r.method
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetMethod(method string) error {
|
|
|
method = strings.ToUpper(method)
|
|
|
if !validMethod(method) {
|
|
|
return fmt.Errorf("invalid method: %s", method)
|
|
|
}
|
|
|
r.method = method
|
|
|
r.rawRequest.Method = method
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetMethodNoError(method string) *Request {
|
|
|
r.SetMethod(method)
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) Uri() string {
|
|
|
return r.uri
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetUri(uri string) error {
|
|
|
u, err := url.Parse(uri)
|
|
|
if err != nil {
|
|
|
return fmt.Errorf("parse uri error: %s", err)
|
|
|
}
|
|
|
r.uri = uri
|
|
|
u.Host = removeEmptyPort(u.Host)
|
|
|
r.rawRequest.Host = u.Host
|
|
|
r.rawRequest.URL = u
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetUriNoError(uri string) *Request {
|
|
|
r.SetUri(uri)
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) RawRequest() *http.Request {
|
|
|
return r.rawRequest
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetRawRequest(rawRequest *http.Request) *Request {
|
|
|
r.rawRequest = rawRequest
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) RawClient() *http.Client {
|
|
|
return r.rawClient
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetRawClient(rawClient *http.Client) *Request {
|
|
|
r.rawClient = rawClient
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) Do() (*Response, error) {
|
|
|
return Curl(r)
|
|
|
}
|
|
|
|
|
|
func (r *Request) Get() (*Response, error) {
|
|
|
err := r.SetMethod("GET")
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
return Curl(r)
|
|
|
}
|
|
|
|
|
|
func (r *Request) Post(data []byte) (*Response, error) {
|
|
|
err := r.SetMethod("POST")
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
r.bodyDataBytes = data
|
|
|
r.bodyDataReader = nil
|
|
|
return Curl(r)
|
|
|
}
|
|
|
|
|
|
type RequestOpts struct {
|
|
|
rawRequest *http.Request
|
|
|
rawClient *http.Client
|
|
|
|
|
|
alreadyApply bool
|
|
|
bodyDataBytes []byte
|
|
|
bodyDataReader io.Reader
|
|
|
bodyFormData map[string][]string
|
|
|
bodyFileData []RequestFile
|
|
|
//以上优先度为 bodyDataReader> bodyDataBytes > bodyFormData > bodyFileData
|
|
|
FileUploadRecallFn func(filename string, upPos int64, total int64)
|
|
|
proxy string
|
|
|
timeout time.Duration
|
|
|
dialTimeout time.Duration
|
|
|
headers http.Header
|
|
|
cookies []*http.Cookie
|
|
|
transport *http.Transport
|
|
|
queries map[string][]string
|
|
|
disableRedirect bool
|
|
|
//doRawRequest=true 不对request修改,直接发送
|
|
|
doRawRequest bool
|
|
|
//doRawClient=true 不对http client修改,直接发送
|
|
|
doRawClient bool
|
|
|
//doRawTransPort=true 不对http transport修改,直接发送
|
|
|
doRawTransport bool
|
|
|
skipTLSVerify bool
|
|
|
tlsConfig *tls.Config
|
|
|
autoFetchRespBody bool
|
|
|
customIP []string
|
|
|
alreadySetLookUpIPfn bool
|
|
|
lookUpIPfn func(ctx context.Context, host string) ([]net.IPAddr, error)
|
|
|
customDNS []string
|
|
|
basicAuth [2]string
|
|
|
autoCalcContentLength bool
|
|
|
}
|
|
|
|
|
|
func (r *Request) AutoCalcContentLength() bool {
|
|
|
return r.autoCalcContentLength
|
|
|
}
|
|
|
|
|
|
// SetAutoCalcContentLength sets whether to automatically calculate the Content-Length header based on the request body.
|
|
|
// WARN: If set to true, the Content-Length header will be set to the length of the request body, which may cause issues with chunked transfer encoding.
|
|
|
// also the memory usage will be higher
|
|
|
// Note that this function will not work if doRawRequest is true
|
|
|
func (r *Request) SetAutoCalcContentLength(autoCalcContentLength bool) *Request {
|
|
|
r.autoCalcContentLength = autoCalcContentLength
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
// BasicAuth returns the username and password provided in the request's Authorization header.
|
|
|
func (r *RequestOpts) BasicAuth() (string, string) {
|
|
|
return r.basicAuth[0], r.basicAuth[1]
|
|
|
}
|
|
|
|
|
|
// SetBasicAuth sets the request's Authorization header to use HTTP Basic Authentication with the provided username and password.
|
|
|
// Note: If doRawRequest is true, this function will nolonger work
|
|
|
func (r *RequestOpts) SetBasicAuth(username, password string) *RequestOpts {
|
|
|
r.basicAuth = [2]string{username, password}
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) DoRawTransport() bool {
|
|
|
return r.doRawTransport
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetDoRawTransport(doRawTransport bool) *Request {
|
|
|
r.doRawTransport = doRawTransport
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) CustomDNS() []string {
|
|
|
return r.customDNS
|
|
|
}
|
|
|
|
|
|
// Note: if LookUpIPfn is set, this function will not be used
|
|
|
func (r *Request) SetCustomDNS(customDNS []string) error {
|
|
|
for _, v := range customDNS {
|
|
|
if net.ParseIP(v) == nil {
|
|
|
return fmt.Errorf("invalid custom dns: %s", v)
|
|
|
}
|
|
|
}
|
|
|
r.customDNS = customDNS
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
// Note: if LookUpIPfn is set, this function will not be used
|
|
|
func (r *Request) SetCustomDNSNoError(customDNS []string) *Request {
|
|
|
r.SetCustomDNS(customDNS)
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
// Note: if LookUpIPfn is set, this function will not be used
|
|
|
func (r *Request) AddCustomDNS(customDNS []string) error {
|
|
|
for _, v := range customDNS {
|
|
|
if net.ParseIP(v) == nil {
|
|
|
return fmt.Errorf("invalid custom dns: %s", v)
|
|
|
}
|
|
|
}
|
|
|
r.customDNS = customDNS
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
// Note: if LookUpIPfn is set, this function will not be used
|
|
|
func (r *Request) AddCustomDNSNoError(customDNS []string) *Request {
|
|
|
r.AddCustomDNS(customDNS)
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) LookUpIPfn() func(ctx context.Context, host string) ([]net.IPAddr, error) {
|
|
|
return r.lookUpIPfn
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetLookUpIPfn(lookUpIPfn func(ctx context.Context, host string) ([]net.IPAddr, error)) *Request {
|
|
|
if lookUpIPfn == nil {
|
|
|
r.alreadySetLookUpIPfn = false
|
|
|
r.lookUpIPfn = net.DefaultResolver.LookupIPAddr
|
|
|
return r
|
|
|
}
|
|
|
r.lookUpIPfn = lookUpIPfn
|
|
|
r.alreadySetLookUpIPfn = true
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) CustomHostIP() []string {
|
|
|
return r.customIP
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetCustomHostIP(customIP []string) *Request {
|
|
|
r.customIP = customIP
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) AddCustomHostIP(customIP string) *Request {
|
|
|
r.customIP = append(r.customIP, customIP)
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) BodyDataBytes() []byte {
|
|
|
return r.bodyDataBytes
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetBodyDataBytes(bodyDataBytes []byte) *Request {
|
|
|
r.bodyDataBytes = bodyDataBytes
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) BodyDataReader() io.Reader {
|
|
|
return r.bodyDataReader
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetBodyDataReader(bodyDataReader io.Reader) *Request {
|
|
|
r.bodyDataReader = bodyDataReader
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) BodyFormData() map[string][]string {
|
|
|
return r.bodyFormData
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetBodyFormData(bodyFormData map[string][]string) *Request {
|
|
|
r.bodyFormData = bodyFormData
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetFormData(bodyFormData map[string][]string) *Request {
|
|
|
return r.SetBodyFormData(bodyFormData)
|
|
|
}
|
|
|
|
|
|
func (r *Request) AddFormMapData(bodyFormData map[string]string) *Request {
|
|
|
for k, v := range bodyFormData {
|
|
|
r.bodyFormData[k] = append(r.bodyFormData[k], v)
|
|
|
}
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) AddFormData(k, v string) *Request {
|
|
|
r.bodyFormData[k] = append(r.bodyFormData[k], v)
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) BodyFileData() []RequestFile {
|
|
|
return r.bodyFileData
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetBodyFileData(bodyFileData []RequestFile) *Request {
|
|
|
r.bodyFileData = bodyFileData
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) Proxy() string {
|
|
|
return r.proxy
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetProxy(proxy string) *Request {
|
|
|
r.proxy = proxy
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) Timeout() time.Duration {
|
|
|
return r.timeout
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetTimeout(timeout time.Duration) *Request {
|
|
|
r.timeout = timeout
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) DialTimeout() time.Duration {
|
|
|
return r.dialTimeout
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetDialTimeout(dialTimeout time.Duration) *Request {
|
|
|
r.dialTimeout = dialTimeout
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) Headers() http.Header {
|
|
|
return r.headers
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetHeaders(headers http.Header) *Request {
|
|
|
r.headers = headers
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) AddHeader(key, val string) *Request {
|
|
|
r.headers.Add(key, val)
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetHeader(key, val string) *Request {
|
|
|
r.headers.Set(key, val)
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetContentType(ct string) *Request {
|
|
|
r.headers.Set("Content-Type", ct)
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetUserAgent(ua string) *Request {
|
|
|
r.headers.Set("User-Agent", ua)
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) DeleteHeader(key string) *Request {
|
|
|
r.headers.Del(key)
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) Cookies() []*http.Cookie {
|
|
|
return r.cookies
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetCookies(cookies []*http.Cookie) *Request {
|
|
|
r.cookies = cookies
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) Transport() *http.Transport {
|
|
|
return r.transport
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetTransport(transport *http.Transport) *Request {
|
|
|
r.transport = transport
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) Queries() map[string][]string {
|
|
|
return r.queries
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetQueries(queries map[string][]string) *Request {
|
|
|
r.queries = queries
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) AddQueries(queries map[string]string) *Request {
|
|
|
for k, v := range queries {
|
|
|
r.queries[k] = append(r.queries[k], v)
|
|
|
}
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) AddQuery(key, value string) *Request {
|
|
|
r.queries[key] = append(r.queries[key], value)
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) DelQueryKv(key, value string) *Request {
|
|
|
if _, ok := r.queries[key]; !ok {
|
|
|
return r
|
|
|
}
|
|
|
for i, v := range r.queries[key] {
|
|
|
if v == value {
|
|
|
r.queries[key] = append(r.queries[key][:i], r.queries[key][i+1:]...)
|
|
|
}
|
|
|
}
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) DelQuery(key string) *Request {
|
|
|
if _, ok := r.queries[key]; !ok {
|
|
|
return r
|
|
|
}
|
|
|
delete(r.queries, key)
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) DisableRedirect() bool {
|
|
|
return r.disableRedirect
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetDisableRedirect(disableRedirect bool) *Request {
|
|
|
r.disableRedirect = disableRedirect
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) DoRawRequest() bool {
|
|
|
return r.doRawRequest
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetDoRawRequest(doRawRequest bool) *Request {
|
|
|
r.doRawRequest = doRawRequest
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) DoRawClient() bool {
|
|
|
return r.doRawClient
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetDoRawClient(doRawClient bool) *Request {
|
|
|
r.doRawClient = doRawClient
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *RequestOpts) SkipTLSVerify() bool {
|
|
|
return r.skipTLSVerify
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetSkipTLSVerify(skipTLSVerify bool) *Request {
|
|
|
r.skipTLSVerify = skipTLSVerify
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) TlsConfig() *tls.Config {
|
|
|
return r.tlsConfig
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetTlsConfig(tlsConfig *tls.Config) *Request {
|
|
|
r.tlsConfig = tlsConfig
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) AutoFetchRespBody() bool {
|
|
|
return r.autoFetchRespBody
|
|
|
}
|
|
|
|
|
|
func (r *Request) SetAutoFetchRespBody(autoFetchRespBody bool) *Request {
|
|
|
r.autoFetchRespBody = autoFetchRespBody
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) ResetReqHeader() *Request {
|
|
|
r.headers = make(http.Header)
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) ResetReqCookies() *Request {
|
|
|
r.cookies = []*http.Cookie{}
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) AddSimpleCookie(key, value string) *Request {
|
|
|
r.cookies = append(r.cookies, &http.Cookie{Name: key, Value: value, Path: "/"})
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) AddCookie(key, value, path string) *Request {
|
|
|
r.cookies = append(r.cookies, &http.Cookie{Name: key, Value: value, Path: path})
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) AddFile(formName, filepath string) error {
|
|
|
f, err := os.Open(filepath)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
stat, err := f.Stat()
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
r.bodyFileData = append(r.bodyFileData, RequestFile{
|
|
|
FormName: formName,
|
|
|
FileName: stat.Name(),
|
|
|
FileData: f,
|
|
|
FileSize: stat.Size(),
|
|
|
FileType: "application/octet-stream",
|
|
|
})
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func (r *Request) AddFileWithName(formName, filepath, filename string) error {
|
|
|
f, err := os.Open(filepath)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
stat, err := f.Stat()
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
r.bodyFileData = append(r.bodyFileData, RequestFile{
|
|
|
FormName: formName,
|
|
|
FileName: filename,
|
|
|
FileData: f,
|
|
|
FileSize: stat.Size(),
|
|
|
FileType: "application/octet-stream",
|
|
|
})
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func (r *Request) AddFileWithType(formName, filepath, filetype string) error {
|
|
|
f, err := os.Open(filepath)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
stat, err := f.Stat()
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
r.bodyFileData = append(r.bodyFileData, RequestFile{
|
|
|
FormName: formName,
|
|
|
FileName: stat.Name(),
|
|
|
FileData: f,
|
|
|
FileSize: stat.Size(),
|
|
|
FileType: filetype,
|
|
|
})
|
|
|
return nil
|
|
|
}
|
|
|
func (r *Request) AddFileWithNameAndType(formName, filepath, filename, filetype string) error {
|
|
|
f, err := os.Open(filepath)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
stat, err := f.Stat()
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
r.bodyFileData = append(r.bodyFileData, RequestFile{
|
|
|
FormName: formName,
|
|
|
FileName: filename,
|
|
|
FileData: f,
|
|
|
FileSize: stat.Size(),
|
|
|
FileType: filetype,
|
|
|
})
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func (r *Request) AddFileNoError(formName, filepath string) *Request {
|
|
|
r.AddFile(formName, filepath)
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) AddFileWithNameNoError(formName, filepath, filename string) *Request {
|
|
|
r.AddFileWithName(formName, filepath, filename)
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func (r *Request) AddFileWithTypeNoError(formName, filepath, filetype string) *Request {
|
|
|
r.AddFileWithType(formName, filepath, filetype)
|
|
|
return r
|
|
|
}
|
|
|
func (r *Request) AddFileWithNameAndTypeNoError(formName, filepath, filename, filetype string) *Request {
|
|
|
r.AddFileWithNameAndType(formName, filepath, filename, filetype)
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
type RequestFile struct {
|
|
|
FormName string
|
|
|
FileName string
|
|
|
FileData io.Reader
|
|
|
FileSize int64
|
|
|
FileType string
|
|
|
}
|
|
|
|
|
|
type RequestOpt func(opt *RequestOpts) error
|
|
|
|
|
|
// if doRawTransport is true, this function will nolonger work
|
|
|
func WithDialTimeout(timeout time.Duration) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
opt.dialTimeout = timeout
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// if doRawTransport is true, this function will nolonger work
|
|
|
func WithTimeout(timeout time.Duration) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
opt.timeout = timeout
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// if doRawTransport is true, this function will nolonger work
|
|
|
func WithTlsConfig(tlscfg *tls.Config) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
opt.tlsConfig = tlscfg
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// if doRawRequest is true, this function will nolonger work
|
|
|
func WithHeader(key, val string) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
opt.headers.Set(key, val)
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// if doRawRequest is true, this function will nolonger work
|
|
|
func WithHeaderMap(header map[string]string) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
for key, val := range header {
|
|
|
opt.headers.Set(key, val)
|
|
|
}
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// if doRawRequest is true, this function will nolonger work
|
|
|
func WithReader(r io.Reader) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
opt.bodyDataReader = r
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// if doRawRequest is true, this function will nolonger work
|
|
|
func WithBytes(r []byte) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
opt.bodyDataBytes = r
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// if doRawRequest is true, this function will nolonger work
|
|
|
func WithFormData(data map[string][]string) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
opt.bodyFormData = data
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// if doRawRequest is true, this function will nolonger work
|
|
|
func WithFileDatas(data []RequestFile) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
opt.bodyFileData = data
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// if doRawRequest is true, this function will nolonger work
|
|
|
func WithFileData(data RequestFile) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
opt.bodyFileData = append(opt.bodyFileData, data)
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// if doRawRequest is true, this function will nolonger work
|
|
|
func WithAddFile(formName, filepath string) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
f, err := os.Open(filepath)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
stat, err := f.Stat()
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
opt.bodyFileData = append(opt.bodyFileData, RequestFile{
|
|
|
FormName: formName,
|
|
|
FileName: stat.Name(),
|
|
|
FileData: f,
|
|
|
FileSize: stat.Size(),
|
|
|
FileType: "application/octet-stream",
|
|
|
})
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func WithAddFileWithName(formName, filepath, filename string) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
f, err := os.Open(filepath)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
stat, err := f.Stat()
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
opt.bodyFileData = append(opt.bodyFileData, RequestFile{
|
|
|
FormName: formName,
|
|
|
FileName: filename,
|
|
|
FileData: f,
|
|
|
FileSize: stat.Size(),
|
|
|
FileType: "application/octet-stream",
|
|
|
})
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func WithAddFileWithType(formName, filepath, filetype string) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
f, err := os.Open(filepath)
|
|
|
if err != nil {
|
|
|
return nil
|
|
|
}
|
|
|
stat, err := f.Stat()
|
|
|
if err != nil {
|
|
|
return nil
|
|
|
}
|
|
|
opt.bodyFileData = append(opt.bodyFileData, RequestFile{
|
|
|
FormName: formName,
|
|
|
FileName: stat.Name(),
|
|
|
FileData: f,
|
|
|
FileSize: stat.Size(),
|
|
|
FileType: filetype,
|
|
|
})
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func WithAddFileWithNameAndType(formName, filepath, filename, filetype string) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
f, err := os.Open(filepath)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
stat, err := f.Stat()
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
opt.bodyFileData = append(opt.bodyFileData, RequestFile{
|
|
|
FormName: formName,
|
|
|
FileName: filename,
|
|
|
FileData: f,
|
|
|
FileSize: stat.Size(),
|
|
|
FileType: filetype,
|
|
|
})
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func WithFetchRespBody(fetch bool) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
opt.autoFetchRespBody = fetch
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func WithCookies(ck []*http.Cookie) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
opt.cookies = ck
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func WithCookie(key, val, path string) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
opt.cookies = append(opt.cookies, &http.Cookie{Name: key, Value: val, Path: path})
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func WithSimpleCookie(key, val string) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
opt.cookies = append(opt.cookies, &http.Cookie{Name: key, Value: val, Path: "/"})
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func WithCookieMap(header map[string]string, path string) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
for key, val := range header {
|
|
|
opt.cookies = append(opt.cookies, &http.Cookie{Name: key, Value: val, Path: path})
|
|
|
}
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func WithQueries(queries map[string][]string) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
opt.queries = queries
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func WithAddQueries(queries map[string]string) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
for k, v := range queries {
|
|
|
opt.queries[k] = append(opt.queries[k], v)
|
|
|
}
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func WithAddQuery(key, val string) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
opt.queries[key] = append(opt.queries[key], val)
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func WithProxy(proxy string) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
opt.proxy = proxy
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func WithProcess(fn func(string, int64, int64)) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
opt.FileUploadRecallFn = fn
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func WithContentType(ct string) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
opt.headers.Set("Content-Type", ct)
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func WithUserAgent(ua string) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
opt.headers.Set("User-Agent", ua)
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func WithSkipTLSVerify(skip bool) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
opt.skipTLSVerify = skip
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func WithDisableRedirect(disable bool) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
opt.disableRedirect = disable
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func WithDoRawRequest(doRawRequest bool) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
opt.doRawRequest = doRawRequest
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func WithDoRawClient(doRawClient bool) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
opt.doRawClient = doRawClient
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func WithDoRawTransport(doRawTrans bool) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
opt.doRawTransport = doRawTrans
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func WithTransport(hs *http.Transport) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
opt.transport = hs
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func WithRawRequest(req *http.Request) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
opt.rawRequest = req
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func WithRawClient(hc *http.Client) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
opt.rawClient = hc
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func WithCustomHostIP(ip []string) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
if len(ip) == 0 {
|
|
|
return nil
|
|
|
}
|
|
|
for _, v := range ip {
|
|
|
if net.ParseIP(v) == nil {
|
|
|
return fmt.Errorf("invalid custom ip: %s", v)
|
|
|
}
|
|
|
}
|
|
|
opt.customIP = ip
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func WithAddCustomHostIP(ip string) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
if net.ParseIP(ip) == nil {
|
|
|
return fmt.Errorf("invalid custom ip: %s", ip)
|
|
|
}
|
|
|
opt.customIP = append(opt.customIP, ip)
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func WithLookUpFn(lookUpIPfn func(ctx context.Context, host string) ([]net.IPAddr, error)) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
if lookUpIPfn == nil {
|
|
|
opt.alreadySetLookUpIPfn = false
|
|
|
opt.lookUpIPfn = net.DefaultResolver.LookupIPAddr
|
|
|
return nil
|
|
|
}
|
|
|
opt.lookUpIPfn = lookUpIPfn
|
|
|
opt.alreadySetLookUpIPfn = true
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// WithCustomDNS will use custom dns to resolve the host
|
|
|
// Note: if LookUpIPfn is set, this function will not be used
|
|
|
func WithCustomDNS(customDNS []string) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
for _, v := range customDNS {
|
|
|
if net.ParseIP(v) == nil {
|
|
|
return fmt.Errorf("invalid custom dns: %s", v)
|
|
|
}
|
|
|
}
|
|
|
opt.customDNS = customDNS
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// WithAddCustomDNS will use a custom dns to resolve the host
|
|
|
// Note: if LookUpIPfn is set, this function will not be used
|
|
|
func WithAddCustomDNS(customDNS string) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
if net.ParseIP(customDNS) == nil {
|
|
|
return fmt.Errorf("invalid custom dns: %s", customDNS)
|
|
|
}
|
|
|
opt.customDNS = append(opt.customDNS, customDNS)
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// WithAutoCalcContentLength sets whether to automatically calculate the Content-Length header based on the request body.
|
|
|
// WARN: If set to true, the Content-Length header will be set to the length of the request body, which may cause issues with chunked transfer encoding.
|
|
|
// also the memory usage will be higher
|
|
|
// Note that this function will not work if doRawRequest is true
|
|
|
func (r *RequestOpts) WithAutoCalcContentLength(autoCalcContentLength bool) RequestOpt {
|
|
|
return func(opt *RequestOpts) error {
|
|
|
r.autoCalcContentLength = autoCalcContentLength
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
type Response struct {
|
|
|
*http.Response
|
|
|
req Request
|
|
|
data *Body
|
|
|
}
|
|
|
|
|
|
type Body struct {
|
|
|
full []byte
|
|
|
raw io.ReadCloser
|
|
|
isFull bool
|
|
|
}
|
|
|
|
|
|
func (b *Body) readAll() {
|
|
|
if !b.isFull {
|
|
|
b.full, _ = io.ReadAll(b.raw)
|
|
|
b.isFull = true
|
|
|
b.raw.Close()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func (b *Body) String() string {
|
|
|
b.readAll()
|
|
|
return string(b.full)
|
|
|
}
|
|
|
|
|
|
func (b *Body) Bytes() []byte {
|
|
|
b.readAll()
|
|
|
return b.full
|
|
|
}
|
|
|
|
|
|
func (b *Body) Unmarshal(u interface{}) error {
|
|
|
b.readAll()
|
|
|
return json.Unmarshal(b.full, u)
|
|
|
}
|
|
|
|
|
|
// Reader returns a reader for the body
|
|
|
// if this function is called, other functions like String, Bytes, Unmarshal may not work
|
|
|
func (b *Body) Reader() io.ReadCloser {
|
|
|
if b.isFull {
|
|
|
return io.NopCloser(bytes.NewReader(b.full))
|
|
|
}
|
|
|
b.isFull = true
|
|
|
return b.raw
|
|
|
}
|
|
|
|
|
|
func (b *Body) Close() error {
|
|
|
return b.raw.Close()
|
|
|
}
|
|
|
|
|
|
func (r *Response) GetRequest() Request {
|
|
|
return r.req
|
|
|
}
|
|
|
|
|
|
func (r *Response) Body() *Body {
|
|
|
return r.data
|
|
|
}
|
|
|
|
|
|
func Curl(r *Request) (*Response, error) {
|
|
|
r.errInfo = nil
|
|
|
err := applyOptions(r)
|
|
|
if err != nil {
|
|
|
return nil, fmt.Errorf("apply options error: %s", err)
|
|
|
}
|
|
|
resp, err := r.rawClient.Do(r.rawRequest)
|
|
|
var res = Response{
|
|
|
Response: resp,
|
|
|
req: *r,
|
|
|
data: new(Body),
|
|
|
}
|
|
|
if err != nil {
|
|
|
res.Response = &http.Response{}
|
|
|
return &res, fmt.Errorf("do request error: %s", err)
|
|
|
}
|
|
|
res.data.raw = resp.Body
|
|
|
if r.autoFetchRespBody {
|
|
|
res.data.full, _ = io.ReadAll(resp.Body)
|
|
|
res.data.isFull = true
|
|
|
resp.Body.Close()
|
|
|
}
|
|
|
return &res, r.errInfo
|
|
|
}
|
|
|
|
|
|
func NewReq(uri string, opts ...RequestOpt) *Request {
|
|
|
return NewSimpleRequest(uri, "GET", opts...)
|
|
|
}
|
|
|
|
|
|
func NewReqWithContext(ctx context.Context, uri string, opts ...RequestOpt) *Request {
|
|
|
return NewSimpleRequestWithContext(ctx, uri, "GET", opts...)
|
|
|
}
|
|
|
|
|
|
func NewSimpleRequest(uri string, method string, opts ...RequestOpt) *Request {
|
|
|
r, _ := newRequest(context.Background(), uri, method, opts...)
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func NewRequest(uri string, method string, opts ...RequestOpt) (*Request, error) {
|
|
|
return newRequest(context.Background(), uri, method, opts...)
|
|
|
}
|
|
|
|
|
|
func NewSimpleRequestWithContext(ctx context.Context, uri string, method string, opts ...RequestOpt) *Request {
|
|
|
r, _ := newRequest(ctx, uri, method, opts...)
|
|
|
return r
|
|
|
}
|
|
|
|
|
|
func NewRequestWithContext(ctx context.Context, uri string, method string, opts ...RequestOpt) (*Request, error) {
|
|
|
return newRequest(ctx, uri, method, opts...)
|
|
|
}
|
|
|
|
|
|
func newRequest(ctx context.Context, uri string, method string, opts ...RequestOpt) (*Request, error) {
|
|
|
var req *http.Request
|
|
|
var err error
|
|
|
if method == "" {
|
|
|
method = "GET"
|
|
|
}
|
|
|
method = strings.ToUpper(method)
|
|
|
req, err = http.NewRequestWithContext(ctx, method, uri, nil)
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
var r = &Request{
|
|
|
ctx: ctx,
|
|
|
uri: uri,
|
|
|
method: method,
|
|
|
RequestOpts: RequestOpts{
|
|
|
rawRequest: req,
|
|
|
rawClient: new(http.Client),
|
|
|
timeout: DefaultTimeout,
|
|
|
dialTimeout: DefaultDialTimeout,
|
|
|
autoFetchRespBody: DefaultFetchRespBody,
|
|
|
lookUpIPfn: net.DefaultResolver.LookupIPAddr,
|
|
|
bodyFormData: make(map[string][]string),
|
|
|
queries: make(map[string][]string),
|
|
|
},
|
|
|
}
|
|
|
|
|
|
r.headers = make(http.Header)
|
|
|
if strings.ToUpper(method) == "POST" {
|
|
|
r.headers.Set("Content-Type", HEADER_FORM_URLENCODE)
|
|
|
}
|
|
|
r.headers.Set("User-Agent", "B612 / 1.2.0")
|
|
|
for _, v := range opts {
|
|
|
if v != nil {
|
|
|
err = v(&r.RequestOpts)
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
if r.transport == nil {
|
|
|
r.transport = &http.Transport{}
|
|
|
}
|
|
|
if r.doRawTransport {
|
|
|
if r.skipTLSVerify {
|
|
|
if r.transport.TLSClientConfig == nil {
|
|
|
r.transport.TLSClientConfig = &tls.Config{}
|
|
|
}
|
|
|
r.transport.TLSClientConfig.InsecureSkipVerify = true
|
|
|
}
|
|
|
if r.tlsConfig != nil {
|
|
|
r.transport.TLSClientConfig = r.tlsConfig
|
|
|
}
|
|
|
r.transport.DialContext = func(ctx context.Context, netType, addr string) (net.Conn, error) {
|
|
|
var lastErr error
|
|
|
var addrs []string
|
|
|
host, port, err := net.SplitHostPort(addr)
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
if len(r.customIP) > 0 {
|
|
|
for _, v := range r.customIP {
|
|
|
ipAddr := net.ParseIP(v)
|
|
|
if ipAddr == nil {
|
|
|
return nil, fmt.Errorf("invalid custom ip: %s", r.customIP)
|
|
|
}
|
|
|
tmpAddr := net.JoinHostPort(v, port)
|
|
|
addrs = append(addrs, tmpAddr)
|
|
|
}
|
|
|
} else {
|
|
|
ipLists, err := r.lookUpIPfn(ctx, host)
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
for _, v := range ipLists {
|
|
|
tmpAddr := net.JoinHostPort(v.String(), port)
|
|
|
addrs = append(addrs, tmpAddr)
|
|
|
}
|
|
|
}
|
|
|
for _, addr := range addrs {
|
|
|
c, err := net.DialTimeout(netType, addr, r.dialTimeout)
|
|
|
if err != nil {
|
|
|
lastErr = err
|
|
|
continue
|
|
|
}
|
|
|
if r.timeout != 0 {
|
|
|
err = c.SetDeadline(time.Now().Add(r.timeout))
|
|
|
}
|
|
|
return c, nil
|
|
|
}
|
|
|
return nil, lastErr
|
|
|
}
|
|
|
}
|
|
|
return r, nil
|
|
|
}
|
|
|
|
|
|
func applyDataReader(r *Request) error {
|
|
|
// 优先度为:bodyDataReader > bodyDataBytes > bodyFormData > bodyFileData
|
|
|
if r.bodyDataReader != nil {
|
|
|
r.rawRequest.Body = io.NopCloser(r.bodyDataReader)
|
|
|
return nil
|
|
|
}
|
|
|
if len(r.bodyDataBytes) != 0 {
|
|
|
r.rawRequest.Body = io.NopCloser(bytes.NewReader(r.bodyDataBytes))
|
|
|
return nil
|
|
|
}
|
|
|
if len(r.bodyFormData) != 0 && len(r.bodyFileData) == 0 {
|
|
|
var body = url.Values{}
|
|
|
for k, v := range r.bodyFormData {
|
|
|
for _, vv := range v {
|
|
|
body.Add(k, vv)
|
|
|
}
|
|
|
}
|
|
|
r.rawRequest.Body = io.NopCloser(strings.NewReader(body.Encode()))
|
|
|
return nil
|
|
|
}
|
|
|
if len(r.bodyFileData) != 0 {
|
|
|
var pr, pw = io.Pipe()
|
|
|
var w = multipart.NewWriter(pw)
|
|
|
r.rawRequest.Header.Set("Content-Type", w.FormDataContentType())
|
|
|
go func() {
|
|
|
defer pw.Close() // ensure pipe writer is closed
|
|
|
|
|
|
if len(r.bodyFormData) != 0 {
|
|
|
for k, v := range r.bodyFormData {
|
|
|
for _, vv := range v {
|
|
|
if err := w.WriteField(k, vv); err != nil {
|
|
|
r.errInfo = err
|
|
|
pw.CloseWithError(err) // close pipe with error
|
|
|
return
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
for _, v := range r.bodyFileData {
|
|
|
var fw, err = w.CreateFormFile(v.FormName, v.FileName)
|
|
|
if err != nil {
|
|
|
r.errInfo = err
|
|
|
pw.CloseWithError(err) // close pipe with error
|
|
|
return
|
|
|
}
|
|
|
if _, err := copyWithContext(r.ctx, r.FileUploadRecallFn, v.FileName, v.FileSize, fw, v.FileData); err != nil {
|
|
|
r.errInfo = err
|
|
|
pw.CloseWithError(err) // close pipe with error
|
|
|
return
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if err := w.Close(); err != nil {
|
|
|
pw.CloseWithError(err) // close pipe with error if writer close fails
|
|
|
}
|
|
|
}()
|
|
|
|
|
|
r.rawRequest.Body = pr
|
|
|
return nil
|
|
|
}
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func applyOptions(r *Request) error {
|
|
|
defer func() {
|
|
|
r.alreadyApply = true
|
|
|
}()
|
|
|
var req = r.rawRequest
|
|
|
if !r.doRawRequest {
|
|
|
if r.queries != nil {
|
|
|
sid := req.URL.Query()
|
|
|
for k, v := range r.queries {
|
|
|
for _, vv := range v {
|
|
|
sid.Add(k, vv)
|
|
|
}
|
|
|
}
|
|
|
req.URL.RawQuery = sid.Encode()
|
|
|
}
|
|
|
for k, v := range r.headers {
|
|
|
for _, vv := range v {
|
|
|
req.Header.Add(k, vv)
|
|
|
}
|
|
|
}
|
|
|
if len(r.cookies) != 0 {
|
|
|
for _, v := range r.cookies {
|
|
|
req.AddCookie(v)
|
|
|
}
|
|
|
}
|
|
|
if r.basicAuth[0] != "" || r.basicAuth[1] != "" {
|
|
|
req.SetBasicAuth(r.basicAuth[0], r.basicAuth[1])
|
|
|
}
|
|
|
err := applyDataReader(r)
|
|
|
if err != nil {
|
|
|
return fmt.Errorf("apply data reader error: %s", err)
|
|
|
}
|
|
|
if r.autoCalcContentLength {
|
|
|
if req.Body != nil {
|
|
|
data, err := io.ReadAll(req.Body)
|
|
|
if err != nil {
|
|
|
return fmt.Errorf("read data error: %s", err)
|
|
|
}
|
|
|
req.Header.Set("Content-Length", strconv.Itoa(len(data)))
|
|
|
req.Body = io.NopCloser(bytes.NewReader(data))
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
if r.proxy != "" {
|
|
|
purl, err := url.Parse(r.proxy)
|
|
|
if err != nil {
|
|
|
return fmt.Errorf("parse proxy url error: %s", err)
|
|
|
}
|
|
|
r.transport.Proxy = http.ProxyURL(purl)
|
|
|
}
|
|
|
if !r.doRawClient {
|
|
|
if !r.doRawTransport {
|
|
|
if !r.alreadySetLookUpIPfn && len(r.customIP) > 0 {
|
|
|
resolver := net.Resolver{
|
|
|
PreferGo: true,
|
|
|
Dial: func(ctx context.Context, network, address string) (conn net.Conn, err error) {
|
|
|
for _, addr := range r.customIP {
|
|
|
if conn, err = net.Dial("udp", addr+":53"); err != nil {
|
|
|
continue
|
|
|
} else {
|
|
|
return conn, nil
|
|
|
}
|
|
|
}
|
|
|
return
|
|
|
},
|
|
|
}
|
|
|
r.lookUpIPfn = resolver.LookupIPAddr
|
|
|
}
|
|
|
}
|
|
|
r.rawClient.Transport = r.transport
|
|
|
if r.disableRedirect {
|
|
|
r.rawClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
|
|
return http.ErrUseLastResponse
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func copyWithContext(ctx context.Context, recall func(string, int64, int64), filename string, total int64, dst io.Writer, src io.Reader) (written int64, err error) {
|
|
|
pr, pw := io.Pipe()
|
|
|
defer pr.Close()
|
|
|
|
|
|
go func() {
|
|
|
defer pw.Close()
|
|
|
_, err := io.Copy(pw, src)
|
|
|
if err != nil {
|
|
|
pw.CloseWithError(err)
|
|
|
}
|
|
|
}()
|
|
|
var count int64
|
|
|
buf := make([]byte, 4096)
|
|
|
for {
|
|
|
select {
|
|
|
case <-ctx.Done():
|
|
|
return written, ctx.Err()
|
|
|
default:
|
|
|
nr, err := pr.Read(buf)
|
|
|
if err != nil {
|
|
|
if err == io.EOF {
|
|
|
go recall(filename, count, total)
|
|
|
return written, nil
|
|
|
}
|
|
|
return written, err
|
|
|
}
|
|
|
count += int64(nr)
|
|
|
if recall != nil {
|
|
|
go recall(filename, count, total)
|
|
|
}
|
|
|
nw, err := dst.Write(buf[:nr])
|
|
|
if err != nil {
|
|
|
return written, err
|
|
|
}
|
|
|
if nr != nw {
|
|
|
return written, io.ErrShortWrite
|
|
|
}
|
|
|
written += int64(nr)
|
|
|
}
|
|
|
}
|
|
|
}
|