Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix error codes for http2, x509 and tls errors #2025

Merged
merged 6 commits into from
May 21, 2021
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This summing of error codes seems very brittle, and I just saw we do the same in http2ErrorCodeOffset(). :-/ Why is this done instead of having a specific error code we can assert on?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are a bunch of error codes defined in the http2 specification and some of them can be returned for stream, some for connection errors so it was easier to just have 20 codes between those and make the codes on the spot instead of defining like 14 error codes per http2*Error

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I guess it's fine since they're constants, but it's very confusing to read. Maybe some comments would help.

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