diff --git a/proxy/proxy.go b/proxy/proxy.go new file mode 100644 index 000000000..167b46ad5 --- /dev/null +++ b/proxy/proxy.go @@ -0,0 +1,64 @@ +package proxy + +import ( + "errors" + "fmt" + "net/http" + "net/http/httputil" + "net/url" +) + +func NewHandler(endpoints []string) (*httputil.ReverseProxy, error) { + d, err := newDirector(endpoints) + if err != nil { + return nil, err + } + + proxy := httputil.ReverseProxy{ + Director: d.direct, + Transport: &http.Transport{}, + FlushInterval: 0, + } + + return &proxy, nil +} + +func newDirector(endpoints []string) (*director, error) { + if len(endpoints) == 0 { + return nil, errors.New("one or more endpoints required") + } + + urls := make([]url.URL, len(endpoints)) + for i, e := range endpoints { + u, err := url.Parse(e) + if err != nil { + return nil, fmt.Errorf("invalid endpoint %q: %v", e, err) + } + + if u.Scheme == "" { + return nil, fmt.Errorf("invalid endpoint %q: scheme required", e) + } + + if u.Host == "" { + return nil, fmt.Errorf("invalid endpoint %q: host empty", e) + } + + urls[i] = *u + } + + d := director{ + endpoints: urls, + } + + return &d, nil +} + +type director struct { + endpoints []url.URL +} + +func (d *director) direct(req *http.Request) { + choice := d.endpoints[0] + req.URL.Scheme = choice.Scheme + req.URL.Host = choice.Host +} diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go new file mode 100644 index 000000000..707660cc2 --- /dev/null +++ b/proxy/proxy_test.go @@ -0,0 +1,71 @@ +package proxy + +import ( + "net/http" + "net/url" + "reflect" + "testing" +) + +func TestNewDirector(t *testing.T) { + tests := []struct { + good bool + endpoints []string + }{ + {true, []string{"http://192.0.2.8"}}, + {true, []string{"http://192.0.2.8:8001"}}, + {true, []string{"http://example.com"}}, + {true, []string{"http://example.com:8001"}}, + {true, []string{"http://192.0.2.8:8001", "http://example.com:8002"}}, + + {false, []string{"192.0.2.8"}}, + {false, []string{"192.0.2.8:8001"}}, + {false, []string{""}}, + } + + for i, tt := range tests { + _, err := newDirector(tt.endpoints) + if tt.good != (err == nil) { + t.Errorf("#%d: expected success = %t, got err = %v", i, tt.good, err) + } + } +} + +func TestDirectorDirect(t *testing.T) { + d := &director{ + endpoints: []url.URL{ + url.URL{ + Scheme: "http", + Host: "bar.example.com", + }, + }, + } + + req := &http.Request{ + Method: "GET", + Host: "foo.example.com", + URL: &url.URL{ + Host: "foo.example.com", + Path: "/v2/keys/baz", + }, + } + + d.direct(req) + + want := &http.Request{ + Method: "GET", + // this field must not change + Host: "foo.example.com", + URL: &url.URL{ + // the Scheme field is updated per the director's first endpoint + Scheme: "http", + // the Host field is updated per the director's first endpoint + Host: "bar.example.com", + Path: "/v2/keys/baz", + }, + } + + if !reflect.DeepEqual(want, req) { + t.Fatalf("HTTP request does not match expected criteria: want=%#v got=%#v", want, req) + } +} diff --git a/test b/test index f3d422bd2..d41c7f69b 100755 --- a/test +++ b/test @@ -14,7 +14,7 @@ COVER=${COVER:-"-cover"} source ./build -TESTABLE="wal snap etcdserver etcdserver/etcdhttp etcdserver/etcdserverpb functional raft store" +TESTABLE="wal snap etcdserver etcdserver/etcdhttp etcdserver/etcdserverpb functional proxy raft store" FORMATTABLE="$TESTABLE cors.go main.go" # user has not provided PKG override