diff --git a/pkg/requestdecorator/README.md b/pkg/requestdecorator/README.md new file mode 100644 index 0000000000..76f8ca798f --- /dev/null +++ b/pkg/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/pkg/requestdecorator/requestdecorator.go b/pkg/requestdecorator/requestdecorator.go new file mode 100644 index 0000000000..c236e3fe3f --- /dev/null +++ b/pkg/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/pkg/requestdecorator/requestdecorator_test.go b/pkg/requestdecorator/requestdecorator_test.go new file mode 100644 index 0000000000..5f1c2565a0 --- /dev/null +++ b/pkg/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) + } +} diff --git a/registry/auth.go b/registry/auth.go index eaecc0f267..2c37f7f640 100644 --- a/registry/auth.go +++ b/registry/auth.go @@ -14,7 +14,7 @@ import ( "time" "github.com/Sirupsen/logrus" - "github.com/docker/docker/utils" + "github.com/docker/docker/pkg/requestdecorator" ) const ( @@ -225,7 +225,7 @@ func SaveConfig(configFile *ConfigFile) error { } // Login tries to register/login to the registry server. -func Login(authConfig *AuthConfig, registryEndpoint *Endpoint, factory *utils.HTTPRequestFactory) (string, error) { +func Login(authConfig *AuthConfig, registryEndpoint *Endpoint, factory *requestdecorator.RequestFactory) (string, error) { // Separates the v2 registry login logic from the v1 logic. if registryEndpoint.Version == APIVersion2 { return loginV2(authConfig, registryEndpoint, factory) @@ -235,7 +235,7 @@ func Login(authConfig *AuthConfig, registryEndpoint *Endpoint, factory *utils.HT } // loginV1 tries to register/login to the v1 registry server. -func loginV1(authConfig *AuthConfig, registryEndpoint *Endpoint, factory *utils.HTTPRequestFactory) (string, error) { +func loginV1(authConfig *AuthConfig, registryEndpoint *Endpoint, factory *requestdecorator.RequestFactory) (string, error) { var ( status string reqBody []byte @@ -348,7 +348,7 @@ func loginV1(authConfig *AuthConfig, registryEndpoint *Endpoint, factory *utils. // now, users should create their account through other means like directly from a web page // served by the v2 registry service provider. Whether this will be supported in the future // is to be determined. -func loginV2(authConfig *AuthConfig, registryEndpoint *Endpoint, factory *utils.HTTPRequestFactory) (string, error) { +func loginV2(authConfig *AuthConfig, registryEndpoint *Endpoint, factory *requestdecorator.RequestFactory) (string, error) { logrus.Debugf("attempting v2 login to registry endpoint %s", registryEndpoint) var ( err error @@ -381,7 +381,7 @@ func loginV2(authConfig *AuthConfig, registryEndpoint *Endpoint, factory *utils. return "", fmt.Errorf("no successful auth challenge for %s - errors: %s", registryEndpoint, allErrors) } -func tryV2BasicAuthLogin(authConfig *AuthConfig, params map[string]string, registryEndpoint *Endpoint, client *http.Client, factory *utils.HTTPRequestFactory) error { +func tryV2BasicAuthLogin(authConfig *AuthConfig, params map[string]string, registryEndpoint *Endpoint, client *http.Client, factory *requestdecorator.RequestFactory) error { req, err := factory.NewRequest("GET", registryEndpoint.Path(""), nil) if err != nil { return err @@ -402,7 +402,7 @@ func tryV2BasicAuthLogin(authConfig *AuthConfig, params map[string]string, regis return nil } -func tryV2TokenAuthLogin(authConfig *AuthConfig, params map[string]string, registryEndpoint *Endpoint, client *http.Client, factory *utils.HTTPRequestFactory) error { +func tryV2TokenAuthLogin(authConfig *AuthConfig, params map[string]string, registryEndpoint *Endpoint, client *http.Client, factory *requestdecorator.RequestFactory) error { token, err := getToken(authConfig.Username, authConfig.Password, params, registryEndpoint, client, factory) if err != nil { return err diff --git a/registry/endpoint.go b/registry/endpoint.go index b883d36d0c..69a718e12f 100644 --- a/registry/endpoint.go +++ b/registry/endpoint.go @@ -11,8 +11,8 @@ import ( "strings" "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/requestdecorator" "github.com/docker/docker/registry/v2" - "github.com/docker/docker/utils" ) // for mocking in unit tests @@ -162,7 +162,7 @@ func (e *Endpoint) Ping() (RegistryInfo, error) { return RegistryInfo{}, fmt.Errorf("unable to ping registry endpoint %s\nv2 ping attempt failed with error: %s\n v1 ping attempt failed with error: %s", e, errV2, errV1) } -func (e *Endpoint) pingV1(factory *utils.HTTPRequestFactory) (RegistryInfo, error) { +func (e *Endpoint) pingV1(factory *requestdecorator.RequestFactory) (RegistryInfo, error) { logrus.Debugf("attempting v1 ping for registry endpoint %s", e) if e.String() == IndexServerAddress() { @@ -216,7 +216,7 @@ func (e *Endpoint) pingV1(factory *utils.HTTPRequestFactory) (RegistryInfo, erro return info, nil } -func (e *Endpoint) pingV2(factory *utils.HTTPRequestFactory) (RegistryInfo, error) { +func (e *Endpoint) pingV2(factory *requestdecorator.RequestFactory) (RegistryInfo, error) { logrus.Debugf("attempting v2 ping for registry endpoint %s", e) req, err := factory.NewRequest("GET", e.Path(""), nil) diff --git a/registry/httpfactory.go b/registry/httpfactory.go index a4fea3822a..f1b89e5829 100644 --- a/registry/httpfactory.go +++ b/registry/httpfactory.go @@ -5,42 +5,26 @@ import ( "github.com/docker/docker/autogen/dockerversion" "github.com/docker/docker/pkg/parsers/kernel" - "github.com/docker/docker/utils" + "github.com/docker/docker/pkg/requestdecorator" ) -func HTTPRequestFactory(metaHeaders map[string][]string) *utils.HTTPRequestFactory { +func HTTPRequestFactory(metaHeaders map[string][]string) *requestdecorator.RequestFactory { // FIXME: this replicates the 'info' job. - httpVersion := make([]utils.VersionInfo, 0, 4) - httpVersion = append(httpVersion, &simpleVersionInfo{"docker", dockerversion.VERSION}) - httpVersion = append(httpVersion, &simpleVersionInfo{"go", runtime.Version()}) - httpVersion = append(httpVersion, &simpleVersionInfo{"git-commit", dockerversion.GITCOMMIT}) + httpVersion := make([]requestdecorator.UAVersionInfo, 0, 4) + httpVersion = append(httpVersion, requestdecorator.NewUAVersionInfo("docker", dockerversion.VERSION)) + httpVersion = append(httpVersion, requestdecorator.NewUAVersionInfo("go", runtime.Version())) + httpVersion = append(httpVersion, requestdecorator.NewUAVersionInfo("git-commit", dockerversion.GITCOMMIT)) if kernelVersion, err := kernel.GetKernelVersion(); err == nil { - httpVersion = append(httpVersion, &simpleVersionInfo{"kernel", kernelVersion.String()}) + httpVersion = append(httpVersion, requestdecorator.NewUAVersionInfo("kernel", kernelVersion.String())) } - httpVersion = append(httpVersion, &simpleVersionInfo{"os", runtime.GOOS}) - httpVersion = append(httpVersion, &simpleVersionInfo{"arch", runtime.GOARCH}) - ud := utils.NewHTTPUserAgentDecorator(httpVersion...) - md := &utils.HTTPMetaHeadersDecorator{ + httpVersion = append(httpVersion, requestdecorator.NewUAVersionInfo("os", runtime.GOOS)) + httpVersion = append(httpVersion, requestdecorator.NewUAVersionInfo("arch", runtime.GOARCH)) + uad := &requestdecorator.UserAgentDecorator{ + Versions: httpVersion, + } + mhd := &requestdecorator.MetaHeadersDecorator{ Headers: metaHeaders, } - factory := utils.NewHTTPRequestFactory(ud, md) + factory := requestdecorator.NewRequestFactory(uad, mhd) return factory } - -// simpleVersionInfo is a simple implementation of -// the interface VersionInfo, which is used -// to provide version information for some product, -// component, etc. It stores the product name and the version -// in string and returns them on calls to Name() and Version(). -type simpleVersionInfo struct { - name string - version string -} - -func (v *simpleVersionInfo) Name() string { - return v.name -} - -func (v *simpleVersionInfo) Version() string { - return v.version -} diff --git a/registry/registry_test.go b/registry/registry_test.go index d96630d90e..a066de9f8e 100644 --- a/registry/registry_test.go +++ b/registry/registry_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "github.com/docker/docker/utils" + "github.com/docker/docker/pkg/requestdecorator" ) var ( @@ -25,7 +25,7 @@ func spawnTestRegistrySession(t *testing.T) *Session { if err != nil { t.Fatal(err) } - r, err := NewSession(authConfig, utils.NewHTTPRequestFactory(), endpoint, true) + r, err := NewSession(authConfig, requestdecorator.NewRequestFactory(), endpoint, true) if err != nil { t.Fatal(err) } @@ -40,7 +40,7 @@ func TestPublicSession(t *testing.T) { if err != nil { t.Fatal(err) } - r, err := NewSession(authConfig, utils.NewHTTPRequestFactory(), endpoint, true) + r, err := NewSession(authConfig, requestdecorator.NewRequestFactory(), endpoint, true) if err != nil { t.Fatal(err) } diff --git a/registry/session.go b/registry/session.go index 1d70eff9a1..4682a5074c 100644 --- a/registry/session.go +++ b/registry/session.go @@ -19,19 +19,20 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/docker/pkg/httputils" + "github.com/docker/docker/pkg/requestdecorator" "github.com/docker/docker/pkg/tarsum" "github.com/docker/docker/utils" ) type Session struct { authConfig *AuthConfig - reqFactory *utils.HTTPRequestFactory + reqFactory *requestdecorator.RequestFactory indexEndpoint *Endpoint jar *cookiejar.Jar timeout TimeoutType } -func NewSession(authConfig *AuthConfig, factory *utils.HTTPRequestFactory, endpoint *Endpoint, timeout bool) (r *Session, err error) { +func NewSession(authConfig *AuthConfig, factory *requestdecorator.RequestFactory, endpoint *Endpoint, timeout bool) (r *Session, err error) { r = &Session{ authConfig: authConfig, indexEndpoint: endpoint, @@ -55,7 +56,7 @@ func NewSession(authConfig *AuthConfig, factory *utils.HTTPRequestFactory, endpo } if info.Standalone { logrus.Debugf("Endpoint %s is eligible for private registry. Enabling decorator.", r.indexEndpoint.String()) - dec := utils.NewHTTPAuthDecorator(authConfig.Username, authConfig.Password) + dec := requestdecorator.NewAuthDecorator(authConfig.Username, authConfig.Password) factory.AddDecorator(dec) } } diff --git a/registry/token.go b/registry/token.go index c79a8ca6c5..b03bd891bb 100644 --- a/registry/token.go +++ b/registry/token.go @@ -8,14 +8,14 @@ import ( "net/url" "strings" - "github.com/docker/docker/utils" + "github.com/docker/docker/pkg/requestdecorator" ) type tokenResponse struct { Token string `json:"token"` } -func getToken(username, password string, params map[string]string, registryEndpoint *Endpoint, client *http.Client, factory *utils.HTTPRequestFactory) (token string, err error) { +func getToken(username, password string, params map[string]string, registryEndpoint *Endpoint, client *http.Client, factory *requestdecorator.RequestFactory) (token string, err error) { realm, ok := params["realm"] if !ok { return "", errors.New("no realm specified for token auth challenge") diff --git a/utils/http.go b/utils/http.go deleted file mode 100644 index 01251d9ac1..0000000000 --- a/utils/http.go +++ /dev/null @@ -1,168 +0,0 @@ -package utils - -import ( - "io" - "net/http" - "strings" - - "github.com/Sirupsen/logrus" -) - -// VersionInfo is used to model entities which has a version. -// It is basically a tupple with name and version. -type VersionInfo interface { - Name() string - Version() string -} - -func validVersion(version VersionInfo) bool { - const stopChars = " \t\r\n/" - name := version.Name() - vers := version.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 VersionInfo will be converted to a string in the format of -// "product/version", where the "product" is get from the Name() method, while -// version is get from the Version() method. Several pieces of verson information -// will be concatinated and separated by space. -func appendVersions(base string, versions ...VersionInfo) 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 !validVersion(v) { - continue - } - verstrs = append(verstrs, v.Name()+"/"+v.Version()) - } - return strings.Join(verstrs, " ") -} - -// HTTPRequestDecorator is used to change an instance of -// http.Request. It could be used to add more header fields, -// change body, etc. -type HTTPRequestDecorator 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) -} - -// HTTPUserAgentDecorator appends the product/version to the user agent field -// of a request. -type HTTPUserAgentDecorator struct { - versions []VersionInfo -} - -func NewHTTPUserAgentDecorator(versions ...VersionInfo) HTTPRequestDecorator { - return &HTTPUserAgentDecorator{ - versions: versions, - } -} - -func (h *HTTPUserAgentDecorator) ChangeRequest(req *http.Request) (newReq *http.Request, err error) { - if req == nil { - return req, nil - } - - userAgent := appendVersions(req.UserAgent(), h.versions...) - if len(userAgent) > 0 { - req.Header.Set("User-Agent", userAgent) - } - return req, nil -} - -type HTTPMetaHeadersDecorator struct { - Headers map[string][]string -} - -func (h *HTTPMetaHeadersDecorator) ChangeRequest(req *http.Request) (newReq *http.Request, err error) { - if h.Headers == nil { - return req, nil - } - for k, v := range h.Headers { - req.Header[k] = v - } - return req, nil -} - -type HTTPAuthDecorator struct { - login string - password string -} - -func NewHTTPAuthDecorator(login, password string) HTTPRequestDecorator { - return &HTTPAuthDecorator{ - login: login, - password: password, - } -} - -func (self *HTTPAuthDecorator) ChangeRequest(req *http.Request) (*http.Request, error) { - req.SetBasicAuth(self.login, self.password) - return req, nil -} - -// HTTPRequestFactory creates an HTTP request -// and applies a list of decorators on the request. -type HTTPRequestFactory struct { - decorators []HTTPRequestDecorator -} - -func NewHTTPRequestFactory(d ...HTTPRequestDecorator) *HTTPRequestFactory { - return &HTTPRequestFactory{ - decorators: d, - } -} - -func (self *HTTPRequestFactory) AddDecorator(d ...HTTPRequestDecorator) { - self.decorators = append(self.decorators, d...) -} - -func (self *HTTPRequestFactory) GetDecorators() []HTTPRequestDecorator { - return self.decorators -} - -// NewRequest() creates a new *http.Request, -// applies all decorators in the HTTPRequestFactory on the request, -// then applies decorators provided by d on the request. -func (h *HTTPRequestFactory) NewRequest(method, urlStr string, body io.Reader, d ...HTTPRequestDecorator) (*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, err = dec.ChangeRequest(req) - if err != nil { - return nil, err - } - } - for _, dec := range d { - req, err = dec.ChangeRequest(req) - if err != nil { - return nil, err - } - } - logrus.Debugf("%v -- HEADERS: %v", req.URL, req.Header) - return req, err -}