k8s.io/apimachinery@v0.29.2/pkg/util/net/http_test.go (about)

     1  //go:build go1.8
     2  // +build go1.8
     3  
     4  /*
     5  Copyright 2016 The Kubernetes Authors.
     6  
     7  Licensed under the Apache License, Version 2.0 (the "License");
     8  you may not use this file except in compliance with the License.
     9  You may obtain a copy of the License at
    10  
    11      http://www.apache.org/licenses/LICENSE-2.0
    12  
    13  Unless required by applicable law or agreed to in writing, software
    14  distributed under the License is distributed on an "AS IS" BASIS,
    15  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    16  See the License for the specific language governing permissions and
    17  limitations under the License.
    18  */
    19  
    20  package net
    21  
    22  import (
    23  	"crypto/tls"
    24  	"fmt"
    25  	"io"
    26  	"net"
    27  	"net/http"
    28  	"net/url"
    29  	"reflect"
    30  	"strings"
    31  	"testing"
    32  
    33  	"github.com/stretchr/testify/assert"
    34  	netutils "k8s.io/utils/net"
    35  )
    36  
    37  func TestGetClientIP(t *testing.T) {
    38  	ipString := "10.0.0.1"
    39  	ip := netutils.ParseIPSloppy(ipString)
    40  	invalidIPString := "invalidIPString"
    41  	testCases := []struct {
    42  		Request    http.Request
    43  		ExpectedIP net.IP
    44  	}{
    45  		{
    46  			Request: http.Request{},
    47  		},
    48  		{
    49  			Request: http.Request{
    50  				Header: map[string][]string{
    51  					"X-Real-Ip": {ipString},
    52  				},
    53  			},
    54  			ExpectedIP: ip,
    55  		},
    56  		{
    57  			Request: http.Request{
    58  				Header: map[string][]string{
    59  					"X-Real-Ip": {invalidIPString},
    60  				},
    61  			},
    62  		},
    63  		{
    64  			Request: http.Request{
    65  				Header: map[string][]string{
    66  					"X-Forwarded-For": {ipString},
    67  				},
    68  			},
    69  			ExpectedIP: ip,
    70  		},
    71  		{
    72  			Request: http.Request{
    73  				Header: map[string][]string{
    74  					"X-Forwarded-For": {invalidIPString},
    75  				},
    76  			},
    77  		},
    78  		{
    79  			Request: http.Request{
    80  				Header: map[string][]string{
    81  					"X-Forwarded-For": {invalidIPString + "," + ipString},
    82  				},
    83  			},
    84  			ExpectedIP: ip,
    85  		},
    86  		{
    87  			Request: http.Request{
    88  				// RemoteAddr is in the form host:port
    89  				RemoteAddr: ipString + ":1234",
    90  			},
    91  			ExpectedIP: ip,
    92  		},
    93  		{
    94  			Request: http.Request{
    95  				RemoteAddr: invalidIPString,
    96  			},
    97  		},
    98  		{
    99  			Request: http.Request{
   100  				Header: map[string][]string{
   101  					"X-Forwarded-For": {invalidIPString},
   102  				},
   103  				// RemoteAddr is in the form host:port
   104  				RemoteAddr: ipString,
   105  			},
   106  			ExpectedIP: ip,
   107  		},
   108  	}
   109  
   110  	for i, test := range testCases {
   111  		if a, e := GetClientIP(&test.Request), test.ExpectedIP; reflect.DeepEqual(e, a) != true {
   112  			t.Fatalf("test case %d failed. expected: %v, actual: %v", i, e, a)
   113  		}
   114  	}
   115  }
   116  
   117  func TestAppendForwardedForHeader(t *testing.T) {
   118  	testCases := []struct {
   119  		addr, forwarded, expected string
   120  	}{
   121  		{"1.2.3.4:8000", "", "1.2.3.4"},
   122  		{"1.2.3.4:8000", "8.8.8.8", "8.8.8.8, 1.2.3.4"},
   123  		{"1.2.3.4:8000", "8.8.8.8, 1.2.3.4", "8.8.8.8, 1.2.3.4, 1.2.3.4"},
   124  		{"1.2.3.4:8000", "foo,bar", "foo,bar, 1.2.3.4"},
   125  	}
   126  	for i, test := range testCases {
   127  		req := &http.Request{
   128  			RemoteAddr: test.addr,
   129  			Header:     make(http.Header),
   130  		}
   131  		if test.forwarded != "" {
   132  			req.Header.Set("X-Forwarded-For", test.forwarded)
   133  		}
   134  
   135  		AppendForwardedForHeader(req)
   136  		actual := req.Header.Get("X-Forwarded-For")
   137  		if actual != test.expected {
   138  			t.Errorf("[%d] Expected %q, Got %q", i, test.expected, actual)
   139  		}
   140  	}
   141  }
   142  
   143  func TestProxierWithNoProxyCIDR(t *testing.T) {
   144  	testCases := []struct {
   145  		name    string
   146  		noProxy string
   147  		url     string
   148  
   149  		expectedDelegated bool
   150  	}{
   151  		{
   152  			name:              "no env",
   153  			url:               "https://192.168.143.1/api",
   154  			expectedDelegated: true,
   155  		},
   156  		{
   157  			name:              "no cidr",
   158  			noProxy:           "192.168.63.1",
   159  			url:               "https://192.168.143.1/api",
   160  			expectedDelegated: true,
   161  		},
   162  		{
   163  			name:              "hostname",
   164  			noProxy:           "192.168.63.0/24,192.168.143.0/24",
   165  			url:               "https://my-hostname/api",
   166  			expectedDelegated: true,
   167  		},
   168  		{
   169  			name:              "match second cidr",
   170  			noProxy:           "192.168.63.0/24,192.168.143.0/24",
   171  			url:               "https://192.168.143.1/api",
   172  			expectedDelegated: false,
   173  		},
   174  		{
   175  			name:              "match second cidr with host:port",
   176  			noProxy:           "192.168.63.0/24,192.168.143.0/24",
   177  			url:               "https://192.168.143.1:8443/api",
   178  			expectedDelegated: false,
   179  		},
   180  		{
   181  			name:              "IPv6 cidr",
   182  			noProxy:           "2001:db8::/48",
   183  			url:               "https://[2001:db8::1]/api",
   184  			expectedDelegated: false,
   185  		},
   186  		{
   187  			name:              "IPv6+port cidr",
   188  			noProxy:           "2001:db8::/48",
   189  			url:               "https://[2001:db8::1]:8443/api",
   190  			expectedDelegated: false,
   191  		},
   192  		{
   193  			name:              "IPv6, not matching cidr",
   194  			noProxy:           "2001:db8::/48",
   195  			url:               "https://[2001:db8:1::1]/api",
   196  			expectedDelegated: true,
   197  		},
   198  		{
   199  			name:              "IPv6+port, not matching cidr",
   200  			noProxy:           "2001:db8::/48",
   201  			url:               "https://[2001:db8:1::1]:8443/api",
   202  			expectedDelegated: true,
   203  		},
   204  	}
   205  
   206  	for _, test := range testCases {
   207  		t.Setenv("NO_PROXY", test.noProxy)
   208  		actualDelegated := false
   209  		proxyFunc := NewProxierWithNoProxyCIDR(func(req *http.Request) (*url.URL, error) {
   210  			actualDelegated = true
   211  			return nil, nil
   212  		})
   213  
   214  		req, err := http.NewRequest("GET", test.url, nil)
   215  		if err != nil {
   216  			t.Errorf("%s: unexpected err: %v", test.name, err)
   217  			continue
   218  		}
   219  		if _, err := proxyFunc(req); err != nil {
   220  			t.Errorf("%s: unexpected err: %v", test.name, err)
   221  			continue
   222  		}
   223  
   224  		if test.expectedDelegated != actualDelegated {
   225  			t.Errorf("%s: expected %v, got %v", test.name, test.expectedDelegated, actualDelegated)
   226  			continue
   227  		}
   228  	}
   229  }
   230  
   231  type fakeTLSClientConfigHolder struct {
   232  	called bool
   233  }
   234  
   235  func (f *fakeTLSClientConfigHolder) TLSClientConfig() *tls.Config {
   236  	f.called = true
   237  	return nil
   238  }
   239  func (f *fakeTLSClientConfigHolder) RoundTrip(*http.Request) (*http.Response, error) {
   240  	return nil, nil
   241  }
   242  
   243  func TestTLSClientConfigHolder(t *testing.T) {
   244  	rt := &fakeTLSClientConfigHolder{}
   245  	TLSClientConfig(rt)
   246  
   247  	if !rt.called {
   248  		t.Errorf("didn't find tls config")
   249  	}
   250  }
   251  
   252  func TestJoinPreservingTrailingSlash(t *testing.T) {
   253  	tests := []struct {
   254  		a    string
   255  		b    string
   256  		want string
   257  	}{
   258  		// All empty
   259  		{"", "", ""},
   260  
   261  		// Empty a
   262  		{"", "/", "/"},
   263  		{"", "foo", "foo"},
   264  		{"", "/foo", "/foo"},
   265  		{"", "/foo/", "/foo/"},
   266  
   267  		// Empty b
   268  		{"/", "", "/"},
   269  		{"foo", "", "foo"},
   270  		{"/foo", "", "/foo"},
   271  		{"/foo/", "", "/foo/"},
   272  
   273  		// Both populated
   274  		{"/", "/", "/"},
   275  		{"foo", "foo", "foo/foo"},
   276  		{"/foo", "/foo", "/foo/foo"},
   277  		{"/foo/", "/foo/", "/foo/foo/"},
   278  	}
   279  	for _, tt := range tests {
   280  		name := fmt.Sprintf("%q+%q=%q", tt.a, tt.b, tt.want)
   281  		t.Run(name, func(t *testing.T) {
   282  			if got := JoinPreservingTrailingSlash(tt.a, tt.b); got != tt.want {
   283  				t.Errorf("JoinPreservingTrailingSlash() = %v, want %v", got, tt.want)
   284  			}
   285  		})
   286  	}
   287  }
   288  
   289  func TestAllowsHTTP2(t *testing.T) {
   290  	testcases := []struct {
   291  		Name         string
   292  		Transport    *http.Transport
   293  		ExpectAllows bool
   294  	}{
   295  		{
   296  			Name:         "empty",
   297  			Transport:    &http.Transport{},
   298  			ExpectAllows: true,
   299  		},
   300  		{
   301  			Name:         "empty tlsconfig",
   302  			Transport:    &http.Transport{TLSClientConfig: &tls.Config{}},
   303  			ExpectAllows: true,
   304  		},
   305  		{
   306  			Name:         "zero-length NextProtos",
   307  			Transport:    &http.Transport{TLSClientConfig: &tls.Config{NextProtos: []string{}}},
   308  			ExpectAllows: true,
   309  		},
   310  		{
   311  			Name:         "includes h2 in NextProtos after",
   312  			Transport:    &http.Transport{TLSClientConfig: &tls.Config{NextProtos: []string{"http/1.1", "h2"}}},
   313  			ExpectAllows: true,
   314  		},
   315  		{
   316  			Name:         "includes h2 in NextProtos before",
   317  			Transport:    &http.Transport{TLSClientConfig: &tls.Config{NextProtos: []string{"h2", "http/1.1"}}},
   318  			ExpectAllows: true,
   319  		},
   320  		{
   321  			Name:         "includes h2 in NextProtos between",
   322  			Transport:    &http.Transport{TLSClientConfig: &tls.Config{NextProtos: []string{"http/1.1", "h2", "h3"}}},
   323  			ExpectAllows: true,
   324  		},
   325  		{
   326  			Name:         "excludes h2 in NextProtos",
   327  			Transport:    &http.Transport{TLSClientConfig: &tls.Config{NextProtos: []string{"http/1.1"}}},
   328  			ExpectAllows: false,
   329  		},
   330  	}
   331  
   332  	for _, tc := range testcases {
   333  		t.Run(tc.Name, func(t *testing.T) {
   334  			allows := allowsHTTP2(tc.Transport)
   335  			if allows != tc.ExpectAllows {
   336  				t.Errorf("expected %v, got %v", tc.ExpectAllows, allows)
   337  			}
   338  		})
   339  	}
   340  }
   341  
   342  func TestSourceIPs(t *testing.T) {
   343  	tests := []struct {
   344  		name         string
   345  		realIP       string
   346  		forwardedFor string
   347  		remoteAddr   string
   348  		expected     []string
   349  	}{{
   350  		name:     "no headers, missing remoteAddr",
   351  		expected: []string{},
   352  	}, {
   353  		name:       "no headers, just remoteAddr host:port",
   354  		remoteAddr: "1.2.3.4:555",
   355  		expected:   []string{"1.2.3.4"},
   356  	}, {
   357  		name:       "no headers, just remoteAddr host",
   358  		remoteAddr: "1.2.3.4",
   359  		expected:   []string{"1.2.3.4"},
   360  	}, {
   361  		name:         "empty forwarded-for chain",
   362  		forwardedFor: " ",
   363  		remoteAddr:   "1.2.3.4",
   364  		expected:     []string{"1.2.3.4"},
   365  	}, {
   366  		name:         "invalid forwarded-for chain",
   367  		forwardedFor: "garbage garbage values!",
   368  		remoteAddr:   "1.2.3.4",
   369  		expected:     []string{"1.2.3.4"},
   370  	}, {
   371  		name:         "partially invalid forwarded-for chain",
   372  		forwardedFor: "garbage garbage values!,4.5.6.7",
   373  		remoteAddr:   "1.2.3.4",
   374  		expected:     []string{"4.5.6.7", "1.2.3.4"},
   375  	}, {
   376  		name:         "valid forwarded-for chain",
   377  		forwardedFor: "120.120.120.126,2.2.2.2,4.5.6.7",
   378  		remoteAddr:   "1.2.3.4",
   379  		expected:     []string{"120.120.120.126", "2.2.2.2", "4.5.6.7", "1.2.3.4"},
   380  	}, {
   381  		name:         "valid forwarded-for chain with redundant remoteAddr",
   382  		forwardedFor: "2.2.2.2,1.2.3.4",
   383  		remoteAddr:   "1.2.3.4",
   384  		expected:     []string{"2.2.2.2", "1.2.3.4"},
   385  	}, {
   386  		name:       "invalid Real-Ip",
   387  		realIP:     "garbage, just garbage!",
   388  		remoteAddr: "1.2.3.4",
   389  		expected:   []string{"1.2.3.4"},
   390  	}, {
   391  		name:         "invalid Real-Ip with forwarded-for",
   392  		realIP:       "garbage, just garbage!",
   393  		forwardedFor: "2.2.2.2",
   394  		remoteAddr:   "1.2.3.4",
   395  		expected:     []string{"2.2.2.2", "1.2.3.4"},
   396  	}, {
   397  		name:       "valid Real-Ip",
   398  		realIP:     "2.2.2.2",
   399  		remoteAddr: "1.2.3.4",
   400  		expected:   []string{"2.2.2.2", "1.2.3.4"},
   401  	}, {
   402  		name:       "redundant Real-Ip",
   403  		realIP:     "1.2.3.4",
   404  		remoteAddr: "1.2.3.4",
   405  		expected:   []string{"1.2.3.4"},
   406  	}, {
   407  		name:         "valid Real-Ip with forwarded-for",
   408  		realIP:       "2.2.2.2",
   409  		forwardedFor: "120.120.120.126,4.5.6.7",
   410  		remoteAddr:   "1.2.3.4",
   411  		expected:     []string{"120.120.120.126", "4.5.6.7", "2.2.2.2", "1.2.3.4"},
   412  	}, {
   413  		name:         "redundant Real-Ip with forwarded-for",
   414  		realIP:       "2.2.2.2",
   415  		forwardedFor: "120.120.120.126,2.2.2.2,4.5.6.7",
   416  		remoteAddr:   "1.2.3.4",
   417  		expected:     []string{"120.120.120.126", "2.2.2.2", "4.5.6.7", "1.2.3.4"},
   418  	}, {
   419  		name:         "full redundancy",
   420  		realIP:       "1.2.3.4",
   421  		forwardedFor: "1.2.3.4",
   422  		remoteAddr:   "1.2.3.4",
   423  		expected:     []string{"1.2.3.4"},
   424  	}, {
   425  		name:         "full ipv6",
   426  		realIP:       "abcd:ef01:2345:6789:abcd:ef01:2345:6789",
   427  		forwardedFor: "aaaa:bbbb:cccc:dddd:eeee:ffff:0:1111,0:1111:2222:3333:4444:5555:6666:7777",
   428  		remoteAddr:   "aaaa:aaaa:aaaa:aaaa:aaaa:aaaa:aaaa:aaaa",
   429  		expected: []string{
   430  			"aaaa:bbbb:cccc:dddd:eeee:ffff:0:1111",
   431  			"0:1111:2222:3333:4444:5555:6666:7777",
   432  			"abcd:ef01:2345:6789:abcd:ef01:2345:6789",
   433  			"aaaa:aaaa:aaaa:aaaa:aaaa:aaaa:aaaa:aaaa",
   434  		},
   435  	}, {
   436  		name:         "mixed ipv4 ipv6",
   437  		forwardedFor: "aaaa:bbbb:cccc:dddd:eeee:ffff:0:1111,1.2.3.4",
   438  		remoteAddr:   "0:0:0:0:0:ffff:102:304", // ipv6 equivalent to 1.2.3.4
   439  		expected: []string{
   440  			"aaaa:bbbb:cccc:dddd:eeee:ffff:0:1111",
   441  			"1.2.3.4",
   442  		},
   443  	}}
   444  
   445  	for _, test := range tests {
   446  		t.Run(test.name, func(t *testing.T) {
   447  			req, _ := http.NewRequest("GET", "https://cluster.k8s.io/apis/foobars/v1/foo/bar", nil)
   448  			req.RemoteAddr = test.remoteAddr
   449  			if test.forwardedFor != "" {
   450  				req.Header.Set("X-Forwarded-For", test.forwardedFor)
   451  			}
   452  			if test.realIP != "" {
   453  				req.Header.Set("X-Real-Ip", test.realIP)
   454  			}
   455  
   456  			actualIPs := SourceIPs(req)
   457  			actual := make([]string, len(actualIPs))
   458  			for i, ip := range actualIPs {
   459  				actual[i] = ip.String()
   460  			}
   461  
   462  			assert.Equal(t, test.expected, actual)
   463  		})
   464  	}
   465  }
   466  
   467  func TestParseWarningHeader(t *testing.T) {
   468  	tests := []struct {
   469  		name string
   470  
   471  		header string
   472  
   473  		wantResult    WarningHeader
   474  		wantRemainder string
   475  		wantErr       string
   476  	}{
   477  		// invalid cases
   478  		{
   479  			name:    "empty",
   480  			header:  ``,
   481  			wantErr: "fewer than 3 segments",
   482  		},
   483  		{
   484  			name:    "bad code",
   485  			header:  `A B`,
   486  			wantErr: "fewer than 3 segments",
   487  		},
   488  		{
   489  			name:    "short code",
   490  			header:  `1 - "text"`,
   491  			wantErr: "not 3 digits",
   492  		},
   493  		{
   494  			name:    "bad code",
   495  			header:  `A - "text"`,
   496  			wantErr: "not 3 digits",
   497  		},
   498  		{
   499  			name:    "invalid date quoting",
   500  			header:  `  299 - "text\"\\\a\b\c"  "Tue, 15 Nov 1994 08:12:31 GMT `,
   501  			wantErr: "unterminated date segment",
   502  		},
   503  		{
   504  			name:    "invalid post-date",
   505  			header:  `  299 - "text\"\\\a\b\c"  "Tue, 15 Nov 1994 08:12:31 GMT" other`,
   506  			wantErr: "unexpected token after warn-date",
   507  		},
   508  		{
   509  			name:    "agent control character",
   510  			header:  "  299 agent\u0000name \"text\"",
   511  			wantErr: "invalid agent",
   512  		},
   513  		{
   514  			name:    "agent non-utf8 character",
   515  			header:  "  299 agent\xc5name \"text\"",
   516  			wantErr: "invalid agent",
   517  		},
   518  		{
   519  			name:    "text control character",
   520  			header:  "  299 - \"text\u0000\"content",
   521  			wantErr: "invalid text",
   522  		},
   523  		{
   524  			name:    "text non-utf8 character",
   525  			header:  "  299 - \"text\xc5\"content",
   526  			wantErr: "invalid text",
   527  		},
   528  
   529  		// valid cases
   530  		{
   531  			name:       "ok",
   532  			header:     `299 - "text"`,
   533  			wantResult: WarningHeader{Code: 299, Agent: `-`, Text: `text`},
   534  		},
   535  		{
   536  			name:       "ok",
   537  			header:     `299 - "text\"\\\a\b\c"`,
   538  			wantResult: WarningHeader{Code: 299, Agent: `-`, Text: `text"\abc`},
   539  		},
   540  		// big code
   541  		{
   542  			name:       "big code",
   543  			header:     `321 - "text"`,
   544  			wantResult: WarningHeader{Code: 321, Agent: "-", Text: "text"},
   545  		},
   546  		// RFC 2047 decoding
   547  		{
   548  			name:       "ok, rfc 2047, iso-8859-1, q",
   549  			header:     `299 - "=?iso-8859-1?q?this=20is=20some=20text?="`,
   550  			wantResult: WarningHeader{Code: 299, Agent: `-`, Text: `this is some text`},
   551  		},
   552  		{
   553  			name:       "ok, rfc 2047, utf-8, b",
   554  			header:     `299 - "=?UTF-8?B?VGhpcyBpcyBhIGhvcnNleTog8J+Qjg==?= And =?UTF-8?B?VGhpcyBpcyBhIGhvcnNleTog8J+Qjg==?="`,
   555  			wantResult: WarningHeader{Code: 299, Agent: `-`, Text: `This is a horsey: 🐎 And This is a horsey: 🐎`},
   556  		},
   557  		{
   558  			name:       "ok, rfc 2047, utf-8, q",
   559  			header:     `299 - "=?UTF-8?Q?This is a \"horsey\": =F0=9F=90=8E?="`,
   560  			wantResult: WarningHeader{Code: 299, Agent: `-`, Text: `This is a "horsey": 🐎`},
   561  		},
   562  		{
   563  			name:       "ok, rfc 2047, unknown charset",
   564  			header:     `299 - "=?UTF-9?Q?This is a horsey: =F0=9F=90=8E?="`,
   565  			wantResult: WarningHeader{Code: 299, Agent: "-", Text: `=?UTF-9?Q?This is a horsey: =F0=9F=90=8E?=`},
   566  		},
   567  		{
   568  			name:       "ok with spaces",
   569  			header:     `  299 - "text\"\\\a\b\c"  `,
   570  			wantResult: WarningHeader{Code: 299, Agent: `-`, Text: `text"\abc`},
   571  		},
   572  		{
   573  			name:       "ok with date",
   574  			header:     `  299 - "text\"\\\a\b\c"  "Tue, 15 Nov 1994 08:12:31 GMT" `,
   575  			wantResult: WarningHeader{Code: 299, Agent: `-`, Text: `text"\abc`},
   576  		},
   577  		{
   578  			name:       "ok with date and comma",
   579  			header:     `  299 - "text\"\\\a\b\c"  "Tue, 15 Nov 1994 08:12:31 GMT" , `,
   580  			wantResult: WarningHeader{Code: 299, Agent: `-`, Text: `text"\abc`},
   581  		},
   582  		{
   583  			name:       "ok with comma",
   584  			header:     `  299 - "text\"\\\a\b\c"  , `,
   585  			wantResult: WarningHeader{Code: 299, Agent: `-`, Text: `text"\abc`},
   586  		},
   587  		{
   588  			name:          "ok with date and comma and remainder",
   589  			header:        `  299 - "text\"\\\a\b\c"  "Tue, 15 Nov 1994 08:12:31 GMT" , remainder `,
   590  			wantResult:    WarningHeader{Code: 299, Agent: `-`, Text: `text"\abc`},
   591  			wantRemainder: "remainder",
   592  		},
   593  		{
   594  			name:          "ok with comma and remainder",
   595  			header:        `  299 - "text\"\\\a\b\c"  ,remainder text,second remainder`,
   596  			wantResult:    WarningHeader{Code: 299, Agent: `-`, Text: `text"\abc`},
   597  			wantRemainder: "remainder text,second remainder",
   598  		},
   599  		{
   600  			name:       "ok with utf-8 content directly in warn-text",
   601  			header:     ` 299 - "Test of Iñtërnâtiônàlizætiøn,💝🐹🌇⛔" `,
   602  			wantResult: WarningHeader{Code: 299, Agent: `-`, Text: `Test of Iñtërnâtiônàlizætiøn,💝🐹🌇⛔`},
   603  		},
   604  	}
   605  	for _, tt := range tests {
   606  		t.Run(tt.name, func(t *testing.T) {
   607  			gotResult, gotRemainder, err := ParseWarningHeader(tt.header)
   608  			switch {
   609  			case err == nil && len(tt.wantErr) > 0:
   610  				t.Errorf("ParseWarningHeader() no error, expected error %q", tt.wantErr)
   611  				return
   612  			case err != nil && len(tt.wantErr) == 0:
   613  				t.Errorf("ParseWarningHeader() error %q, expected no error", err)
   614  				return
   615  			case err != nil && len(tt.wantErr) > 0 && !strings.Contains(err.Error(), tt.wantErr):
   616  				t.Errorf("ParseWarningHeader() error %q, expected error %q", err, tt.wantErr)
   617  				return
   618  			}
   619  			if err != nil {
   620  				return
   621  			}
   622  			if !reflect.DeepEqual(gotResult, tt.wantResult) {
   623  				t.Errorf("ParseWarningHeader() gotResult = %#v, want %#v", gotResult, tt.wantResult)
   624  			}
   625  			if gotRemainder != tt.wantRemainder {
   626  				t.Errorf("ParseWarningHeader() gotRemainder = %v, want %v", gotRemainder, tt.wantRemainder)
   627  			}
   628  		})
   629  	}
   630  }
   631  
   632  func TestNewWarningHeader(t *testing.T) {
   633  	tests := []struct {
   634  		name string
   635  
   636  		code  int
   637  		agent string
   638  		text  string
   639  
   640  		want    string
   641  		wantErr string
   642  	}{
   643  		// invalid cases
   644  		{
   645  			name:    "code too low",
   646  			code:    -1,
   647  			agent:   `-`,
   648  			text:    `example warning`,
   649  			wantErr: "between 0 and 999",
   650  		},
   651  		{
   652  			name:    "code too high",
   653  			code:    1000,
   654  			agent:   `-`,
   655  			text:    `example warning`,
   656  			wantErr: "between 0 and 999",
   657  		},
   658  		{
   659  			name:    "agent with space",
   660  			code:    299,
   661  			agent:   `test agent`,
   662  			text:    `example warning`,
   663  			wantErr: `agent must be valid`,
   664  		},
   665  		{
   666  			name:    "agent with newline",
   667  			code:    299,
   668  			agent:   "test\nagent",
   669  			text:    `example warning`,
   670  			wantErr: `agent must be valid`,
   671  		},
   672  		{
   673  			name:    "agent with backslash",
   674  			code:    299,
   675  			agent:   `test\agent`,
   676  			text:    `example warning`,
   677  			wantErr: `agent must be valid`,
   678  		},
   679  		{
   680  			name:    "agent with quote",
   681  			code:    299,
   682  			agent:   `test"agent"`,
   683  			text:    `example warning`,
   684  			wantErr: `agent must be valid`,
   685  		},
   686  		{
   687  			name:    "agent with control character",
   688  			code:    299,
   689  			agent:   "test\u0000agent",
   690  			text:    `example warning`,
   691  			wantErr: `agent must be valid`,
   692  		},
   693  		{
   694  			name:    "agent with non-UTF8",
   695  			code:    299,
   696  			agent:   "test\xc5agent",
   697  			text:    `example warning`,
   698  			wantErr: `agent must be valid`,
   699  		},
   700  		{
   701  			name:    "text with newline",
   702  			code:    299,
   703  			agent:   `-`,
   704  			text:    "Test of new\nline",
   705  			wantErr: "text must be valid",
   706  		},
   707  		{
   708  			name:    "text with control character",
   709  			code:    299,
   710  			agent:   `-`,
   711  			text:    "Test of control\u0000character",
   712  			wantErr: "text must be valid",
   713  		},
   714  		{
   715  			name:    "text with non-UTF8",
   716  			code:    299,
   717  			agent:   `-`,
   718  			text:    "Test of control\xc5character",
   719  			wantErr: "text must be valid",
   720  		},
   721  
   722  		{
   723  			name:  "valid empty text",
   724  			code:  299,
   725  			agent: `-`,
   726  			text:  ``,
   727  			want:  `299 - ""`,
   728  		},
   729  		{
   730  			name:  "valid empty agent",
   731  			code:  299,
   732  			agent: ``,
   733  			text:  `example warning`,
   734  			want:  `299 - "example warning"`,
   735  		},
   736  		{
   737  			name:  "valid low code",
   738  			code:  1,
   739  			agent: `-`,
   740  			text:  `example warning`,
   741  			want:  `001 - "example warning"`,
   742  		},
   743  		{
   744  			name:  "valid high code",
   745  			code:  999,
   746  			agent: `-`,
   747  			text:  `example warning`,
   748  			want:  `999 - "example warning"`,
   749  		},
   750  		{
   751  			name:  "valid utf-8",
   752  			code:  299,
   753  			agent: `-`,
   754  			text:  `Test of "Iñtërnâtiônàlizætiøn,💝🐹🌇⛔"`,
   755  			want:  `299 - "Test of \"Iñtërnâtiônàlizætiøn,💝🐹🌇⛔\""`,
   756  		},
   757  	}
   758  
   759  	for _, tt := range tests {
   760  		t.Run(tt.name, func(t *testing.T) {
   761  			got, err := NewWarningHeader(tt.code, tt.agent, tt.text)
   762  
   763  			switch {
   764  			case err == nil && len(tt.wantErr) > 0:
   765  				t.Fatalf("ParseWarningHeader() no error, expected error %q", tt.wantErr)
   766  			case err != nil && len(tt.wantErr) == 0:
   767  				t.Fatalf("ParseWarningHeader() error %q, expected no error", err)
   768  			case err != nil && len(tt.wantErr) > 0 && !strings.Contains(err.Error(), tt.wantErr):
   769  				t.Fatalf("ParseWarningHeader() error %q, expected error %q", err, tt.wantErr)
   770  			}
   771  			if err != nil {
   772  				return
   773  			}
   774  
   775  			if got != tt.want {
   776  				t.Fatalf("NewWarningHeader() = %v, want %v", got, tt.want)
   777  			}
   778  
   779  			roundTrip, remaining, err := ParseWarningHeader(got)
   780  			if err != nil {
   781  				t.Fatalf("error roundtripping: %v", err)
   782  			}
   783  			if len(remaining) > 0 {
   784  				t.Fatalf("unexpected remainder roundtripping: %s", remaining)
   785  			}
   786  			agent := tt.agent
   787  			if len(agent) == 0 {
   788  				agent = "-"
   789  			}
   790  			expect := WarningHeader{Code: tt.code, Agent: agent, Text: tt.text}
   791  			if roundTrip != expect {
   792  				t.Fatalf("after round trip, want:\n%#v\ngot\n%#v", expect, roundTrip)
   793  			}
   794  		})
   795  	}
   796  }
   797  
   798  func TestParseWarningHeaders(t *testing.T) {
   799  	tests := []struct {
   800  		name string
   801  
   802  		headers []string
   803  
   804  		want     []WarningHeader
   805  		wantErrs []string
   806  	}{
   807  		{
   808  			name:     "empty",
   809  			headers:  []string{},
   810  			want:     nil,
   811  			wantErrs: []string{},
   812  		},
   813  		{
   814  			name: "multi-header with error",
   815  			headers: []string{
   816  				`299 - "warning 1.1",299 - "warning 1.2"`,
   817  				`299 - "warning 2", 299 - "warning unquoted`,
   818  				` 299 - "warning 3.1" ,  299 - "warning 3.2" `,
   819  			},
   820  			want: []WarningHeader{
   821  				{Code: 299, Agent: "-", Text: "warning 1.1"},
   822  				{Code: 299, Agent: "-", Text: "warning 1.2"},
   823  				{Code: 299, Agent: "-", Text: "warning 2"},
   824  				{Code: 299, Agent: "-", Text: "warning 3.1"},
   825  				{Code: 299, Agent: "-", Text: "warning 3.2"},
   826  			},
   827  			wantErrs: []string{"invalid warning header: invalid quoted string: missing closing quote"},
   828  		},
   829  	}
   830  	for _, tt := range tests {
   831  		t.Run(tt.name, func(t *testing.T) {
   832  			got, gotErrs := ParseWarningHeaders(tt.headers)
   833  
   834  			switch {
   835  			case len(gotErrs) != len(tt.wantErrs):
   836  				t.Fatalf("ParseWarningHeader() got %v, expected %v", gotErrs, tt.wantErrs)
   837  			case len(gotErrs) == len(tt.wantErrs) && len(gotErrs) > 0:
   838  				gotErrStrings := []string{}
   839  				for _, err := range gotErrs {
   840  					gotErrStrings = append(gotErrStrings, err.Error())
   841  				}
   842  				if !reflect.DeepEqual(gotErrStrings, tt.wantErrs) {
   843  					t.Fatalf("ParseWarningHeader() got %v, expected %v", gotErrs, tt.wantErrs)
   844  				}
   845  			}
   846  			if len(gotErrs) > 0 {
   847  				return
   848  			}
   849  
   850  			if !reflect.DeepEqual(got, tt.want) {
   851  				t.Errorf("ParseWarningHeaders() got %#v, want %#v", got, tt.want)
   852  			}
   853  		})
   854  	}
   855  }
   856  
   857  func TestIsProbableEOF(t *testing.T) {
   858  	tests := []struct {
   859  		name     string
   860  		err      error
   861  		expected bool
   862  	}{
   863  		{
   864  			name:     "with no error",
   865  			expected: false,
   866  		},
   867  		{
   868  			name:     "with EOF error",
   869  			err:      io.EOF,
   870  			expected: true,
   871  		},
   872  		{
   873  			name:     "with unexpected EOF error",
   874  			err:      io.ErrUnexpectedEOF,
   875  			expected: true,
   876  		},
   877  		{
   878  			name:     "with broken connection error",
   879  			err:      fmt.Errorf("http: can't write HTTP request on broken connection"),
   880  			expected: true,
   881  		},
   882  		{
   883  			name:     "with server sent GOAWAY error",
   884  			err:      fmt.Errorf("error foo - http2: server sent GOAWAY and closed the connection - error bar"),
   885  			expected: true,
   886  		},
   887  		{
   888  			name:     "with connection reset by peer error",
   889  			err:      fmt.Errorf("error foo - connection reset by peer - error bar"),
   890  			expected: true,
   891  		},
   892  		{
   893  			name:     "with use of closed network connection error",
   894  			err:      fmt.Errorf("error foo - Use of closed network connection - error bar"),
   895  			expected: true,
   896  		},
   897  		{
   898  			name: "with url error",
   899  			err: &url.Error{
   900  				Err: io.ErrUnexpectedEOF,
   901  			},
   902  			expected: true,
   903  		},
   904  		{
   905  			name:     "with unrecognized error",
   906  			err:      fmt.Errorf("error foo"),
   907  			expected: false,
   908  		},
   909  	}
   910  
   911  	for _, test := range tests {
   912  		t.Run(test.name, func(t *testing.T) {
   913  			actual := IsProbableEOF(test.err)
   914  			assert.Equal(t, test.expected, actual)
   915  		})
   916  	}
   917  }
   918  
   919  func TestReadIdleTimeoutSeconds(t *testing.T) {
   920  	t.Setenv("HTTP2_READ_IDLE_TIMEOUT_SECONDS", "60")
   921  	if e, a := 60, readIdleTimeoutSeconds(); e != a {
   922  		t.Errorf("expected %d, got %d", e, a)
   923  	}
   924  
   925  	t.Setenv("HTTP2_READ_IDLE_TIMEOUT_SECONDS", "illegal value")
   926  	if e, a := 30, readIdleTimeoutSeconds(); e != a {
   927  		t.Errorf("expected %d, got %d", e, a)
   928  	}
   929  }
   930  
   931  func TestPingTimeoutSeconds(t *testing.T) {
   932  	t.Setenv("HTTP2_PING_TIMEOUT_SECONDS", "60")
   933  	if e, a := 60, pingTimeoutSeconds(); e != a {
   934  		t.Errorf("expected %d, got %d", e, a)
   935  	}
   936  
   937  	t.Setenv("HTTP2_PING_TIMEOUT_SECONDS", "illegal value")
   938  	if e, a := 15, pingTimeoutSeconds(); e != a {
   939  		t.Errorf("expected %d, got %d", e, a)
   940  	}
   941  }
   942  
   943  func Benchmark_ParseQuotedString(b *testing.B) {
   944  	str := `"The quick brown" fox jumps over the lazy dog`
   945  	b.ReportAllocs()
   946  	b.ResetTimer()
   947  	for i := 0; i < b.N; i++ {
   948  		quoted, remainder, err := parseQuotedString(str)
   949  		if err != nil {
   950  			b.Errorf("Unexpected error %s", err)
   951  		}
   952  		if quoted != "The quick brown" {
   953  			b.Errorf("Unexpected quoted string %s", quoted)
   954  		}
   955  		if remainder != "fox jumps over the lazy dog" {
   956  			b.Errorf("Unexpected remainder string %s", quoted)
   957  		}
   958  	}
   959  }