diff --git a/Gopkg.lock b/Gopkg.lock index f29f0e5163..1439a642d8 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -109,8 +109,14 @@ [[projects]] name = "github.com/fabric8-services/fabric8-common" - packages = ["id"] - revision = "51e44b47e514721ddaa3fca99e5fb5bc1a6e7eb2" + packages = [ + "configuration", + "errors", + "httpsupport", + "id", + "log" + ] + revision = "a85e65e22cfc18d7c2004317d340ad328ca8dcd1" [[projects]] name = "github.com/fabric8-services/fabric8-notification" @@ -817,6 +823,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "beb030782f5b973d71fe2e572e9dc7d62e11c3e87e44287930b04d4d93946e54" + inputs-digest = "b4592fb885f50aa589fa251d49f36b127ea9af0fb47006a9d5422d59a2cdf0b7" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index f9721cfbcd..1ee9e0ba63 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -130,7 +130,7 @@ ignored = [ [[constraint]] name = "github.com/fabric8-services/fabric8-common" - revision = "51e44b47e514721ddaa3fca99e5fb5bc1a6e7eb2" + revision = "a85e65e22cfc18d7c2004317d340ad328ca8dcd1" [[constraint]] name = "github.com/jstemmer/go-junit-report" diff --git a/controller/collaborators.go b/controller/collaborators.go index 3c5bf952f3..1e38cd1d0b 100644 --- a/controller/collaborators.go +++ b/controller/collaborators.go @@ -3,9 +3,10 @@ package controller import ( "net/http" + "github.com/fabric8-services/fabric8-common/httpsupport" "github.com/fabric8-services/fabric8-wit/app" "github.com/fabric8-services/fabric8-wit/auth" - "github.com/fabric8-services/fabric8-wit/rest/proxy" + "github.com/goadesign/goa" ) @@ -28,25 +29,25 @@ func NewCollaboratorsController(service *goa.Service, config CollaboratorsConfig // List collaborators for the given space ID. func (c *CollaboratorsController) List(ctx *app.ListCollaboratorsContext) error { - return proxy.RouteHTTP(ctx, c.config.GetAuthShortServiceHostName()) + return httpsupport.RouteHTTP(ctx, c.config.GetAuthShortServiceHostName()) } // Add user's identity to the list of space collaborators. func (c *CollaboratorsController) Add(ctx *app.AddCollaboratorsContext) error { - return proxy.RouteHTTP(ctx, c.config.GetAuthShortServiceHostName()) + return httpsupport.RouteHTTP(ctx, c.config.GetAuthShortServiceHostName()) } // AddMany adds user's identities to the list of space collaborators. func (c *CollaboratorsController) AddMany(ctx *app.AddManyCollaboratorsContext) error { - return proxy.RouteHTTP(ctx, c.config.GetAuthShortServiceHostName()) + return httpsupport.RouteHTTP(ctx, c.config.GetAuthShortServiceHostName()) } // Remove user from the list of space collaborators. func (c *CollaboratorsController) Remove(ctx *app.RemoveCollaboratorsContext) error { - return proxy.RouteHTTP(ctx, c.config.GetAuthShortServiceHostName()) + return httpsupport.RouteHTTP(ctx, c.config.GetAuthShortServiceHostName()) } // RemoveMany removes users from the list of space collaborators. func (c *CollaboratorsController) RemoveMany(ctx *app.RemoveManyCollaboratorsContext) error { - return proxy.RouteHTTP(ctx, c.config.GetAuthShortServiceHostName()) + return httpsupport.RouteHTTP(ctx, c.config.GetAuthShortServiceHostName()) } diff --git a/controller/features.go b/controller/features.go index aa5bacb68c..b2cd150cbb 100644 --- a/controller/features.go +++ b/controller/features.go @@ -1,8 +1,9 @@ package controller import ( + "github.com/fabric8-services/fabric8-common/httpsupport" "github.com/fabric8-services/fabric8-wit/app" - "github.com/fabric8-services/fabric8-wit/rest/proxy" + "github.com/goadesign/goa" ) @@ -27,10 +28,10 @@ func NewFeaturesController(service *goa.Service, config FeaturesControllerConfig // List runs the list action. func (c *FeaturesController) List(ctx *app.ListFeaturesContext) error { - return proxy.RouteHTTP(ctx, c.config.GetTogglesServiceURL()) + return httpsupport.RouteHTTP(ctx, c.config.GetTogglesServiceURL()) } // Show runs the show action. func (c *FeaturesController) Show(ctx *app.ShowFeaturesContext) error { - return proxy.RouteHTTP(ctx, c.config.GetTogglesServiceURL()) + return httpsupport.RouteHTTP(ctx, c.config.GetTogglesServiceURL()) } diff --git a/controller/login.go b/controller/login.go index 8cc744a267..be4614796e 100644 --- a/controller/login.go +++ b/controller/login.go @@ -5,6 +5,7 @@ import ( "net/http" "net/url" + "github.com/fabric8-services/fabric8-common/httpsupport" "github.com/fabric8-services/fabric8-wit/account" "github.com/fabric8-services/fabric8-wit/app" "github.com/fabric8-services/fabric8-wit/auth" @@ -13,7 +14,6 @@ import ( "github.com/fabric8-services/fabric8-wit/jsonapi" "github.com/fabric8-services/fabric8-wit/login" - "github.com/fabric8-services/fabric8-wit/rest/proxy" "github.com/goadesign/goa" ) @@ -49,7 +49,7 @@ func (c *LoginController) Authorize(ctx *app.AuthorizeLoginContext) error { // Refresh obtain a new access token using the refresh token. func (c *LoginController) Refresh(ctx *app.RefreshLoginContext) error { - return proxy.RouteHTTPToPath(ctx, c.configuration.GetAuthShortServiceHostName(), authservice.RefreshTokenPath()) + return httpsupport.RouteHTTPToPath(ctx, c.configuration.GetAuthShortServiceHostName(), authservice.RefreshTokenPath()) } func redirectLocation(params url.Values, location string) (string, error) { @@ -77,5 +77,5 @@ func redirectWithParams(ctx redirectContext, config auth.ServiceConfiguration, h // Generate generates access tokens in Dev Mode func (c *LoginController) Generate(ctx *app.GenerateLoginContext) error { - return proxy.RouteHTTPToPath(ctx, c.configuration.GetAuthServiceURL(), authservice.GenerateTokenPath()) + return httpsupport.RouteHTTPToPath(ctx, c.configuration.GetAuthServiceURL(), authservice.GenerateTokenPath()) } diff --git a/controller/search.go b/controller/search.go index 6338fe4e78..a9d75fd75d 100644 --- a/controller/search.go +++ b/controller/search.go @@ -10,8 +10,8 @@ import ( "strings" "time" + "github.com/fabric8-services/fabric8-common/httpsupport" "github.com/fabric8-services/fabric8-common/id" - "github.com/fabric8-services/fabric8-wit/app" "github.com/fabric8-services/fabric8-wit/application" "github.com/fabric8-services/fabric8-wit/auth" @@ -20,7 +20,6 @@ import ( "github.com/fabric8-services/fabric8-wit/jsonapi" "github.com/fabric8-services/fabric8-wit/log" "github.com/fabric8-services/fabric8-wit/ptr" - "github.com/fabric8-services/fabric8-wit/rest/proxy" "github.com/fabric8-services/fabric8-wit/search" "github.com/fabric8-services/fabric8-wit/space" "github.com/fabric8-services/fabric8-wit/workitem" @@ -463,7 +462,7 @@ func (c *SearchController) Spaces(ctx *app.SpacesSearchContext) error { // Users runs the user search action. func (c *SearchController) Users(ctx *app.UsersSearchContext) error { - return proxy.RouteHTTP(ctx, c.configuration.GetAuthShortServiceHostName()) + return httpsupport.RouteHTTP(ctx, c.configuration.GetAuthShortServiceHostName()) } // Iterate over the WI list and read parent IDs diff --git a/controller/users.go b/controller/users.go index 684dc033cc..1b42bc0f97 100644 --- a/controller/users.go +++ b/controller/users.go @@ -3,21 +3,20 @@ package controller import ( "context" "fmt" - "github.com/fabric8-services/fabric8-wit/errors" "math/rand" "net/http" + "github.com/fabric8-services/fabric8-common/httpsupport" idpackage "github.com/fabric8-services/fabric8-common/id" "github.com/fabric8-services/fabric8-wit/account" "github.com/fabric8-services/fabric8-wit/app" "github.com/fabric8-services/fabric8-wit/application" "github.com/fabric8-services/fabric8-wit/auth" + "github.com/fabric8-services/fabric8-wit/errors" "github.com/fabric8-services/fabric8-wit/jsonapi" "github.com/fabric8-services/fabric8-wit/log" "github.com/fabric8-services/fabric8-wit/rest" "github.com/fabric8-services/fabric8-wit/token" - - "github.com/fabric8-services/fabric8-wit/rest/proxy" "github.com/goadesign/goa" errs "github.com/pkg/errors" "github.com/satori/go.uuid" @@ -158,7 +157,7 @@ func (c *UsersController) Obfuscate(ctx *app.ObfuscateUsersContext) error { // Show runs the show action. func (c *UsersController) Show(ctx *app.ShowUsersContext) error { - return proxy.RouteHTTP(ctx, c.config.GetAuthShortServiceHostName()) + return httpsupport.RouteHTTP(ctx, c.config.GetAuthShortServiceHostName()) } // CreateUserAsServiceAccount updates a user when requested using a service account token @@ -398,12 +397,12 @@ func (c *UsersController) updateUserInDB(id *uuid.UUID, ctx *app.UpdateUserAsSer // Update updates the authorized user based on the provided Token func (c *UsersController) Update(ctx *app.UpdateUsersContext) error { - return proxy.RouteHTTP(ctx, c.config.GetAuthShortServiceHostName()) + return httpsupport.RouteHTTP(ctx, c.config.GetAuthShortServiceHostName()) } // List runs the list action. func (c *UsersController) List(ctx *app.ListUsersContext) error { - return proxy.RouteHTTP(ctx, c.config.GetAuthShortServiceHostName()) + return httpsupport.RouteHTTP(ctx, c.config.GetAuthShortServiceHostName()) } // ConvertUsersSimple converts a array of simple Identity IDs into a Generic Reletionship List diff --git a/rest/proxy/proxy.go b/rest/proxy/proxy.go deleted file mode 100644 index fbf4f8b662..0000000000 --- a/rest/proxy/proxy.go +++ /dev/null @@ -1,167 +0,0 @@ -package proxy - -import ( - "bytes" - "compress/gzip" - "context" - "io/ioutil" - "net/http" - "net/http/httputil" - "net/url" - "strings" - - "github.com/fabric8-services/fabric8-wit/jsonapi" - "github.com/fabric8-services/fabric8-wit/log" - - "github.com/goadesign/goa" - "github.com/pkg/errors" -) - -// RouteHTTPToPath uses a reverse proxy to route the http request to the scheme, host provided in targetHost -// and path provided in targetPath. -func RouteHTTPToPath(ctx jsonapi.InternalServerErrorContext, targetHost string, targetPath string) error { - return route(ctx, targetHost, &targetPath) -} - -// RouteHTTP uses a reverse proxy to route the http request to the scheme, host, and base path provided in target. -// If the target's path is "/base" and the incoming request was for "/dir", -// the target request will be for /base/dir. -func RouteHTTP(ctx jsonapi.InternalServerErrorContext, target string) error { - return route(ctx, target, nil) -} - -func route(ctx jsonapi.InternalServerErrorContext, targetHost string, targetPath *string) error { - rw := goa.ContextResponse(ctx) - if rw == nil { - return jsonapi.JSONErrorResponse(ctx, errors.New("unable to get response from context")) - } - req := goa.ContextRequest(ctx) - if req == nil { - return jsonapi.JSONErrorResponse(ctx, errors.New("unable to get request from context")) - } - - targetURL, err := url.Parse(targetHost) - if err != nil { - log.Error(ctx, map[string]interface{}{ - "err": err, - "target_host": targetHost, - "request_uri": req.RequestURI, - }, "unable to parse target host") - return jsonapi.JSONErrorResponse(ctx, err) - } - - director := newDirector(ctx, req, targetURL, targetPath) - proxy := &httputil.ReverseProxy{Director: director} - - if strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") { - gzr := gunzipResponseWriter{ctx: ctx, ResponseWriter: rw, targetURL: *req.URL} - proxy.ServeHTTP(gzr, req.Request) - } else { - proxy.ServeHTTP(rw, req.Request) - } - - return nil -} - -func newDirector(ctx context.Context, originalRequestData *goa.RequestData, target *url.URL, targetPath *string) func(*http.Request) { - targetQuery := target.RawQuery - return func(req *http.Request) { - // Get the original request URL for info log - scheme := "http" - if req.URL != nil && req.URL.Scheme == "https" { // isHTTPS - scheme = "https" - } - xForwardProto := req.Header.Get("X-Forwarded-Proto") - if xForwardProto != "" { - scheme = xForwardProto - } - originalReq := &url.URL{Scheme: scheme, Host: originalRequestData.Host, Path: req.URL.Path, RawQuery: req.URL.RawQuery} - - // Modify the request to route to a new target - if target.Scheme == "" { - req.URL.Scheme = "http" - } else { - req.URL.Scheme = target.Scheme - } - req.URL.Host = target.Host - if targetPath != nil { - req.URL.Path = *targetPath - } else { - req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path) - } - if targetQuery == "" || req.URL.RawQuery == "" { - req.URL.RawQuery = targetQuery + req.URL.RawQuery - } else { - req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery - } - if _, ok := req.Header["User-Agent"]; !ok { - // explicitly disable User-Agent so it's not set to default value - req.Header.Set("User-Agent", "") - } - requestID := log.ExtractRequestID(ctx) - if requestID != "" { - req.Header.Set("X-Request-ID", requestID) - } - - // Log the original and target URLs - originalReqString := originalReq.String() - targetReqString := req.URL.String() - log.Info(ctx, map[string]interface{}{ - "original_req_url": originalReqString, - "target_req_url": targetReqString, - "target": target, - "target_string": target.String(), - }, "Routing %s to %s", originalReqString, targetReqString) - } -} - -type gunzipResponseWriter struct { - http.ResponseWriter - ctx context.Context - targetURL url.URL -} - -func (w gunzipResponseWriter) Write(b []byte) (int, error) { - // Write gunzipped data to the client - gr, err := gzip.NewReader(bytes.NewBuffer(b)) - if err != nil { - return 0, err - } - defer func() { - err := gr.Close() - if err != nil { - log.Error(w.ctx, map[string]interface{}{ - "err": err, - "target_url": w.targetURL.String(), - }, "unable to close gzip writer while serving request in proxy") - } - }() - data, err := ioutil.ReadAll(gr) - if err != nil { - return 0, err - } - return w.ResponseWriter.Write(data) -} - -func (w gunzipResponseWriter) WriteHeader(code int) { - w.Header().Del("Content-Length") - // Remove duplicated headers - for key, value := range w.Header() { - if len(value) > 0 { - w.Header().Set(key, value[0]) - } - } - w.ResponseWriter.WriteHeader(code) -} - -func singleJoiningSlash(a, b string) string { - aslash := strings.HasSuffix(a, "/") - bslash := strings.HasPrefix(b, "/") - switch { - case aslash && bslash: - return a + b[1:] - case !aslash && !bslash: - return a + "/" + b - } - return a + b -} diff --git a/rest/proxy/proxy_test.go b/rest/proxy/proxy_test.go deleted file mode 100644 index 517649329c..0000000000 --- a/rest/proxy/proxy_test.go +++ /dev/null @@ -1,130 +0,0 @@ -package proxy - -import ( - "compress/gzip" - "context" - "fmt" - "io" - "net/http" - "net/http/httptest" - "net/url" - "strings" - "testing" - "time" - - "github.com/fabric8-services/fabric8-wit/app" - "github.com/fabric8-services/fabric8-wit/resource" - "github.com/fabric8-services/fabric8-wit/rest" - - "github.com/goadesign/goa" - "github.com/satori/go.uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestProxy(t *testing.T) { - resource.Require(t, resource.UnitTest) - go startServer() - waitForServer(t) - - // GET with custom header and 201 response - rw := httptest.NewRecorder() - u, err := url.Parse("http://domain.org/api") - require.NoError(t, err) - req, err := http.NewRequest("GET", u.String(), nil) - require.NoError(t, err) - - ctx := context.Background() - goaCtx := goa.NewContext(goa.WithAction(ctx, "ProxyTest"), rw, req, url.Values{}) - statusCtx, err := app.NewShowAreaContext(goaCtx, req, goa.New("StatusService")) // needs a context matching an endpoint that support the `InternalServerError` response type - require.NoError(t, err) - statusCtx.Request.Header.Del("Accept-Encoding") - - err = RouteHTTP(statusCtx, "http://localhost:8889") - require.NoError(t, err) - - assert.Equal(t, 201, rw.Code) - assert.Equal(t, "proxyTest", rw.Header().Get("Custom-Test-Header")) - body := rest.ReadBody(rw.Result().Body) - assert.Equal(t, veryLongBody, body) - - // POST, gzipped, changed target path - rw = httptest.NewRecorder() - req, err = http.NewRequest("POST", u.String(), nil) - require.NoError(t, err) - - ctx = context.Background() - goaCtx = goa.NewContext(goa.WithAction(ctx, "ProxyTest"), rw, req, url.Values{}) - statusCtx, err = app.NewShowAreaContext(goaCtx, req, goa.New("StatusService")) - require.NoError(t, err) - statusCtx.Request.Header.Set("Accept-Encoding", "gzip") - - err = RouteHTTPToPath(statusCtx, "http://localhost:8889", "/api") - require.NoError(t, err) - - assert.Equal(t, 201, rw.Code) - assert.Equal(t, "proxyTest", rw.Header().Get("Custom-Test-Header")) - body = rest.ReadBody(rw.Result().Body) - assert.Equal(t, veryLongBody, body) -} - -func startServer() { - http.HandleFunc("/api", handlerGzip) - http.ListenAndServe(":8889", nil) -} - -func waitForServer(t *testing.T) { - req, err := http.NewRequest("GET", "http://localhost:8889/api", nil) - require.NoError(t, err) - for i := 0; i < 30; i++ { - time.Sleep(100 * time.Millisecond) - client := &http.Client{Timeout: time.Duration(500 * time.Millisecond)} - res, err := client.Do(req) - if err == nil && res.StatusCode == 201 { - return - } - } - assert.Fail(t, "Failed to start server") -} - -func handler(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Custom-Test-Header", "proxyTest") - w.WriteHeader(201) - fmt.Fprint(w, veryLongBody) -} - -func handlerGzip(w http.ResponseWriter, r *http.Request) { - if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { - handler(w, r) - return - } - w.Header().Set("Content-Encoding", "gzip") - gz := gzip.NewWriter(w) - defer gz.Close() - gzr := gzipResponseWriter{Writer: gz, ResponseWriter: w} - handler(gzr, r) -} - -type gzipResponseWriter struct { - io.Writer - http.ResponseWriter -} - -func (w gzipResponseWriter) Write(b []byte) (int, error) { - return w.Writer.Write(b) -} - -func (w gzipResponseWriter) WriteHeader(code int) { - w.Header().Del("Content-Length") - w.ResponseWriter.WriteHeader(code) -} - -var veryLongBody = generateLongBody() - -func generateLongBody() string { - body := uuid.NewV4().String() - for i := 0; i < 100; i++ { - body = body + uuid.NewV4().String() - } - return body -}