diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index d6e13a6991d..d84056176a0 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -93,8 +93,10 @@ func (g *APIGroup) InstallREST(mux mux, paths ...string) { for _, prefix := range paths { prefix = strings.TrimRight(prefix, "/") + proxyHandler := &ProxyHandler{prefix + "/proxy/", g.handler.storage, g.handler.codec} mux.Handle(prefix+"/", http.StripPrefix(prefix, restHandler)) mux.Handle(prefix+"/watch/", http.StripPrefix(prefix+"/watch/", watchHandler)) + mux.Handle(prefix+"/proxy/", http.StripPrefix(prefix+"/proxy/", proxyHandler)) mux.Handle(prefix+"/redirect/", http.StripPrefix(prefix+"/redirect/", redirectHandler)) mux.Handle(prefix+"/operations", http.StripPrefix(prefix+"/operations", opHandler)) mux.Handle(prefix+"/operations/", http.StripPrefix(prefix+"/operations/", opHandler)) diff --git a/pkg/apiserver/apiserver_test.go b/pkg/apiserver/apiserver_test.go index 12d8ae3585c..f08dc999f9c 100644 --- a/pkg/apiserver/apiserver_test.go +++ b/pkg/apiserver/apiserver_test.go @@ -77,8 +77,9 @@ type SimpleRESTStorage struct { requestedFieldSelector labels.Selector requestedResourceVersion uint64 - // The location + // The id requested, and location to return for ResourceLocation requestedResourceLocationID string + resourceLocation string // If non-nil, called inside the WorkFunc when answering update, delete, create. // obj receives the original input to the update, delete, or create call. @@ -157,7 +158,7 @@ func (storage *SimpleRESTStorage) ResourceLocation(id string) (string, error) { if err := storage.errors["resourceLocation"]; err != nil { return "", err } - return id, nil + return storage.resourceLocation, nil } func extractBody(response *http.Response, object runtime.Object) (string, error) { diff --git a/pkg/apiserver/minionproxy.go b/pkg/apiserver/minionproxy.go index b85e0ccc8ef..23ac515a367 100644 --- a/pkg/apiserver/minionproxy.go +++ b/pkg/apiserver/minionproxy.go @@ -31,6 +31,7 @@ import ( "github.com/golang/glog" ) +// TODO: replace with proxy handler on minions func handleProxyMinion(w http.ResponseWriter, req *http.Request) { path := strings.TrimLeft(req.URL.Path, "/") rawQuery := req.URL.RawQuery diff --git a/pkg/apiserver/proxy.go b/pkg/apiserver/proxy.go new file mode 100644 index 00000000000..02ebf9c39d4 --- /dev/null +++ b/pkg/apiserver/proxy.go @@ -0,0 +1,226 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package apiserver + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "net/http/httputil" + "net/url" + "path" + "strings" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/httplog" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + + "code.google.com/p/go.net/html" + "github.com/golang/glog" +) + +// tagsToAttrs states which attributes of which tags require URL substitution. +// Sources: http://www.w3.org/TR/REC-html40/index/attributes.html +// http://www.w3.org/html/wg/drafts/html/master/index.html#attributes-1 +var tagsToAttrs = map[string]util.StringSet{ + "a": util.NewStringSet("href"), + "applet": util.NewStringSet("codebase"), + "area": util.NewStringSet("href"), + "audio": util.NewStringSet("src"), + "base": util.NewStringSet("href"), + "blockquote": util.NewStringSet("cite"), + "body": util.NewStringSet("background"), + "button": util.NewStringSet("formaction"), + "command": util.NewStringSet("icon"), + "del": util.NewStringSet("cite"), + "embed": util.NewStringSet("src"), + "form": util.NewStringSet("action"), + "frame": util.NewStringSet("longdesc", "src"), + "head": util.NewStringSet("profile"), + "html": util.NewStringSet("manifest"), + "iframe": util.NewStringSet("longdesc", "src"), + "img": util.NewStringSet("longdesc", "src", "usemap"), + "input": util.NewStringSet("src", "usemap", "formaction"), + "ins": util.NewStringSet("cite"), + "link": util.NewStringSet("href"), + "object": util.NewStringSet("classid", "codebase", "data", "usemap"), + "q": util.NewStringSet("cite"), + "script": util.NewStringSet("src"), + "source": util.NewStringSet("src"), + "video": util.NewStringSet("poster", "src"), + + // TODO: css URLs hidden in style elements. +} + +// ProxyHandler provides a http.Handler which will proxy traffic to locations +// specified by items implementing Redirector. +type ProxyHandler struct { + prefix string + storage map[string]RESTStorage + codec Codec +} + +func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + parts := strings.SplitN(req.URL.Path, "/", 3) + if len(parts) < 2 { + notFound(w, req) + return + } + resourceName := parts[0] + id := parts[1] + rest := "" + if len(parts) == 3 { + rest = parts[2] + } + storage, ok := r.storage[resourceName] + if !ok { + httplog.LogOf(w).Addf("'%v' has no storage object", resourceName) + notFound(w, req) + return + } + + redirector, ok := storage.(Redirector) + if !ok { + httplog.LogOf(w).Addf("'%v' is not a redirector", resourceName) + notFound(w, req) + return + } + + location, err := redirector.ResourceLocation(id) + if err != nil { + status := errToAPIStatus(err) + writeJSON(status.Code, r.codec, status, w) + return + } + + destURL, err := url.Parse(location) + if err != nil { + status := errToAPIStatus(err) + writeJSON(status.Code, r.codec, status, w) + return + } + destURL.Path = rest + destURL.RawQuery = req.URL.RawQuery + newReq, err := http.NewRequest(req.Method, destURL.String(), req.Body) + if err != nil { + glog.Errorf("Failed to create request: %s", err) + } + newReq.Header = req.Header + + proxy := httputil.NewSingleHostReverseProxy(&url.URL{Scheme: "http", Host: destURL.Host}) + proxy.Transport = &proxyTransport{ + proxyScheme: req.URL.Scheme, + proxyHost: req.URL.Host, + proxyPathPrepend: path.Join(r.prefix, resourceName, id), + } + proxy.ServeHTTP(w, newReq) +} + +type proxyTransport struct { + proxyScheme string + proxyHost string + proxyPathPrepend string +} + +func (t *proxyTransport) RoundTrip(req *http.Request) (*http.Response, error) { + resp, err := http.DefaultTransport.RoundTrip(req) + + if err != nil { + message := fmt.Sprintf("Error: '%s'\nTrying to reach: '%v'", err.Error(), req.URL.String()) + resp = &http.Response{ + StatusCode: http.StatusServiceUnavailable, + Body: ioutil.NopCloser(strings.NewReader(message)), + } + return resp, nil + } + + if resp.Header.Get("Content-Type") != "text/html" { + // Do nothing, simply pass through + return resp, nil + } + + return t.fixLinks(req, resp) +} + +// updateURLs checks and updates any of n's attributes that are listed in tagsToAttrs. +// Any URLs found are, if they're relative, updated with the necessary changes to make +// a visit to that URL also go through the proxy. +// sourceURL is the URL of the page which we're currently on; it's required to make +// relative links work. +func (t *proxyTransport) updateURLs(n *html.Node, sourceURL *url.URL) { + if n.Type != html.ElementNode { + return + } + attrs, ok := tagsToAttrs[n.Data] + if !ok { + return + } + for i, attr := range n.Attr { + if !attrs.Has(attr.Key) { + continue + } + url, err := url.Parse(attr.Val) + if err != nil { + continue + } + // Is this URL relative? + if url.Host == "" { + url.Scheme = t.proxyScheme + url.Host = t.proxyHost + url.Path = path.Join(t.proxyPathPrepend, path.Dir(sourceURL.Path), url.Path, "/") + n.Attr[i].Val = url.String() + } else if url.Host == sourceURL.Host { + url.Scheme = t.proxyScheme + url.Host = t.proxyHost + url.Path = path.Join(t.proxyPathPrepend, url.Path) + n.Attr[i].Val = url.String() + } + } +} + +// scan recursively calls f for every n and every subnode of n. +func (t *proxyTransport) scan(n *html.Node, f func(*html.Node)) { + f(n) + for c := n.FirstChild; c != nil; c = c.NextSibling { + t.scan(c, f) + } +} + +// fixLinks modifies links in an HTML file such that they will be redirected through the proxy if needed. +func (t *proxyTransport) fixLinks(req *http.Request, resp *http.Response) (*http.Response, error) { + defer resp.Body.Close() + + doc, err := html.Parse(resp.Body) + if err != nil { + glog.Errorf("Parse failed: %v", err) + return resp, err + } + + newContent := &bytes.Buffer{} + t.scan(doc, func(n *html.Node) { t.updateURLs(n, req.URL) }) + if err := html.Render(newContent, doc); err != nil { + glog.Errorf("Failed to render: %v", err) + } + + resp.Body = ioutil.NopCloser(newContent) + // Update header node with new content-length + // TODO: Remove any hash/signature headers here? + resp.Header.Del("Content-Length") + resp.ContentLength = int64(newContent.Len()) + + return resp, err +} diff --git a/pkg/apiserver/proxy_test.go b/pkg/apiserver/proxy_test.go new file mode 100644 index 00000000000..b50eb67264d --- /dev/null +++ b/pkg/apiserver/proxy_test.go @@ -0,0 +1,190 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package apiserver + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "code.google.com/p/go.net/html" +) + +func parseURLOrDie(inURL string) *url.URL { + parsed, err := url.Parse(inURL) + if err != nil { + panic(err) + } + return parsed +} + +// fmtHTML parses and re-emits 'in', effectively canonicalizing it. +func fmtHTML(in string) string { + doc, err := html.Parse(strings.NewReader(in)) + if err != nil { + panic(err) + } + out := &bytes.Buffer{} + if err := html.Render(out, doc); err != nil { + panic(err) + } + return string(out.Bytes()) +} + +func TestProxyTransport_fixLinks(t *testing.T) { + testTransport := &proxyTransport{ + proxyScheme: "http", + proxyHost: "foo.com", + proxyPathPrepend: "/proxy/minion/minion1:10250/", + } + testTransport2 := &proxyTransport{ + proxyScheme: "https", + proxyHost: "foo.com", + proxyPathPrepend: "/proxy/minion/minion1:8080/", + } + + table := map[string]struct { + input string + sourceURL string + transport *proxyTransport + output string + }{ + "normal": { + input: `
kubelet.loggoogle.log`, + sourceURL: "http://myminion.com/logs/log.log", + transport: testTransport, + output: `
kubelet.loggoogle.log`, + }, + "subdir": { + input: `kubelet.loggoogle.log`, + sourceURL: "http://myminion.com/whatever/apt/somelog.log", + transport: testTransport2, + output: `kubelet.loggoogle.log`, + }, + "image": { + input: `
`, + sourceURL: "http://myminion.com/", + transport: testTransport, + output: `
`, + }, + "abs": { + input: ``, + sourceURL: "http://myminion.com/any/path/", + transport: testTransport, + output: ``, + }, + "abs but same host": { + input: ``, + sourceURL: "http://myminion.com/any/path/", + transport: testTransport, + output: ``, + }, + } + + for name, item := range table { + // Canonicalize the html so we can diff. + item.input = fmtHTML(item.input) + item.output = fmtHTML(item.output) + req := &http.Request{ + Method: "GET", + URL: parseURLOrDie(item.sourceURL), + } + resp := &http.Response{ + Status: "200 OK", + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(strings.NewReader(item.input)), + Close: true, + } + updatedResp, err := item.transport.fixLinks(req, resp) + if err != nil { + t.Errorf("%v: Unexpected error: %v", name, err) + continue + } + body, err := ioutil.ReadAll(updatedResp.Body) + if err != nil { + t.Errorf("%v: Unexpected error: %v", name, err) + continue + } + if e, a := item.output, string(body); e != a { + t.Errorf("%v: expected %v, but got %v", name, e, a) + } + } +} + +func TestProxy(t *testing.T) { + table := []struct { + method string + path string + reqBody string + respBody string + }{ + {"GET", "/some/dir", "", "answer"}, + {"POST", "/some/other/dir", "question", "answer"}, + {"PUT", "/some/dir/id", "different question", "answer"}, + {"DELETE", "/some/dir/id", "", "ok"}, + } + + for _, item := range table { + proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + gotBody, err := ioutil.ReadAll(req.Body) + if err != nil { + t.Errorf("%v - unexpected error %v", item.method, err) + } + if e, a := item.reqBody, string(gotBody); e != a { + t.Errorf("%v - expected %v, got %v", item.method, e, a) + } + fmt.Fprint(w, item.respBody) + })) + + simpleStorage := &SimpleRESTStorage{ + errors: map[string]error{}, + resourceLocation: proxyServer.URL, + } + handler := Handle(map[string]RESTStorage{ + "foo": simpleStorage, + }, codec, "/prefix/version") + server := httptest.NewServer(handler) + + req, err := http.NewRequest( + item.method, + server.URL+"/prefix/version/proxy/foo/id"+item.path, + strings.NewReader(item.reqBody), + ) + if err != nil { + t.Errorf("%v - unexpected error %v", item.method, err) + continue + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Errorf("%v - unexpected error %v", item.method, err) + continue + } + gotResp, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Errorf("%v - unexpected error %v", item.method, err) + } + resp.Body.Close() + if e, a := item.respBody, string(gotResp); e != a { + t.Errorf("%v - expected %v, got %v", item.method, e, a) + } + } +} diff --git a/pkg/apiserver/redirect_test.go b/pkg/apiserver/redirect_test.go index aa780e6e60c..6425f920d8c 100644 --- a/pkg/apiserver/redirect_test.go +++ b/pkg/apiserver/redirect_test.go @@ -51,6 +51,7 @@ func TestRedirect(t *testing.T) { for _, item := range table { simpleStorage.errors["resourceLocation"] = item.err + simpleStorage.resourceLocation = item.id resp, err := client.Get(server.URL + "/prefix/version/redirect/foo/" + item.id) if resp == nil { t.Fatalf("Unexpected nil resp") diff --git a/pkg/registry/service/storage.go b/pkg/registry/service/storage.go index 579104ff2f3..8b02b629155 100644 --- a/pkg/registry/service/storage.go +++ b/pkg/registry/service/storage.go @@ -180,7 +180,7 @@ func (rs *RegistryStorage) ResourceLocation(id string) (string, error) { if len(e.Endpoints) == 0 { return "", fmt.Errorf("no endpoints available for %v", id) } - return e.Endpoints[rand.Intn(len(e.Endpoints))], nil + return "http://" + e.Endpoints[rand.Intn(len(e.Endpoints))], nil } func (rs *RegistryStorage) deleteExternalLoadBalancer(service *api.Service) error { diff --git a/pkg/registry/service/storage_test.go b/pkg/registry/service/storage_test.go index 4cda3645b27..f4945493be2 100644 --- a/pkg/registry/service/storage_test.go +++ b/pkg/registry/service/storage_test.go @@ -291,7 +291,7 @@ func TestServiceRegistryResourceLocation(t *testing.T) { if err != nil { t.Errorf("Unexpected error: %v", err) } - if e, a := "foo:80", location; e != a { + if e, a := "http://foo:80", location; e != a { t.Errorf("Expected %v, but got %v", e, a) } if e, a := "foo", registry.GottenID; e != a {