Sfoglia il codice sorgente

Merge pull request #1382 from monnand/650-http-utils

650 http utils and user agent field
Victor Vieux 12 anni fa
parent
commit
feda3db1dd
6 ha cambiato i file con 172 aggiunte e 97 eliminazioni
  1. 1 1
      api.go
  2. 3 2
      auth/auth.go
  3. 3 3
      auth/auth_test.go
  4. 15 86
      registry/registry.go
  5. 16 5
      server.go
  6. 134 0
      utils/http.go

+ 1 - 1
api.go

@@ -87,7 +87,7 @@ func postAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Reque
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	status, err := auth.Login(authConfig)
+	status, err := auth.Login(authConfig, srv.HTTPRequestFactory())
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}

+ 3 - 2
auth/auth.go

@@ -5,6 +5,7 @@ import (
 	"encoding/json"
 	"encoding/json"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
+	"github.com/dotcloud/docker/utils"
 	"io/ioutil"
 	"io/ioutil"
 	"net/http"
 	"net/http"
 	"os"
 	"os"
@@ -140,7 +141,7 @@ func SaveConfig(configFile *ConfigFile) error {
 }
 }
 
 
 // try to register/login to the registry server
 // try to register/login to the registry server
-func Login(authConfig *AuthConfig) (string, error) {
+func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, error) {
 	client := &http.Client{}
 	client := &http.Client{}
 	reqStatusCode := 0
 	reqStatusCode := 0
 	var status string
 	var status string
@@ -171,7 +172,7 @@ func Login(authConfig *AuthConfig) (string, error) {
 			"Please check your e-mail for a confirmation link.")
 			"Please check your e-mail for a confirmation link.")
 	} else if reqStatusCode == 400 {
 	} else if reqStatusCode == 400 {
 		if string(reqBody) == "\"Username or email already exists\"" {
 		if string(reqBody) == "\"Username or email already exists\"" {
-			req, err := http.NewRequest("GET", IndexServerAddress()+"users/", nil)
+			req, err := factory.NewRequest("GET", IndexServerAddress()+"users/", nil)
 			req.SetBasicAuth(authConfig.Username, authConfig.Password)
 			req.SetBasicAuth(authConfig.Username, authConfig.Password)
 			resp, err := client.Do(req)
 			resp, err := client.Do(req)
 			if err != nil {
 			if err != nil {

+ 3 - 3
auth/auth_test.go

@@ -33,7 +33,7 @@ func TestLogin(t *testing.T) {
 	os.Setenv("DOCKER_INDEX_URL", "https://indexstaging-docker.dotcloud.com")
 	os.Setenv("DOCKER_INDEX_URL", "https://indexstaging-docker.dotcloud.com")
 	defer os.Setenv("DOCKER_INDEX_URL", "")
 	defer os.Setenv("DOCKER_INDEX_URL", "")
 	authConfig := &AuthConfig{Username: "unittester", Password: "surlautrerivejetattendrai", Email: "noise+unittester@dotcloud.com"}
 	authConfig := &AuthConfig{Username: "unittester", Password: "surlautrerivejetattendrai", Email: "noise+unittester@dotcloud.com"}
-	status, err := Login(authConfig)
+	status, err := Login(authConfig, nil)
 	if err != nil {
 	if err != nil {
 		t.Fatal(err)
 		t.Fatal(err)
 	}
 	}
@@ -53,7 +53,7 @@ func TestCreateAccount(t *testing.T) {
 	token := hex.EncodeToString(tokenBuffer)[:12]
 	token := hex.EncodeToString(tokenBuffer)[:12]
 	username := "ut" + token
 	username := "ut" + token
 	authConfig := &AuthConfig{Username: username, Password: "test42", Email: "docker-ut+" + token + "@example.com"}
 	authConfig := &AuthConfig{Username: username, Password: "test42", Email: "docker-ut+" + token + "@example.com"}
-	status, err := Login(authConfig)
+	status, err := Login(authConfig, nil)
 	if err != nil {
 	if err != nil {
 		t.Fatal(err)
 		t.Fatal(err)
 	}
 	}
@@ -63,7 +63,7 @@ func TestCreateAccount(t *testing.T) {
 		t.Fatalf("Expected status: \"%s\", found \"%s\" instead.", expectedStatus, status)
 		t.Fatalf("Expected status: \"%s\", found \"%s\" instead.", expectedStatus, status)
 	}
 	}
 
 
-	status, err = Login(authConfig)
+	status, err = Login(authConfig, nil)
 	if err == nil {
 	if err == nil {
 		t.Fatalf("Expected error but found nil instead")
 		t.Fatalf("Expected error but found nil instead")
 	}
 	}

+ 15 - 86
registry/registry.go

@@ -100,13 +100,6 @@ func ResolveRepositoryName(reposName string) (string, string, error) {
 	return endpoint, reposName, err
 	return endpoint, reposName, err
 }
 }
 
 
-// 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 doWithCookies(c *http.Client, req *http.Request) (*http.Response, error) {
 func doWithCookies(c *http.Client, req *http.Request) (*http.Response, error) {
 	for _, cookie := range c.Jar.Cookies(req.URL) {
 	for _, cookie := range c.Jar.Cookies(req.URL) {
 		req.AddCookie(cookie)
 		req.AddCookie(cookie)
@@ -121,29 +114,14 @@ func doWithCookies(c *http.Client, req *http.Request) (*http.Response, error) {
 	return res, err
 	return res, err
 }
 }
 
 
-// Set the user agent field in the header based on the versions provided
-// in NewRegistry() and extra.
-func (r *Registry) setUserAgent(req *http.Request, extra ...VersionInfo) {
-	if len(r.baseVersions)+len(extra) == 0 {
-		return
-	}
-	if len(extra) == 0 {
-		req.Header.Set("User-Agent", r.baseVersionsStr)
-	} else {
-		req.Header.Set("User-Agent", appendVersions(r.baseVersionsStr, extra...))
-	}
-	return
-}
-
 // Retrieve the history of a given image from the Registry.
 // Retrieve the history of a given image from the Registry.
 // Return a list of the parent's json (requested image included)
 // Return a list of the parent's json (requested image included)
 func (r *Registry) GetRemoteHistory(imgID, registry string, token []string) ([]string, error) {
 func (r *Registry) GetRemoteHistory(imgID, registry string, token []string) ([]string, error) {
-	req, err := http.NewRequest("GET", registry+"images/"+imgID+"/ancestry", nil)
+	req, err := r.reqFactory.NewRequest("GET", registry+"images/"+imgID+"/ancestry", nil)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 	req.Header.Set("Authorization", "Token "+strings.Join(token, ", "))
 	req.Header.Set("Authorization", "Token "+strings.Join(token, ", "))
-	r.setUserAgent(req)
 	res, err := doWithCookies(r.client, req)
 	res, err := doWithCookies(r.client, req)
 	if err != nil || res.StatusCode != 200 {
 	if err != nil || res.StatusCode != 200 {
 		if res != nil {
 		if res != nil {
@@ -170,7 +148,7 @@ func (r *Registry) GetRemoteHistory(imgID, registry string, token []string) ([]s
 func (r *Registry) LookupRemoteImage(imgID, registry string, token []string) bool {
 func (r *Registry) LookupRemoteImage(imgID, registry string, token []string) bool {
 	rt := &http.Transport{Proxy: http.ProxyFromEnvironment}
 	rt := &http.Transport{Proxy: http.ProxyFromEnvironment}
 
 
-	req, err := http.NewRequest("GET", registry+"images/"+imgID+"/json", nil)
+	req, err := r.reqFactory.NewRequest("GET", registry+"images/"+imgID+"/json", nil)
 	if err != nil {
 	if err != nil {
 		return false
 		return false
 	}
 	}
@@ -185,12 +163,11 @@ func (r *Registry) LookupRemoteImage(imgID, registry string, token []string) boo
 // Retrieve an image from the Registry.
 // Retrieve an image from the Registry.
 func (r *Registry) GetRemoteImageJSON(imgID, registry string, token []string) ([]byte, int, error) {
 func (r *Registry) GetRemoteImageJSON(imgID, registry string, token []string) ([]byte, int, error) {
 	// Get the JSON
 	// Get the JSON
-	req, err := http.NewRequest("GET", registry+"images/"+imgID+"/json", nil)
+	req, err := r.reqFactory.NewRequest("GET", registry+"images/"+imgID+"/json", nil)
 	if err != nil {
 	if err != nil {
 		return nil, -1, fmt.Errorf("Failed to download json: %s", err)
 		return nil, -1, fmt.Errorf("Failed to download json: %s", err)
 	}
 	}
 	req.Header.Set("Authorization", "Token "+strings.Join(token, ", "))
 	req.Header.Set("Authorization", "Token "+strings.Join(token, ", "))
-	r.setUserAgent(req)
 	res, err := doWithCookies(r.client, req)
 	res, err := doWithCookies(r.client, req)
 	if err != nil {
 	if err != nil {
 		return nil, -1, fmt.Errorf("Failed to download json: %s", err)
 		return nil, -1, fmt.Errorf("Failed to download json: %s", err)
@@ -213,12 +190,11 @@ func (r *Registry) GetRemoteImageJSON(imgID, registry string, token []string) ([
 }
 }
 
 
 func (r *Registry) GetRemoteImageLayer(imgID, registry string, token []string) (io.ReadCloser, error) {
 func (r *Registry) GetRemoteImageLayer(imgID, registry string, token []string) (io.ReadCloser, error) {
-	req, err := http.NewRequest("GET", registry+"images/"+imgID+"/layer", nil)
+	req, err := r.reqFactory.NewRequest("GET", registry+"images/"+imgID+"/layer", nil)
 	if err != nil {
 	if err != nil {
 		return nil, fmt.Errorf("Error while getting from the server: %s\n", err)
 		return nil, fmt.Errorf("Error while getting from the server: %s\n", err)
 	}
 	}
 	req.Header.Set("Authorization", "Token "+strings.Join(token, ", "))
 	req.Header.Set("Authorization", "Token "+strings.Join(token, ", "))
-	r.setUserAgent(req)
 	res, err := doWithCookies(r.client, req)
 	res, err := doWithCookies(r.client, req)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
@@ -239,7 +215,6 @@ func (r *Registry) GetRemoteTags(registries []string, repository string, token [
 			return nil, err
 			return nil, err
 		}
 		}
 		req.Header.Set("Authorization", "Token "+strings.Join(token, ", "))
 		req.Header.Set("Authorization", "Token "+strings.Join(token, ", "))
-		r.setUserAgent(req)
 		res, err := doWithCookies(r.client, req)
 		res, err := doWithCookies(r.client, req)
 		if err != nil {
 		if err != nil {
 			return nil, err
 			return nil, err
@@ -281,7 +256,6 @@ func (r *Registry) GetRepositoryData(indexEp, remote string) (*RepositoryData, e
 		req.SetBasicAuth(r.authConfig.Username, r.authConfig.Password)
 		req.SetBasicAuth(r.authConfig.Username, r.authConfig.Password)
 	}
 	}
 	req.Header.Set("X-Docker-Token", "true")
 	req.Header.Set("X-Docker-Token", "true")
-	r.setUserAgent(req)
 
 
 	res, err := r.client.Do(req)
 	res, err := r.client.Do(req)
 	if err != nil {
 	if err != nil {
@@ -339,7 +313,7 @@ func (r *Registry) PushImageChecksumRegistry(imgData *ImgData, registry string,
 
 
 	utils.Debugf("[registry] Calling PUT %s", registry+"images/"+imgData.ID+"/checksum")
 	utils.Debugf("[registry] Calling PUT %s", registry+"images/"+imgData.ID+"/checksum")
 
 
-	req, err := http.NewRequest("PUT", registry+"images/"+imgData.ID+"/checksum", nil)
+	req, err := r.reqFactory.NewRequest("PUT", registry+"images/"+imgData.ID+"/checksum", nil)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -375,13 +349,12 @@ func (r *Registry) PushImageJSONRegistry(imgData *ImgData, jsonRaw []byte, regis
 
 
 	utils.Debugf("[registry] Calling PUT %s", registry+"images/"+imgData.ID+"/json")
 	utils.Debugf("[registry] Calling PUT %s", registry+"images/"+imgData.ID+"/json")
 
 
-	req, err := http.NewRequest("PUT", registry+"images/"+imgData.ID+"/json", bytes.NewReader(jsonRaw))
+	req, err := r.reqFactory.NewRequest("PUT", registry+"images/"+imgData.ID+"/json", bytes.NewReader(jsonRaw))
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 	req.Header.Add("Content-type", "application/json")
 	req.Header.Add("Content-type", "application/json")
 	req.Header.Set("Authorization", "Token "+strings.Join(token, ","))
 	req.Header.Set("Authorization", "Token "+strings.Join(token, ","))
-	r.setUserAgent(req)
 
 
 	res, err := doWithCookies(r.client, req)
 	res, err := doWithCookies(r.client, req)
 	if err != nil {
 	if err != nil {
@@ -410,14 +383,13 @@ func (r *Registry) PushImageLayerRegistry(imgID string, layer io.Reader, registr
 
 
 	tarsumLayer := &utils.TarSum{Reader: layer}
 	tarsumLayer := &utils.TarSum{Reader: layer}
 
 
-	req, err := http.NewRequest("PUT", registry+"images/"+imgID+"/layer", tarsumLayer)
+	req, err := r.reqFactory.NewRequest("PUT", registry+"images/"+imgID+"/layer", tarsumLayer)
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
 	}
 	}
 	req.ContentLength = -1
 	req.ContentLength = -1
 	req.TransferEncoding = []string{"chunked"}
 	req.TransferEncoding = []string{"chunked"}
 	req.Header.Set("Authorization", "Token "+strings.Join(token, ","))
 	req.Header.Set("Authorization", "Token "+strings.Join(token, ","))
-	r.setUserAgent(req)
 	res, err := doWithCookies(r.client, req)
 	res, err := doWithCookies(r.client, req)
 	if err != nil {
 	if err != nil {
 		return "", fmt.Errorf("Failed to upload layer: %s", err)
 		return "", fmt.Errorf("Failed to upload layer: %s", err)
@@ -435,7 +407,7 @@ func (r *Registry) PushImageLayerRegistry(imgID string, layer io.Reader, registr
 }
 }
 
 
 func (r *Registry) opaqueRequest(method, urlStr string, body io.Reader) (*http.Request, error) {
 func (r *Registry) opaqueRequest(method, urlStr string, body io.Reader) (*http.Request, error) {
-	req, err := http.NewRequest(method, urlStr, body)
+	req, err := r.reqFactory.NewRequest(method, urlStr, body)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -455,7 +427,6 @@ func (r *Registry) PushRegistryTag(remote, revision, tag, registry string, token
 	}
 	}
 	req.Header.Add("Content-type", "application/json")
 	req.Header.Add("Content-type", "application/json")
 	req.Header.Set("Authorization", "Token "+strings.Join(token, ","))
 	req.Header.Set("Authorization", "Token "+strings.Join(token, ","))
-	r.setUserAgent(req)
 	req.ContentLength = int64(len(revision))
 	req.ContentLength = int64(len(revision))
 	res, err := doWithCookies(r.client, req)
 	res, err := doWithCookies(r.client, req)
 	if err != nil {
 	if err != nil {
@@ -500,7 +471,6 @@ func (r *Registry) PushImageJSONIndex(indexEp, remote string, imgList []*ImgData
 	req.SetBasicAuth(r.authConfig.Username, r.authConfig.Password)
 	req.SetBasicAuth(r.authConfig.Username, r.authConfig.Password)
 	req.ContentLength = int64(len(imgListJSON))
 	req.ContentLength = int64(len(imgListJSON))
 	req.Header.Set("X-Docker-Token", "true")
 	req.Header.Set("X-Docker-Token", "true")
-	r.setUserAgent(req)
 	if validate {
 	if validate {
 		req.Header["X-Docker-Endpoints"] = regs
 		req.Header["X-Docker-Endpoints"] = regs
 	}
 	}
@@ -521,7 +491,6 @@ func (r *Registry) PushImageJSONIndex(indexEp, remote string, imgList []*ImgData
 		req.SetBasicAuth(r.authConfig.Username, r.authConfig.Password)
 		req.SetBasicAuth(r.authConfig.Username, r.authConfig.Password)
 		req.ContentLength = int64(len(imgListJSON))
 		req.ContentLength = int64(len(imgListJSON))
 		req.Header.Set("X-Docker-Token", "true")
 		req.Header.Set("X-Docker-Token", "true")
-		r.setUserAgent(req)
 		if validate {
 		if validate {
 			req.Header["X-Docker-Endpoints"] = regs
 			req.Header["X-Docker-Endpoints"] = regs
 		}
 		}
@@ -576,7 +545,7 @@ func (r *Registry) PushImageJSONIndex(indexEp, remote string, imgList []*ImgData
 
 
 func (r *Registry) SearchRepositories(term string) (*SearchResults, error) {
 func (r *Registry) SearchRepositories(term string) (*SearchResults, error) {
 	u := auth.IndexServerAddress() + "search?q=" + url.QueryEscape(term)
 	u := auth.IndexServerAddress() + "search?q=" + url.QueryEscape(term)
-	req, err := http.NewRequest("GET", u, nil)
+	req, err := r.reqFactory.NewRequest("GET", u, nil)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -628,52 +597,12 @@ type ImgData struct {
 }
 }
 
 
 type Registry struct {
 type Registry struct {
-	client          *http.Client
-	authConfig      *auth.AuthConfig
-	baseVersions    []VersionInfo
-	baseVersionsStr string
+	client     *http.Client
+	authConfig *auth.AuthConfig
+	reqFactory *utils.HTTPRequestFactory
 }
 }
 
 
-func validVersion(version VersionInfo) bool {
-	stopChars := " \t\r\n/"
-	if strings.ContainsAny(version.Name(), stopChars) {
-		return false
-	}
-	if strings.ContainsAny(version.Version(), 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
-	}
-
-	var buf bytes.Buffer
-	if len(base) > 0 {
-		buf.Write([]byte(base))
-	}
-
-	for _, v := range versions {
-		if !validVersion(v) {
-			continue
-		}
-		buf.Write([]byte(v.Name()))
-		buf.Write([]byte("/"))
-		buf.Write([]byte(v.Version()))
-		buf.Write([]byte(" "))
-	}
-	return buf.String()
-}
-
-func NewRegistry(root string, authConfig *auth.AuthConfig, baseVersions ...VersionInfo) (r *Registry, err error) {
+func NewRegistry(root string, authConfig *auth.AuthConfig, factory *utils.HTTPRequestFactory) (r *Registry, err error) {
 	httpTransport := &http.Transport{
 	httpTransport := &http.Transport{
 		DisableKeepAlives: true,
 		DisableKeepAlives: true,
 		Proxy:             http.ProxyFromEnvironment,
 		Proxy:             http.ProxyFromEnvironment,
@@ -689,7 +618,7 @@ func NewRegistry(root string, authConfig *auth.AuthConfig, baseVersions ...Versi
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
-	r.baseVersions = baseVersions
-	r.baseVersionsStr = appendVersions("", baseVersions...)
+
+	r.reqFactory = factory
 	return r, nil
 	return r, nil
 }
 }

+ 16 - 5
server.go

@@ -52,9 +52,9 @@ func (v *simpleVersionInfo) Version() string {
 // docker, go, git-commit (of the docker) and the host's kernel.
 // docker, go, git-commit (of the docker) and the host's kernel.
 //
 //
 // Such information will be used on call to NewRegistry().
 // Such information will be used on call to NewRegistry().
-func (srv *Server) versionInfos() []registry.VersionInfo {
+func (srv *Server) versionInfos() []utils.VersionInfo {
 	v := srv.DockerVersion()
 	v := srv.DockerVersion()
-	ret := make([]registry.VersionInfo, 0, 4)
+	ret := make([]utils.VersionInfo, 0, 4)
 	ret = append(ret, &simpleVersionInfo{"docker", v.Version})
 	ret = append(ret, &simpleVersionInfo{"docker", v.Version})
 
 
 	if len(v.GoVersion) > 0 {
 	if len(v.GoVersion) > 0 {
@@ -102,7 +102,7 @@ func (srv *Server) ContainerExport(name string, out io.Writer) error {
 }
 }
 
 
 func (srv *Server) ImagesSearch(term string) ([]APISearch, error) {
 func (srv *Server) ImagesSearch(term string) ([]APISearch, error) {
-	r, err := registry.NewRegistry(srv.runtime.root, nil, srv.versionInfos()...)
+	r, err := registry.NewRegistry(srv.runtime.root, nil, srv.HTTPRequestFactory())
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -559,7 +559,7 @@ func (srv *Server) poolRemove(kind, key string) error {
 }
 }
 
 
 func (srv *Server) ImagePull(localName string, tag string, out io.Writer, sf *utils.StreamFormatter, authConfig *auth.AuthConfig) error {
 func (srv *Server) ImagePull(localName string, tag string, out io.Writer, sf *utils.StreamFormatter, authConfig *auth.AuthConfig) error {
-	r, err := registry.NewRegistry(srv.runtime.root, authConfig, srv.versionInfos()...)
+	r, err := registry.NewRegistry(srv.runtime.root, authConfig, srv.HTTPRequestFactory())
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -720,7 +720,7 @@ func (srv *Server) ImagePush(localName string, out io.Writer, sf *utils.StreamFo
 
 
 	out = utils.NewWriteFlusher(out)
 	out = utils.NewWriteFlusher(out)
 	img, err := srv.runtime.graph.Get(localName)
 	img, err := srv.runtime.graph.Get(localName)
-	r, err2 := registry.NewRegistry(srv.runtime.root, authConfig, srv.versionInfos()...)
+	r, err2 := registry.NewRegistry(srv.runtime.root, authConfig, srv.HTTPRequestFactory())
 	if err2 != nil {
 	if err2 != nil {
 		return err2
 		return err2
 	}
 	}
@@ -1164,11 +1164,21 @@ func NewServer(flGraphPath string, autoRestart, enableCors bool, dns ListOpts) (
 		pushingPool: make(map[string]struct{}),
 		pushingPool: make(map[string]struct{}),
 		events:      make([]utils.JSONMessage, 0, 64), //only keeps the 64 last events
 		events:      make([]utils.JSONMessage, 0, 64), //only keeps the 64 last events
 		listeners:   make(map[string]chan utils.JSONMessage),
 		listeners:   make(map[string]chan utils.JSONMessage),
+		reqFactory:  nil,
 	}
 	}
 	runtime.srv = srv
 	runtime.srv = srv
 	return srv, nil
 	return srv, nil
 }
 }
 
 
+func (srv *Server) HTTPRequestFactory() *utils.HTTPRequestFactory {
+	if srv.reqFactory == nil {
+		ud := utils.NewHTTPUserAgentDecorator(srv.versionInfos()...)
+		factory := utils.NewHTTPRequestFactory(ud)
+		srv.reqFactory = factory
+	}
+	return srv.reqFactory
+}
+
 func (srv *Server) LogEvent(action, id string) {
 func (srv *Server) LogEvent(action, id string) {
 	now := time.Now().Unix()
 	now := time.Now().Unix()
 	jm := utils.JSONMessage{Status: action, ID: id, Time: now}
 	jm := utils.JSONMessage{Status: action, ID: id, Time: now}
@@ -1189,4 +1199,5 @@ type Server struct {
 	pushingPool map[string]struct{}
 	pushingPool map[string]struct{}
 	events      []utils.JSONMessage
 	events      []utils.JSONMessage
 	listeners   map[string]chan utils.JSONMessage
 	listeners   map[string]chan utils.JSONMessage
+	reqFactory  *utils.HTTPRequestFactory
 }
 }

+ 134 - 0
utils/http.go

@@ -0,0 +1,134 @@
+package utils
+
+import (
+	"bytes"
+	"io"
+	"net/http"
+	"strings"
+)
+
+// 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 {
+	stopChars := " \t\r\n/"
+	if strings.ContainsAny(version.Name(), stopChars) {
+		return false
+	}
+	if strings.ContainsAny(version.Version(), 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
+	}
+
+	var buf bytes.Buffer
+	if len(base) > 0 {
+		buf.Write([]byte(base))
+	}
+
+	for _, v := range versions {
+		name := []byte(v.Name())
+		version := []byte(v.Version())
+
+		if len(name) == 0 || len(version) == 0 {
+			continue
+		}
+		if !validVersion(v) {
+			continue
+		}
+		buf.Write([]byte(v.Name()))
+		buf.Write([]byte("/"))
+		buf.Write([]byte(v.Version()))
+		buf.Write([]byte(" "))
+	}
+	return buf.String()
+}
+
+// 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 {
+	ret := new(HTTPUserAgentDecorator)
+	ret.versions = versions
+	return ret
+}
+
+func (self *HTTPUserAgentDecorator) ChangeRequest(req *http.Request) (newReq *http.Request, err error) {
+	if req == nil {
+		return req, nil
+	}
+
+	userAgent := appendVersions(req.UserAgent(), self.versions...)
+	if len(userAgent) > 0 {
+		req.Header.Set("User-Agent", userAgent)
+	}
+	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 {
+	ret := new(HTTPRequestFactory)
+	ret.decorators = d
+	return ret
+}
+
+// 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 (self *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 self == nil {
+		return req, nil
+	}
+	for _, dec := range self.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
+		}
+	}
+	return req, err
+}