From a3f5dfd0e21b6b474552d303999a6ffc05f93d19 Mon Sep 17 00:00:00 2001 From: Cesar Wong Date: Tue, 14 Apr 2015 10:51:43 -0400 Subject: [PATCH 1/4] Move proxy html transport to utility package Moves the proxy html transport that translates html links from the proxy in apiserver to its own package under util. --- pkg/apiserver/proxy.go | 193 +------------------------- pkg/apiserver/proxy_test.go | 199 --------------------------- pkg/util/proxy/doc.go | 18 +++ pkg/util/proxy/transport.go | 217 +++++++++++++++++++++++++++++ pkg/util/proxy/transport_test.go | 227 +++++++++++++++++++++++++++++++ 5 files changed, 467 insertions(+), 387 deletions(-) create mode 100644 pkg/util/proxy/doc.go create mode 100644 pkg/util/proxy/transport.go create mode 100644 pkg/util/proxy/transport_test.go diff --git a/pkg/apiserver/proxy.go b/pkg/apiserver/proxy.go index 83660205924..5d6fe003fe1 100644 --- a/pkg/apiserver/proxy.go +++ b/pkg/apiserver/proxy.go @@ -17,12 +17,9 @@ limitations under the License. package apiserver import ( - "bytes" - "compress/gzip" "crypto/tls" "fmt" "io" - "io/ioutil" "net" "net/http" "net/http/httputil" @@ -36,47 +33,13 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest" "github.com/GoogleCloudPlatform/kubernetes/pkg/httplog" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" - "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/util/httpstream" + proxyutil "github.com/GoogleCloudPlatform/kubernetes/pkg/util/proxy" "github.com/GoogleCloudPlatform/kubernetes/third_party/golang/netutil" "github.com/golang/glog" - "golang.org/x/net/html" ) -// 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 { @@ -206,10 +169,10 @@ func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { if len(namespace) > 0 { prepend = path.Join(r.prefix, "namespaces", namespace, resource, id) } - transport = &proxyTransport{ - proxyScheme: req.URL.Scheme, - proxyHost: req.URL.Host, - proxyPathPrepend: prepend, + transport = &proxyutil.Transport{ + Scheme: req.URL.Scheme, + Host: req.URL.Host, + PathPrepend: prepend, } } proxy.Transport = transport @@ -317,149 +280,3 @@ func singleJoiningSlash(a, b string) string { } return a + b } - -type proxyTransport struct { - proxyScheme string - proxyHost string - proxyPathPrepend string -} - -func (t *proxyTransport) RoundTrip(req *http.Request) (*http.Response, error) { - // Add reverse proxy headers. - req.Header.Set("X-Forwarded-Uri", t.proxyPathPrepend+req.URL.Path) - req.Header.Set("X-Forwarded-Host", t.proxyHost) - req.Header.Set("X-Forwarded-Proto", t.proxyScheme) - - 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 redirect := resp.Header.Get("Location"); redirect != "" { - resp.Header.Set("Location", t.rewriteURL(redirect, req.URL)) - } - - cType := resp.Header.Get("Content-Type") - cType = strings.TrimSpace(strings.SplitN(cType, ";", 2)[0]) - if cType != "text/html" { - // Do nothing, simply pass through - return resp, nil - } - - return t.fixLinks(req, resp) -} - -// rewriteURL rewrites a single URL to go through the proxy, if the URL refers -// to the same host as sourceURL, which is the page on which the target URL -// occurred. If any error occurs (e.g. parsing), it returns targetURL. -func (t *proxyTransport) rewriteURL(targetURL string, sourceURL *url.URL) string { - url, err := url.Parse(targetURL) - if err != nil { - return targetURL - } - if url.Host != "" && url.Host != sourceURL.Host { - return targetURL - } - - url.Scheme = t.proxyScheme - url.Host = t.proxyHost - origPath := url.Path - - if strings.HasPrefix(url.Path, "/") { - // The path is rooted at the host. Just add proxy prepend. - url.Path = path.Join(t.proxyPathPrepend, url.Path) - } else { - // The path is relative to sourceURL. - url.Path = path.Join(t.proxyPathPrepend, path.Dir(sourceURL.Path), url.Path) - } - - if strings.HasSuffix(origPath, "/") { - // Add back the trailing slash, which was stripped by path.Join(). - url.Path += "/" - } - - return url.String() -} - -// 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 - } - n.Attr[i].Val = t.rewriteURL(attr.Val, sourceURL) - } -} - -// 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) { - origBody := resp.Body - defer origBody.Close() - - newContent := &bytes.Buffer{} - var reader io.Reader = origBody - var writer io.Writer = newContent - encoding := resp.Header.Get("Content-Encoding") - switch encoding { - case "gzip": - var err error - reader, err = gzip.NewReader(reader) - if err != nil { - return nil, fmt.Errorf("errorf making gzip reader: %v", err) - } - gzw := gzip.NewWriter(writer) - defer gzw.Close() - writer = gzw - // TODO: support flate, other encodings. - case "": - // This is fine - default: - // Some encoding we don't understand-- don't try to parse this - glog.Errorf("Proxy encountered encoding %v for text/html; can't understand this so not fixing links.", encoding) - return resp, nil - } - - doc, err := html.Parse(reader) - if err != nil { - glog.Errorf("Parse failed: %v", err) - return resp, err - } - - t.scan(doc, func(n *html.Node) { t.updateURLs(n, req.URL) }) - if err := html.Render(writer, 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 index 15d66a65e41..eeff34fc9cf 100644 --- a/pkg/apiserver/proxy_test.go +++ b/pkg/apiserver/proxy_test.go @@ -17,7 +17,6 @@ limitations under the License. package apiserver import ( - "bytes" "compress/gzip" "fmt" "io" @@ -29,207 +28,9 @@ import ( "testing" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest" - "golang.org/x/net/html" "golang.org/x/net/websocket" ) -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(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", - } - type Item struct { - input string - sourceURL string - transport *proxyTransport - output string - contentType string - forwardedURI string - redirect string - redirectWant string - } - - table := map[string]Item{ - "normal": { - input: `
kubelet.loggoogle.log
`, - sourceURL: "http://myminion.com/logs/log.log", - transport: testTransport, - output: `
kubelet.loggoogle.log
`, - contentType: "text/html", - forwardedURI: "/proxy/minion/minion1:10250/logs/log.log", - }, - "trailing slash": { - input: `
kubelet.loggoogle.log
`, - sourceURL: "http://myminion.com/logs/log.log", - transport: testTransport, - output: `
kubelet.loggoogle.log
`, - contentType: "text/html", - forwardedURI: "/proxy/minion/minion1:10250/logs/log.log", - }, - "content-type charset": { - input: `
kubelet.loggoogle.log
`, - sourceURL: "http://myminion.com/logs/log.log", - transport: testTransport, - output: `
kubelet.loggoogle.log
`, - contentType: "text/html; charset=utf-8", - forwardedURI: "/proxy/minion/minion1:10250/logs/log.log", - }, - "content-type passthrough": { - input: `
kubelet.loggoogle.log
`, - sourceURL: "http://myminion.com/logs/log.log", - transport: testTransport, - output: `
kubelet.loggoogle.log
`, - contentType: "text/plain", - forwardedURI: "/proxy/minion/minion1:10250/logs/log.log", - }, - "subdir": { - input: `kubelet.loggoogle.log`, - sourceURL: "http://myminion.com/whatever/apt/somelog.log", - transport: testTransport2, - output: `kubelet.loggoogle.log`, - contentType: "text/html", - forwardedURI: "/proxy/minion/minion1:8080/whatever/apt/somelog.log", - }, - "image": { - input: `
`, - sourceURL: "http://myminion.com/", - transport: testTransport, - output: `
`, - contentType: "text/html", - forwardedURI: "/proxy/minion/minion1:10250/", - }, - "abs": { - input: `