go.etcd.io/etcd@v3.3.27+incompatible/proxy/httpproxy/reverse_test.go (about)

     1  // Copyright 2015 The etcd Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package httpproxy
    16  
    17  import (
    18  	"bytes"
    19  	"errors"
    20  	"io/ioutil"
    21  	"net/http"
    22  	"net/http/httptest"
    23  	"net/url"
    24  	"reflect"
    25  	"testing"
    26  )
    27  
    28  type staticRoundTripper struct {
    29  	res *http.Response
    30  	err error
    31  }
    32  
    33  func (srt *staticRoundTripper) RoundTrip(*http.Request) (*http.Response, error) {
    34  	return srt.res, srt.err
    35  }
    36  
    37  func TestReverseProxyServe(t *testing.T) {
    38  	u := url.URL{Scheme: "http", Host: "192.0.2.3:4040"}
    39  
    40  	tests := []struct {
    41  		eps  []*endpoint
    42  		rt   http.RoundTripper
    43  		want int
    44  	}{
    45  		// no endpoints available so no requests are even made
    46  		{
    47  			eps: []*endpoint{},
    48  			rt: &staticRoundTripper{
    49  				res: &http.Response{
    50  					StatusCode: http.StatusCreated,
    51  					Body:       ioutil.NopCloser(&bytes.Reader{}),
    52  				},
    53  			},
    54  			want: http.StatusServiceUnavailable,
    55  		},
    56  
    57  		// error is returned from one endpoint that should be available
    58  		{
    59  			eps:  []*endpoint{{URL: u, Available: true}},
    60  			rt:   &staticRoundTripper{err: errors.New("what a bad trip")},
    61  			want: http.StatusBadGateway,
    62  		},
    63  
    64  		// endpoint is available and returns success
    65  		{
    66  			eps: []*endpoint{{URL: u, Available: true}},
    67  			rt: &staticRoundTripper{
    68  				res: &http.Response{
    69  					StatusCode: http.StatusCreated,
    70  					Body:       ioutil.NopCloser(&bytes.Reader{}),
    71  					Header:     map[string][]string{"Content-Type": {"application/json"}},
    72  				},
    73  			},
    74  			want: http.StatusCreated,
    75  		},
    76  	}
    77  
    78  	for i, tt := range tests {
    79  		rp := reverseProxy{
    80  			director:  &director{ep: tt.eps},
    81  			transport: tt.rt,
    82  		}
    83  
    84  		req, _ := http.NewRequest("GET", "http://192.0.2.2:2379", nil)
    85  		rr := httptest.NewRecorder()
    86  		rp.ServeHTTP(rr, req)
    87  
    88  		if rr.Code != tt.want {
    89  			t.Errorf("#%d: unexpected HTTP status code: want = %d, got = %d", i, tt.want, rr.Code)
    90  		}
    91  		if gct := rr.Header().Get("Content-Type"); gct != "application/json" {
    92  			t.Errorf("#%d: Content-Type = %s, want %s", i, gct, "application/json")
    93  		}
    94  	}
    95  }
    96  
    97  func TestRedirectRequest(t *testing.T) {
    98  	loc := url.URL{
    99  		Scheme: "http",
   100  		Host:   "bar.example.com",
   101  	}
   102  
   103  	req := &http.Request{
   104  		Method: "GET",
   105  		Host:   "foo.example.com",
   106  		URL: &url.URL{
   107  			Host: "foo.example.com",
   108  			Path: "/v2/keys/baz",
   109  		},
   110  	}
   111  
   112  	redirectRequest(req, loc)
   113  
   114  	want := &http.Request{
   115  		Method: "GET",
   116  		// this field must not change
   117  		Host: "foo.example.com",
   118  		URL: &url.URL{
   119  			// the Scheme field is updated to that of the provided URL
   120  			Scheme: "http",
   121  			// the Host field is updated to that of the provided URL
   122  			Host: "bar.example.com",
   123  			Path: "/v2/keys/baz",
   124  		},
   125  	}
   126  
   127  	if !reflect.DeepEqual(want, req) {
   128  		t.Fatalf("HTTP request does not match expected criteria: want=%#v got=%#v", want, req)
   129  	}
   130  }
   131  
   132  func TestMaybeSetForwardedFor(t *testing.T) {
   133  	tests := []struct {
   134  		raddr  string
   135  		fwdFor string
   136  		want   string
   137  	}{
   138  		{"192.0.2.3:8002", "", "192.0.2.3"},
   139  		{"192.0.2.3:8002", "192.0.2.2", "192.0.2.2, 192.0.2.3"},
   140  		{"192.0.2.3:8002", "192.0.2.1, 192.0.2.2", "192.0.2.1, 192.0.2.2, 192.0.2.3"},
   141  		{"example.com:8002", "", "example.com"},
   142  
   143  		// While these cases look valid, golang net/http will not let it happen
   144  		// The RemoteAddr field will always be a valid host:port
   145  		{":8002", "", ""},
   146  		{"192.0.2.3", "", ""},
   147  
   148  		// blatantly invalid host w/o a port
   149  		{"12", "", ""},
   150  		{"12", "192.0.2.3", "192.0.2.3"},
   151  	}
   152  
   153  	for i, tt := range tests {
   154  		req := &http.Request{
   155  			RemoteAddr: tt.raddr,
   156  			Header:     make(http.Header),
   157  		}
   158  
   159  		if tt.fwdFor != "" {
   160  			req.Header.Set("X-Forwarded-For", tt.fwdFor)
   161  		}
   162  
   163  		maybeSetForwardedFor(req)
   164  		got := req.Header.Get("X-Forwarded-For")
   165  		if tt.want != got {
   166  			t.Errorf("#%d: incorrect header: want = %q, got = %q", i, tt.want, got)
   167  		}
   168  	}
   169  }
   170  
   171  func TestRemoveSingleHopHeaders(t *testing.T) {
   172  	hdr := http.Header(map[string][]string{
   173  		// single-hop headers that should be removed
   174  		"Connection":          {"close"},
   175  		"Keep-Alive":          {"foo"},
   176  		"Proxy-Authenticate":  {"Basic realm=example.com"},
   177  		"Proxy-Authorization": {"foo"},
   178  		"Te":                  {"deflate,gzip"},
   179  		"Trailers":            {"ETag"},
   180  		"Transfer-Encoding":   {"chunked"},
   181  		"Upgrade":             {"WebSocket"},
   182  
   183  		// headers that should persist
   184  		"Accept": {"application/json"},
   185  		"X-Foo":  {"Bar"},
   186  	})
   187  
   188  	removeSingleHopHeaders(&hdr)
   189  
   190  	want := http.Header(map[string][]string{
   191  		"Accept": {"application/json"},
   192  		"X-Foo":  {"Bar"},
   193  	})
   194  
   195  	if !reflect.DeepEqual(want, hdr) {
   196  		t.Fatalf("unexpected result: want = %#v, got = %#v", want, hdr)
   197  	}
   198  }
   199  
   200  func TestCopyHeader(t *testing.T) {
   201  	tests := []struct {
   202  		src  http.Header
   203  		dst  http.Header
   204  		want http.Header
   205  	}{
   206  		{
   207  			src: http.Header(map[string][]string{
   208  				"Foo": {"bar", "baz"},
   209  			}),
   210  			dst: http.Header(map[string][]string{}),
   211  			want: http.Header(map[string][]string{
   212  				"Foo": {"bar", "baz"},
   213  			}),
   214  		},
   215  		{
   216  			src: http.Header(map[string][]string{
   217  				"Foo":  {"bar"},
   218  				"Ping": {"pong"},
   219  			}),
   220  			dst: http.Header(map[string][]string{}),
   221  			want: http.Header(map[string][]string{
   222  				"Foo":  {"bar"},
   223  				"Ping": {"pong"},
   224  			}),
   225  		},
   226  		{
   227  			src: http.Header(map[string][]string{
   228  				"Foo": {"bar", "baz"},
   229  			}),
   230  			dst: http.Header(map[string][]string{
   231  				"Foo": {"qux"},
   232  			}),
   233  			want: http.Header(map[string][]string{
   234  				"Foo": {"qux", "bar", "baz"},
   235  			}),
   236  		},
   237  	}
   238  
   239  	for i, tt := range tests {
   240  		copyHeader(tt.dst, tt.src)
   241  		if !reflect.DeepEqual(tt.dst, tt.want) {
   242  			t.Errorf("#%d: unexpected headers: want = %v, got = %v", i, tt.want, tt.dst)
   243  		}
   244  	}
   245  }