From 068260af0715eb197e29832977bafe199b1eff03 Mon Sep 17 00:00:00 2001 From: Antonio Murdaca Date: Sun, 29 Mar 2015 15:51:08 +0200 Subject: [PATCH] Refactor utils/http.go, fixes #11899 Signed-off-by: Antonio Murdaca --- requestdecorator/README.md | 2 + requestdecorator/requestdecorator.go | 172 +++++++++++++++++ requestdecorator/requestdecorator_test.go | 222 ++++++++++++++++++++++ 3 files changed, 396 insertions(+) create mode 100644 requestdecorator/README.md create mode 100644 requestdecorator/requestdecorator.go create mode 100644 requestdecorator/requestdecorator_test.go diff --git a/requestdecorator/README.md b/requestdecorator/README.md new file mode 100644 index 0000000..76f8ca7 --- /dev/null +++ b/requestdecorator/README.md @@ -0,0 +1,2 @@ +This package provides helper functions for decorating a request with user agent +versions, auth, meta headers. diff --git a/requestdecorator/requestdecorator.go b/requestdecorator/requestdecorator.go new file mode 100644 index 0000000..c236e3f --- /dev/null +++ b/requestdecorator/requestdecorator.go @@ -0,0 +1,172 @@ +// Package requestdecorator provides helper functions to decorate a request with +// user agent versions, auth, meta headers. +package requestdecorator + +import ( + "errors" + "io" + "net/http" + "strings" + + "github.com/Sirupsen/logrus" +) + +var ( + ErrNilRequest = errors.New("request cannot be nil") +) + +// UAVersionInfo is used to model UserAgent versions. +type UAVersionInfo struct { + Name string + Version string +} + +func NewUAVersionInfo(name, version string) UAVersionInfo { + return UAVersionInfo{ + Name: name, + Version: version, + } +} + +func (vi *UAVersionInfo) isValid() bool { + const stopChars = " \t\r\n/" + name := vi.Name + vers := vi.Version + if len(name) == 0 || strings.ContainsAny(name, stopChars) { + return false + } + if len(vers) == 0 || strings.ContainsAny(vers, stopChars) { + return false + } + return true +} + +// Convert versions to a string and append the string to the string base. +// +// Each UAVersionInfo will be converted to a string in the format of +// "product/version", where the "product" is get from the name field, while +// version is get from the version field. Several pieces of verson information +// will be concatinated and separated by space. +func appendVersions(base string, versions ...UAVersionInfo) string { + if len(versions) == 0 { + return base + } + + verstrs := make([]string, 0, 1+len(versions)) + if len(base) > 0 { + verstrs = append(verstrs, base) + } + + for _, v := range versions { + if !v.isValid() { + continue + } + verstrs = append(verstrs, v.Name+"/"+v.Version) + } + return strings.Join(verstrs, " ") +} + +// Decorator is used to change an instance of +// http.Request. It could be used to add more header fields, +// change body, etc. +type Decorator interface { + // ChangeRequest() changes the request accordingly. + // The changed request will be returned or err will be non-nil + // if an error occur. + ChangeRequest(req *http.Request) (newReq *http.Request, err error) +} + +// UserAgentDecorator appends the product/version to the user agent field +// of a request. +type UserAgentDecorator struct { + Versions []UAVersionInfo +} + +func (h *UserAgentDecorator) ChangeRequest(req *http.Request) (*http.Request, error) { + if req == nil { + return req, ErrNilRequest + } + + userAgent := appendVersions(req.UserAgent(), h.Versions...) + if len(userAgent) > 0 { + req.Header.Set("User-Agent", userAgent) + } + return req, nil +} + +type MetaHeadersDecorator struct { + Headers map[string][]string +} + +func (h *MetaHeadersDecorator) ChangeRequest(req *http.Request) (*http.Request, error) { + if h.Headers == nil { + return req, ErrNilRequest + } + for k, v := range h.Headers { + req.Header[k] = v + } + return req, nil +} + +type AuthDecorator struct { + login string + password string +} + +func NewAuthDecorator(login, password string) Decorator { + return &AuthDecorator{ + login: login, + password: password, + } +} + +func (self *AuthDecorator) ChangeRequest(req *http.Request) (*http.Request, error) { + if req == nil { + return req, ErrNilRequest + } + req.SetBasicAuth(self.login, self.password) + return req, nil +} + +// RequestFactory creates an HTTP request +// and applies a list of decorators on the request. +type RequestFactory struct { + decorators []Decorator +} + +func NewRequestFactory(d ...Decorator) *RequestFactory { + return &RequestFactory{ + decorators: d, + } +} + +func (f *RequestFactory) AddDecorator(d ...Decorator) { + f.decorators = append(f.decorators, d...) +} + +func (f *RequestFactory) GetDecorators() []Decorator { + return f.decorators +} + +// NewRequest() creates a new *http.Request, +// applies all decorators in the Factory on the request, +// then applies decorators provided by d on the request. +func (h *RequestFactory) NewRequest(method, urlStr string, body io.Reader, d ...Decorator) (*http.Request, error) { + req, err := http.NewRequest(method, urlStr, body) + if err != nil { + return nil, err + } + + // By default, a nil factory should work. + if h == nil { + return req, nil + } + for _, dec := range h.decorators { + req, _ = dec.ChangeRequest(req) + } + for _, dec := range d { + req, _ = dec.ChangeRequest(req) + } + logrus.Debugf("%v -- HEADERS: %v", req.URL, req.Header) + return req, err +} diff --git a/requestdecorator/requestdecorator_test.go b/requestdecorator/requestdecorator_test.go new file mode 100644 index 0000000..5f1c256 --- /dev/null +++ b/requestdecorator/requestdecorator_test.go @@ -0,0 +1,222 @@ +package requestdecorator + +import ( + "net/http" + "strings" + "testing" +) + +func TestUAVersionInfo(t *testing.T) { + uavi := NewUAVersionInfo("foo", "bar") + if !uavi.isValid() { + t.Fatalf("UAVersionInfo should be valid") + } + uavi = NewUAVersionInfo("", "bar") + if uavi.isValid() { + t.Fatalf("Expected UAVersionInfo to be invalid") + } + uavi = NewUAVersionInfo("foo", "") + if uavi.isValid() { + t.Fatalf("Expected UAVersionInfo to be invalid") + } +} + +func TestUserAgentDecorator(t *testing.T) { + httpVersion := make([]UAVersionInfo, 2) + httpVersion = append(httpVersion, NewUAVersionInfo("testname", "testversion")) + httpVersion = append(httpVersion, NewUAVersionInfo("name", "version")) + uad := &UserAgentDecorator{ + Versions: httpVersion, + } + + req, err := http.NewRequest("GET", "/something", strings.NewReader("test")) + if err != nil { + t.Fatal(err) + } + reqDecorated, err := uad.ChangeRequest(req) + if err != nil { + t.Fatal(err) + } + + if reqDecorated.Header.Get("User-Agent") != "testname/testversion name/version" { + t.Fatalf("Request should have User-Agent 'testname/testversion name/version'") + } +} + +func TestUserAgentDecoratorErr(t *testing.T) { + httpVersion := make([]UAVersionInfo, 0) + uad := &UserAgentDecorator{ + Versions: httpVersion, + } + + var req *http.Request + _, err := uad.ChangeRequest(req) + if err == nil { + t.Fatalf("Expected to get ErrNilRequest instead no error was returned") + } +} + +func TestMetaHeadersDecorator(t *testing.T) { + var headers = map[string][]string{ + "key1": {"value1"}, + "key2": {"value2"}, + } + mhd := &MetaHeadersDecorator{ + Headers: headers, + } + + req, err := http.NewRequest("GET", "/something", strings.NewReader("test")) + if err != nil { + t.Fatal(err) + } + reqDecorated, err := mhd.ChangeRequest(req) + if err != nil { + t.Fatal(err) + } + + v, ok := reqDecorated.Header["key1"] + if !ok { + t.Fatalf("Expected to have header key1") + } + if v[0] != "value1" { + t.Fatalf("Expected value for key1 isn't value1") + } + + v, ok = reqDecorated.Header["key2"] + if !ok { + t.Fatalf("Expected to have header key2") + } + if v[0] != "value2" { + t.Fatalf("Expected value for key2 isn't value2") + } +} + +func TestMetaHeadersDecoratorErr(t *testing.T) { + mhd := &MetaHeadersDecorator{} + + var req *http.Request + _, err := mhd.ChangeRequest(req) + if err == nil { + t.Fatalf("Expected to get ErrNilRequest instead no error was returned") + } +} + +func TestAuthDecorator(t *testing.T) { + ad := NewAuthDecorator("test", "password") + + req, err := http.NewRequest("GET", "/something", strings.NewReader("test")) + if err != nil { + t.Fatal(err) + } + reqDecorated, err := ad.ChangeRequest(req) + if err != nil { + t.Fatal(err) + } + + username, password, ok := reqDecorated.BasicAuth() + if !ok { + t.Fatalf("Cannot retrieve basic auth info from request") + } + if username != "test" { + t.Fatalf("Expected username to be test, got %s", username) + } + if password != "password" { + t.Fatalf("Expected password to be password, got %s", password) + } +} + +func TestAuthDecoratorErr(t *testing.T) { + ad := &AuthDecorator{} + + var req *http.Request + _, err := ad.ChangeRequest(req) + if err == nil { + t.Fatalf("Expected to get ErrNilRequest instead no error was returned") + } +} + +func TestRequestFactory(t *testing.T) { + ad := NewAuthDecorator("test", "password") + httpVersion := make([]UAVersionInfo, 2) + httpVersion = append(httpVersion, NewUAVersionInfo("testname", "testversion")) + httpVersion = append(httpVersion, NewUAVersionInfo("name", "version")) + uad := &UserAgentDecorator{ + Versions: httpVersion, + } + + requestFactory := NewRequestFactory(ad, uad) + + if dlen := requestFactory.GetDecorators(); len(dlen) != 2 { + t.Fatalf("Expected to have two decorators, got %d", dlen) + } + + req, err := requestFactory.NewRequest("GET", "/test", strings.NewReader("test")) + if err != nil { + t.Fatal(err) + } + + username, password, ok := req.BasicAuth() + if !ok { + t.Fatalf("Cannot retrieve basic auth info from request") + } + if username != "test" { + t.Fatalf("Expected username to be test, got %s", username) + } + if password != "password" { + t.Fatalf("Expected password to be password, got %s", password) + } + if req.Header.Get("User-Agent") != "testname/testversion name/version" { + t.Fatalf("Request should have User-Agent 'testname/testversion name/version'") + } +} + +func TestRequestFactoryNewRequestWithDecorators(t *testing.T) { + ad := NewAuthDecorator("test", "password") + + requestFactory := NewRequestFactory(ad) + + if dlen := requestFactory.GetDecorators(); len(dlen) != 1 { + t.Fatalf("Expected to have one decorators, got %d", dlen) + } + + ad2 := NewAuthDecorator("test2", "password2") + + req, err := requestFactory.NewRequest("GET", "/test", strings.NewReader("test"), ad2) + if err != nil { + t.Fatal(err) + } + + username, password, ok := req.BasicAuth() + if !ok { + t.Fatalf("Cannot retrieve basic auth info from request") + } + if username != "test2" { + t.Fatalf("Expected username to be test, got %s", username) + } + if password != "password2" { + t.Fatalf("Expected password to be password, got %s", password) + } +} + +func TestRequestFactoryAddDecorator(t *testing.T) { + requestFactory := NewRequestFactory() + + if dlen := requestFactory.GetDecorators(); len(dlen) != 0 { + t.Fatalf("Expected to have zero decorators, got %d", dlen) + } + + ad := NewAuthDecorator("test", "password") + requestFactory.AddDecorator(ad) + + if dlen := requestFactory.GetDecorators(); len(dlen) != 1 { + t.Fatalf("Expected to have one decorators, got %d", dlen) + } +} + +func TestRequestFactoryNil(t *testing.T) { + var requestFactory RequestFactory + _, err := requestFactory.NewRequest("GET", "/test", strings.NewReader("test")) + if err != nil { + t.Fatalf("Expected not to get and error, got %s", err) + } +}