Skip to content

Commit

Permalink
Merge e9f2385 into df918f1
Browse files Browse the repository at this point in the history
  • Loading branch information
mstoykov authored May 21, 2021
2 parents df918f1 + e9f2385 commit 8de4709
Show file tree
Hide file tree
Showing 7 changed files with 403 additions and 69 deletions.
23 changes: 13 additions & 10 deletions lib/netext/httpext/error_codes.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,13 @@ const (
tcpBrokenPipeErrorCode errCode = 1201
netUnknownErrnoErrorCode errCode = 1202
tcpDialErrorCode errCode = 1210
tcpDialTimeoutErrorCode errCode = 1211
tcpDialRefusedErrorCode errCode = 1212
tcpDialUnknownErrnoCode errCode = 1213
tcpResetByPeerErrorCode errCode = 1220
// TLS errors
defaultTLSErrorCode errCode = 1300
defaultTLSErrorCode errCode = 1300 //nolint:deadcode,varcheck // this is here to save the number
tlsHeaderErrorCode errCode = 1301
x509UnknownAuthorityErrorCode errCode = 1310
x509HostnameErrorCode errCode = 1311

Expand All @@ -86,6 +88,7 @@ const (

const (
tcpResetByPeerErrorCodeMsg = "%s: connection reset by peer"
tcpDialTimeoutErrorCodeMsg = "dial: i/o timeout"
tcpDialRefusedErrorCodeMsg = "dial: connection refused"
tcpBrokenPipeErrorCodeMsg = "%s: broken pipe"
netUnknownErrnoErrorCodeMsg = "%s: unknown errno `%d` on %s with message `%s`"
Expand Down Expand Up @@ -187,23 +190,23 @@ func errorCodeForError(err error) (errCode, string) {
return blackListedIPErrorCode, blackListedIPErrorCodeMsg
case netext.BlockedHostError:
return blockedHostnameErrorCode, blockedHostnameErrorMsg
case *http2.GoAwayError:
case http2.GoAwayError:
return unknownHTTP2GoAwayErrorCode + http2ErrCodeOffset(e.ErrCode),
fmt.Sprintf(http2GoAwayErrorCodeMsg, e.ErrCode)
case *http2.StreamError:
case http2.StreamError:
return unknownHTTP2StreamErrorCode + http2ErrCodeOffset(e.Code),
fmt.Sprintf(http2StreamErrorCodeMsg, e.Code)
case *http2.ConnectionError:
return unknownHTTP2ConnectionErrorCode + http2ErrCodeOffset(http2.ErrCode(*e)),
fmt.Sprintf(http2ConnectionErrorCodeMsg, http2.ErrCode(*e))
case http2.ConnectionError:
return unknownHTTP2ConnectionErrorCode + http2ErrCodeOffset(http2.ErrCode(e)),
fmt.Sprintf(http2ConnectionErrorCodeMsg, http2.ErrCode(e))
case *net.OpError:
return errorCodeForNetOpError(e)
case *x509.UnknownAuthorityError:
case x509.UnknownAuthorityError:
return x509UnknownAuthorityErrorCode, x509UnknownAuthority
case *x509.HostnameError:
case x509.HostnameError:
return x509HostnameErrorCode, x509HostnameErrorCodeMsg
case *tls.RecordHeaderError:
return defaultTLSErrorCode, err.Error()
case tls.RecordHeaderError:
return tlsHeaderErrorCode, err.Error()
case *url.Error:
return errorCodeForError(e.Err)
default:
Expand Down
238 changes: 211 additions & 27 deletions lib/netext/httpext/error_codes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,18 @@
package httpext

import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"runtime"
"strings"
"syscall"
"testing"
"time"
Expand All @@ -38,40 +41,17 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/net/http2"

"go.k6.io/k6/lib"
"go.k6.io/k6/lib/netext"
"go.k6.io/k6/lib/testutils/httpmultibin"
"go.k6.io/k6/lib/types"
)

func TestDefaultError(t *testing.T) {
t.Parallel()
testErrorCode(t, defaultErrorCode, fmt.Errorf("random error"))
}

func TestHTTP2Errors(t *testing.T) {
t.Parallel()
unknownErrorCode := 220
connectionError := http2.ConnectionError(unknownErrorCode)
testTable := map[errCode]error{
unknownHTTP2ConnectionErrorCode + 1: new(http2.ConnectionError),
unknownHTTP2StreamErrorCode + 1: new(http2.StreamError),
unknownHTTP2GoAwayErrorCode + 1: new(http2.GoAwayError),

unknownHTTP2ConnectionErrorCode: &connectionError,
unknownHTTP2StreamErrorCode: &http2.StreamError{Code: 220},
unknownHTTP2GoAwayErrorCode: &http2.GoAwayError{ErrCode: 220},
}
testMapOfErrorCodes(t, testTable)
}

func TestTLSErrors(t *testing.T) {
t.Parallel()
testTable := map[errCode]error{
x509UnknownAuthorityErrorCode: new(x509.UnknownAuthorityError),
x509HostnameErrorCode: new(x509.HostnameError),
defaultTLSErrorCode: new(tls.RecordHeaderError),
}
testMapOfErrorCodes(t, testTable)
}

func TestDNSErrors(t *testing.T) {
t.Parallel()
var (
Expand Down Expand Up @@ -216,3 +196,207 @@ func TestDnsResolve(t *testing.T) {
assert.Equal(t, dnsNoSuchHostErrorCode, code)
assert.Equal(t, dnsNoSuchHostErrorCodeMsg, msg)
}

func TestHTTP2StreamError(t *testing.T) {
t.Parallel()
tb := httpmultibin.NewHTTPMultiBin(t)

tb.Mux.HandleFunc("/tsr", func(rw http.ResponseWriter, req *http.Request) {
rw.Header().Set("Content-Length", "100000")
rw.WriteHeader(200)

rw.(http.Flusher).Flush()
time.Sleep(time.Millisecond * 2)
panic("expected internal error")
})
client := http.Client{
Timeout: time.Second * 3,
Transport: tb.HTTPTransport,
}

res, err := client.Get(tb.Replacer.Replace("HTTP2BIN_URL/tsr")) //nolint:noctx
require.NotNil(t, res)
require.NoError(t, err)
_, err = ioutil.ReadAll(res.Body)
_ = res.Body.Close()
require.Error(t, err)

code, msg := errorCodeForError(err)
assert.Equal(t, unknownHTTP2StreamErrorCode+errCode(http2.ErrCodeInternal)+1, code)
assert.Contains(t, msg, fmt.Sprintf(http2StreamErrorCodeMsg, http2.ErrCodeInternal))
}

func TestX509HostnameError(t *testing.T) {
t.Parallel()
tb := httpmultibin.NewHTTPMultiBin(t)

client := http.Client{
Timeout: time.Second * 3,
Transport: tb.HTTPTransport,
}
var err error
badHostname := "somewhere.else"
tb.Dialer.Hosts[badHostname], err = lib.NewHostAddress(net.ParseIP(tb.Replacer.Replace("HTTPSBIN_IP")), "")
require.NoError(t, err)
req, err := http.NewRequestWithContext(context.Background(), "GET", tb.Replacer.Replace("https://"+badHostname+":HTTPSBIN_PORT/get"), nil)
require.NoError(t, err)
res, err := client.Do(req) //nolint:bodyclose
require.Nil(t, res)
require.Error(t, err)

code, msg := errorCodeForError(err)
assert.Equal(t, x509HostnameErrorCode, code)
assert.Contains(t, msg, x509HostnameErrorCodeMsg)
}

func TestX509UnknownAuthorityError(t *testing.T) {
t.Parallel()
tb := httpmultibin.NewHTTPMultiBin(t)

client := http.Client{
Timeout: time.Second * 3,
Transport: &http.Transport{
DialContext: tb.HTTPTransport.DialContext,
},
}
req, err := http.NewRequestWithContext(context.Background(), "GET", tb.Replacer.Replace("HTTPSBIN_URL/get"), nil)
require.NoError(t, err)
res, err := client.Do(req) //nolint:bodyclose
require.Nil(t, res)
require.Error(t, err)

code, msg := errorCodeForError(err)
assert.Equal(t, x509UnknownAuthorityErrorCode, code)
assert.Contains(t, msg, x509UnknownAuthority)
}

func TestDefaultTLSError(t *testing.T) {
t.Parallel()

l, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
go func() {
conn, err := l.Accept() //nolint:govet // the shadowing is intentional
require.NoError(t, err)
_, err = conn.Write([]byte("not tls header")) // we just want to get an error
require.NoError(t, err)
// wait so it has time to get the tls header error and not the reset socket one
time.Sleep(time.Second)
}()

client := http.Client{
Timeout: time.Second * 3,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, //nolint:gosec
},
},
}

_, err = client.Get("https://" + l.Addr().String()) //nolint:bodyclose,noctx
require.Error(t, err)

code, msg := errorCodeForError(err)
assert.Equal(t, tlsHeaderErrorCode, code)
urlError := new(url.Error)
require.ErrorAs(t, err, &urlError)
assert.Equal(t, urlError.Err.Error(), msg)
}

func TestHTTP2ConnectionError(t *testing.T) {
t.Parallel()
tb := getHTTP2ServerWithCustomConnContext(t)

// Pre-configure the HTTP client transport with the dialer and TLS config (incl. HTTP2 support)
tb.Mux.HandleFunc("/tsr", func(rw http.ResponseWriter, req *http.Request) {
conn := req.Context().Value(connKey).(*tls.Conn) //nolint:forcetypeassert
f := http2.NewFramer(conn, conn)
require.NoError(t, f.WriteData(3213, false, []byte("something")))
})
client := http.Client{
Timeout: time.Second * 5,
Transport: tb.HTTPTransport,
}

_, err := client.Get(tb.Replacer.Replace("HTTP2BIN_URL/tsr")) //nolint:bodyclose,noctx
code, msg := errorCodeForError(err)
assert.Equal(t, unknownHTTP2ConnectionErrorCode+errCode(http2.ErrCodeProtocol)+1, code)
assert.Equal(t, fmt.Sprintf(http2ConnectionErrorCodeMsg, http2.ErrCodeProtocol), msg)
}

func TestHTTP2GoAwayError(t *testing.T) {
t.Parallel()

tb := getHTTP2ServerWithCustomConnContext(t)
tb.Mux.HandleFunc("/tsr", func(rw http.ResponseWriter, req *http.Request) {
conn := req.Context().Value(connKey).(*tls.Conn) //nolint:forcetypeassert
f := http2.NewFramer(conn, conn)
require.NoError(t, f.WriteGoAway(4, http2.ErrCodeInadequateSecurity, []byte("whatever")))
require.NoError(t, conn.CloseWrite())
})
client := http.Client{
Timeout: time.Second * 5,
Transport: tb.HTTPTransport,
}

_, err := client.Get(tb.Replacer.Replace("HTTP2BIN_URL/tsr")) //nolint:bodyclose,noctx

require.Error(t, err)
code, msg := errorCodeForError(err)
assert.Equal(t, unknownHTTP2GoAwayErrorCode+errCode(http2.ErrCodeInadequateSecurity)+1, code)
assert.Equal(t, fmt.Sprintf(http2GoAwayErrorCodeMsg, http2.ErrCodeInadequateSecurity), msg)
}

type connKeyT int32

const connKey connKeyT = 2

func getHTTP2ServerWithCustomConnContext(t *testing.T) *httpmultibin.HTTPMultiBin {
const http2Domain = "example.com"
mux := http.NewServeMux()
http2Srv := httptest.NewUnstartedServer(mux)
http2Srv.EnableHTTP2 = true
http2Srv.Config.ConnContext = func(ctx context.Context, c net.Conn) context.Context {
return context.WithValue(ctx, connKey, c)
}
http2Srv.StartTLS()
t.Cleanup(http2Srv.Close)
tlsConfig := httpmultibin.GetTLSClientConfig(t, http2Srv)

http2URL, err := url.Parse(http2Srv.URL)
require.NoError(t, err)
http2IP := net.ParseIP(http2URL.Hostname())
require.NotNil(t, http2IP)
http2DomainValue, err := lib.NewHostAddress(http2IP, "")
require.NoError(t, err)

// Set up the dialer with shorter timeouts and the custom domains
dialer := netext.NewDialer(net.Dialer{
Timeout: 2 * time.Second,
KeepAlive: 10 * time.Second,
DualStack: true,
}, netext.NewResolver(net.LookupIP, 0, types.DNSfirst, types.DNSpreferIPv4))
dialer.Hosts = map[string]*lib.HostAddress{
http2Domain: http2DomainValue,
}

transport := &http.Transport{
DialContext: dialer.DialContext,
TLSClientConfig: tlsConfig,
}
require.NoError(t, http2.ConfigureTransport(transport))
return &httpmultibin.HTTPMultiBin{
Mux: mux,
ServerHTTP2: http2Srv,
Replacer: strings.NewReplacer(
"HTTP2BIN_IP_URL", http2Srv.URL,
"HTTP2BIN_DOMAIN", http2Domain,
"HTTP2BIN_URL", fmt.Sprintf("https://%s:%s", http2Domain, http2URL.Port()),
"HTTP2BIN_IP", http2IP.String(),
"HTTP2BIN_PORT", http2URL.Port(),
),
TLSClientConfig: tlsConfig,
Dialer: dialer,
HTTPTransport: transport,
}
}
9 changes: 8 additions & 1 deletion lib/netext/httpext/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ package httpext
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"io/ioutil"
Expand Down Expand Up @@ -338,7 +339,13 @@ func MakeRequest(ctx context.Context, preq *ParsedHTTPRequest) (*Response, error
return nil, fmt.Errorf("unsupported response status: %s", res.Status)
}

resp.Body, resErr = readResponseBody(state, preq.ResponseType, res, resErr)
if resErr == nil {
resp.Body, resErr = readResponseBody(state, preq.ResponseType, res, resErr)
if resErr != nil && errors.Is(resErr, context.DeadlineExceeded) {
// TODO This can be more specific that the timeout happened in the middle of the reading of the body
resErr = NewK6Error(requestTimeoutErrorCode, requestTimeoutErrorCodeMsg, resErr)
}
}
finishedReq := tracerTransport.processLastSavedRequest(wrapDecompressionError(resErr))
if finishedReq != nil {
updateK6Response(resp, finishedReq)
Expand Down
Loading

0 comments on commit 8de4709

Please sign in to comment.