Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
160 changes: 128 additions & 32 deletions dialtesting/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ package dialtesting

import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
Expand All @@ -25,6 +26,9 @@ import (
"strings"
"text/template"
"time"

"github.com/quic-go/quic-go/http3"
"golang.org/x/net/http2"
)

var (
Expand All @@ -35,6 +39,12 @@ var (
const (
MaxBodySize = 10 * 1024
DefaultHTTPTimeout = 60 * time.Second

ProtocolAuto = "auto"
ProtocolHTTP11 = "http/1.1"
ProtocolHTTP2 = "http/2"
ProtocolHTTP2Only = "http/2-only"
ProtocolHTTP3 = "http/3"
)

type HTTPTask struct {
Expand Down Expand Up @@ -68,6 +78,7 @@ type HTTPTask struct {
rawTask *HTTPTask
sslCertNotBefore int64
sslCertNotAfter int64
protocol string
}

func (t *HTTPTask) clear() {
Expand Down Expand Up @@ -117,7 +128,9 @@ func (t *HTTPTask) getResults() (tags map[string]string, fields map[string]inter
tags["url"] = t.rawTask.URL
}

if t.req != nil {
if t.resp != nil {
tags["proto"] = t.resp.Proto
} else if t.req != nil {
tags["proto"] = t.req.Proto
}

Expand Down Expand Up @@ -271,12 +284,31 @@ type HTTPAdvanceOption struct {
Proxy *HTTPOptProxy `json:"proxy,omitempty"`
Secret *HTTPSecret `json:"secret,omitempty"`
RequestTimeout string `json:"request_timeout,omitempty"`
Protocol string `json:"protocol,omitempty"` // "auto", "http/1.1", "http/2", "http/3"
}

type HTTPSecret struct {
NoSaveResponseBody bool `json:"not_save,omitempty"`
}

func (opt *HTTPAdvanceOption) getProtocol() string {
if opt == nil {
return ProtocolAuto
}
switch strings.ToLower(opt.Protocol) {
case "http/1.1", "http1.1", "1.1", "http/1.1 only":
return ProtocolHTTP11
case "http/2", "http2", "2", "http/2 fallback", "http/2 fallback to http/1.1":
return ProtocolHTTP2
case "http/2-only", "http/2 only", "http2-only":
return ProtocolHTTP2Only
case "http/3", "http3", "3":
return ProtocolHTTP3
default:
return ProtocolAuto
}
}

func (t *HTTPTask) run() error {
var (
t1,
Expand Down Expand Up @@ -339,7 +371,9 @@ func (t *HTTPTask) run() error {

t.req = t.req.WithContext(httptrace.WithClientTrace(t.req.Context(), trace))

t.req.Header.Add("Connection", "close")
if t.protocol != ProtocolHTTP2Only && t.protocol != ProtocolHTTP3 {
t.req.Header.Add("Connection", "close")
}

if agentInfo, ok := t.GetOption()["userAgent"]; ok {
t.req.Header.Add("User-Agent", agentInfo)
Expand All @@ -355,6 +389,11 @@ func (t *HTTPTask) run() error {
goto result
}

if t.protocol == ProtocolHTTP2Only && t.resp.Proto != "HTTP/2.0" {
t.reqError = fmt.Sprintf("expected HTTP/2, but got %s", t.resp.Proto)
goto result
}

t.respBody, err = io.ReadAll(t.resp.Body)
t.reqCost = time.Since(t.reqStart)
if err != nil {
Expand Down Expand Up @@ -568,9 +607,84 @@ func (t *HTTPTask) init() error {
httpTimeout = du
}

// setup HTTP client
t.cli = &http.Client{
Timeout: httpTimeout,
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
}

if opt != nil && opt.Certificate != nil {
if opt.Certificate.IgnoreServerCertificateError {
tlsConfig.InsecureSkipVerify = true //nolint:gosec
} else if opt.Certificate.CaCert != "" {
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM([]byte(opt.Certificate.CaCert))
tlsConfig.RootCAs = caCertPool

cert, err := tls.X509KeyPair([]byte(opt.Certificate.Certificate), []byte(opt.Certificate.PrivateKey))
if err != nil {
return err
}
tlsConfig.Certificates = []tls.Certificate{cert}
}
}

protocol := strings.ToLower(opt.getProtocol())
t.protocol = protocol

switch protocol {
case ProtocolHTTP3:
t.cli = &http.Client{
Timeout: httpTimeout,
Transport: &http3.RoundTripper{
TLSClientConfig: tlsConfig,
},
}
case ProtocolHTTP2Only:
if isPlainHTTP(t.URL) {
t.cli = &http.Client{
Timeout: httpTimeout,
Transport: &http2.Transport{
AllowHTTP: true,
DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {
var dialer net.Dialer
return dialer.DialContext(ctx, network, addr)
},
},
}
} else {
http2OnlyTLS := tlsConfig.Clone()
http2OnlyTLS.NextProtos = []string{"h2"}
t.cli = &http.Client{
Timeout: httpTimeout,
Transport: &http.Transport{
TLSClientConfig: http2OnlyTLS,
ForceAttemptHTTP2: true,
},
}
}
case ProtocolHTTP2:
t.cli = &http.Client{
Timeout: httpTimeout,
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
ForceAttemptHTTP2: true,
},
}
case ProtocolHTTP11:
t.cli = &http.Client{
Timeout: httpTimeout,
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
ForceAttemptHTTP2: false,
},
}
default:
t.cli = &http.Client{
Timeout: httpTimeout,
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
ForceAttemptHTTP2: true,
},
}
}

if opt != nil {
Expand All @@ -594,33 +708,10 @@ func (t *HTTPTask) init() error {
opt.RequestBody.bodyType = opt.RequestBody.BodyType
}

// TLS opotions
if opt.Certificate != nil { // see https://venilnoronha.io/a-step-by-step-guide-to-mtls-in-go
if opt.Certificate.IgnoreServerCertificateError {
t.cli.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: opt.Certificate.IgnoreServerCertificateError, //nolint:gosec
},
}
} else if opt.Certificate.CaCert != "" {
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM([]byte(opt.Certificate.CaCert))

cert, err := tls.X509KeyPair([]byte(opt.Certificate.Certificate), []byte(opt.Certificate.PrivateKey))
if err != nil {
return err
}

t.cli.Transport = &http.Transport{
TLSClientConfig: &tls.Config{ //nolint:gosec
RootCAs: caCertPool,
Certificates: []tls.Certificate{cert},
},
}
}
if protocol == ProtocolHTTP3 && opt.Proxy != nil {
return fmt.Errorf("HTTP/3 does not support proxy configuration")
}

// proxy options
if opt.Proxy != nil { // see https://stackoverflow.com/a/14663620/342348
proxyURL, err := url.Parse(opt.Proxy.URL)
if err != nil {
Expand All @@ -629,8 +720,8 @@ func (t *HTTPTask) init() error {

if t.cli.Transport == nil {
t.cli.Transport = &http.Transport{Proxy: http.ProxyURL(proxyURL)}
} else {
t.cli.Transport.(*http.Transport).Proxy = http.ProxyURL(proxyURL)
} else if transport, ok := t.cli.Transport.(*http.Transport); ok {
transport.Proxy = http.ProxyURL(proxyURL)
}
}
}
Expand Down Expand Up @@ -678,6 +769,11 @@ func (t *HTTPTask) init() error {
return nil
}

func isPlainHTTP(rawURL string) bool {
u, err := url.Parse(rawURL)
return err == nil && strings.EqualFold(u.Scheme, "http")
}

func (t *HTTPTask) getHostName() ([]string, error) {
if hostName, err := getHostName(t.URL); err != nil {
return nil, err
Expand Down
Loading
Loading