Change:
The ApiServer proxy wraps partial HTML responses to serve a complete HTML document in the proxy response. This is problematic for applications which expect HTML fragments, such as with AngularJS templates. Example: This is the response for a from the unproxied server: But the proxied response is: This issue is blocking

Discussion:
"github.com/golang/glog" "golang.org/x/net/html" <ins> "golang.org/x/net/html/atom" </ins> "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) <del> // tagsToAttrs states which attributes of which tags require URL substitution. </del> <ins> // atomsToAttrs states which attributes of which tags require URL substitution. </ins> // Sources: http://www.w3.org/TR/REC-html40/index/attributes.html //          http://www.w3.org/html/wg/drafts/html/master/index.html#attributes-1 <del> 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"), </del> <ins> var atomsToAttrs = map[atom.Atom]util.StringSet{ atom.A:          util.NewStringSet("href"), atom.Applet:     util.NewStringSet("codebase"), atom.Area:       util.NewStringSet("href"), atom.Audio:      util.NewStringSet("src"), atom.Base:       util.NewStringSet("href"), atom.Blockquote: util.NewStringSet("cite"), atom.Body:       util.NewStringSet("background"), atom.Button:     util.NewStringSet("formaction"), atom.Command:    util.NewStringSet("icon"), atom.Del:        util.NewStringSet("cite"), atom.Embed:      util.NewStringSet("src"), atom.Form:       util.NewStringSet("action"), atom.Frame:      util.NewStringSet("longdesc", "src"), atom.Head:       util.NewStringSet("profile"), atom.Html:       util.NewStringSet("manifest"), atom.Iframe:     util.NewStringSet("longdesc", "src"), atom.Img:        util.NewStringSet("longdesc", "src", "usemap"), atom.Input:      util.NewStringSet("src", "usemap", "formaction"), atom.Ins:        util.NewStringSet("cite"), atom.Link:       util.NewStringSet("href"), atom.Object:     util.NewStringSet("classid", "codebase", "data", "usemap"), atom.Q:          util.NewStringSet("cite"), atom.Script:     util.NewStringSet("src"), atom.Source:     util.NewStringSet("src"), atom.Video:      util.NewStringSet("poster", "src"), </ins> // TODO: css URLs hidden in style elements. }

return resp, nil } <del> return t.fixLinks(req, resp) </del> <ins> return t.rewriteResponse(req, resp) </ins> } // rewriteURL rewrites a single URL to go through the proxy, if the URL refers

return url.String() } <del> // updateURLs checks and updates any of n's attributes that are listed in tagsToAttrs. // Any URLs found are, if they share the source host, 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. func (t *Transport) 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 </del> <ins> // rewriteHTML scans the HTML for tags with url-valued attributes, and updates // those values with the urlRewriter function. The updated HTML is output to the // writer. func rewriteHTML(reader io.Reader, writer io.Writer, urlRewriter func(string) string) error { // Note: This assumes the content is UTF-8. tokenizer := html.NewTokenizer(reader) var err error for err == nil { tokenType := tokenizer.Next() switch tokenType { case html.ErrorToken: err = tokenizer.Err() case html.StartTagToken, html.SelfClosingTagToken: token := tokenizer.Token() if urlAttrs, ok := atomsToAttrs[token.DataAtom]; ok { for i, attr := range token.Attr { if urlAttrs.Has(attr.Key) { token.Attr[i].Val = urlRewriter(attr.Val) } } } _, err = writer.Write([]byte(token.String())) default: _, err = writer.Write(tokenizer.Raw()) </ins> } <del> n.Attr[i].Val = t.rewriteURL(attr.Val, sourceURL) </del> } <del> } // scan recursively calls f for every n and every subnode of n. func (t *Transport) scan(n *html.Node, f func(*html.Node)) { f(n) for c := n.FirstChild; c != nil; c = c.NextSibling { t.scan(c, f) </del> <ins> if err != io.EOF { return err </ins> } <ins> return nil </ins> } <del> // fixLinks modifies links in an HTML file such that they will be redirected through the proxy if needed. func (t *Transport) fixLinks(req *http.Request, resp *http.Response) (*http.Response, error) { </del> <ins> // rewriteResponse modifies an HTML response by updating absolute links refering // to the original host to instead refer to the proxy transport. func (t *Transport) rewriteResponse(req *http.Request, resp *http.Response) (*http.Response, error) { </ins> origBody := resp.Body defer origBody.Close()

return resp, nil } <del> doc, err := html.Parse(reader) </del> <ins> urlRewriter := func(targetUrl string) string { return t.rewriteURL(targetUrl, req.URL) } err := rewriteHTML(reader, writer, urlRewriter) </ins> if err != nil { <del> glog.Errorf("Parse failed: %v", err) </del> <ins> glog.Errorf("Failed to rewrite URLs: %v", err) </ins> return resp, err } <del> 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) } </del> resp.Body = ioutil.NopCloser(newContent) // Update header node with new content-length // TODO: Remove any hash/signature headers here?

package proxy import ( <del> "bytes" </del> "fmt" "io/ioutil" "net/http"

"net/url" "strings" "testing" <del> "golang.org/x/net/html" </del> ) func parseURLOrDie(inURL string) *url.URL {

return parsed } <del> // 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()) } </del> func TestProxyTransport(t *testing.T) { testTransport := &Transport{ Scheme:      "http",

contentType:  "text/html", forwardedURI: "/proxy/minion/minion1:10250/logs/log.log", }, <ins> "full document": { input:        `<html><header></header><body><pre><a href="kubelet.log">kubelet.log</a><a href="/google.log">google.log</a></pre></body></html>`, sourceURL:    "http://myminion.com/logs/log.log", transport:    testTransport, output:       `<html><header></header><body><pre><a href="kubelet.log">kubelet.log</a><a href="http://foo.com/proxy/minion/minion1:10250/google.log">google.log</a></pre></body></html>`, contentType:  "text/html", forwardedURI: "/proxy/minion/minion1:10250/logs/log.log", }, </ins> "trailing slash": { input:        `<pre><a href="kubelet.log">kubelet.log</a><a href="/google.log/">google.log</a></pre>`, sourceURL:    "http://myminion.com/logs/log.log",

} testItem := func(name string, item *Item) { <del> // Canonicalize the html so we can diff. item.input = fmtHTML(item.input) item.output = fmtHTML(item.output) </del> server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check request headers. if got, want := r.Header.Get("X-Forwarded-Uri"), item.forwardedURI; got != want {