github.com/spotmaxtech/k8s-apimachinery-v0260@v0.0.1/pkg/util/proxy/transport.go (about) 1 /* 2 Copyright 2014 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package proxy 18 19 import ( 20 "bytes" 21 "compress/flate" 22 "compress/gzip" 23 "fmt" 24 "io" 25 "io/ioutil" 26 "net/http" 27 "net/url" 28 "path" 29 "strings" 30 31 "golang.org/x/net/html" 32 "golang.org/x/net/html/atom" 33 "k8s.io/klog/v2" 34 35 "github.com/spotmaxtech/k8s-apimachinery-v0260/pkg/api/errors" 36 "github.com/spotmaxtech/k8s-apimachinery-v0260/pkg/util/net" 37 "github.com/spotmaxtech/k8s-apimachinery-v0260/pkg/util/sets" 38 ) 39 40 // atomsToAttrs states which attributes of which tags require URL substitution. 41 // Sources: http://www.w3.org/TR/REC-html40/index/attributes.html 42 // 43 // http://www.w3.org/html/wg/drafts/html/master/index.html#attributes-1 44 var atomsToAttrs = map[atom.Atom]sets.String{ 45 atom.A: sets.NewString("href"), 46 atom.Applet: sets.NewString("codebase"), 47 atom.Area: sets.NewString("href"), 48 atom.Audio: sets.NewString("src"), 49 atom.Base: sets.NewString("href"), 50 atom.Blockquote: sets.NewString("cite"), 51 atom.Body: sets.NewString("background"), 52 atom.Button: sets.NewString("formaction"), 53 atom.Command: sets.NewString("icon"), 54 atom.Del: sets.NewString("cite"), 55 atom.Embed: sets.NewString("src"), 56 atom.Form: sets.NewString("action"), 57 atom.Frame: sets.NewString("longdesc", "src"), 58 atom.Head: sets.NewString("profile"), 59 atom.Html: sets.NewString("manifest"), 60 atom.Iframe: sets.NewString("longdesc", "src"), 61 atom.Img: sets.NewString("longdesc", "src", "usemap"), 62 atom.Input: sets.NewString("src", "usemap", "formaction"), 63 atom.Ins: sets.NewString("cite"), 64 atom.Link: sets.NewString("href"), 65 atom.Object: sets.NewString("classid", "codebase", "data", "usemap"), 66 atom.Q: sets.NewString("cite"), 67 atom.Script: sets.NewString("src"), 68 atom.Source: sets.NewString("src"), 69 atom.Video: sets.NewString("poster", "src"), 70 71 // TODO: css URLs hidden in style elements. 72 } 73 74 // Transport is a transport for text/html content that replaces URLs in html 75 // content with the prefix of the proxy server 76 type Transport struct { 77 Scheme string 78 Host string 79 PathPrepend string 80 81 http.RoundTripper 82 } 83 84 // RoundTrip implements the http.RoundTripper interface 85 func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { 86 // Add reverse proxy headers. 87 forwardedURI := path.Join(t.PathPrepend, req.URL.EscapedPath()) 88 if strings.HasSuffix(req.URL.Path, "/") { 89 forwardedURI = forwardedURI + "/" 90 } 91 req.Header.Set("X-Forwarded-Uri", forwardedURI) 92 if len(t.Host) > 0 { 93 req.Header.Set("X-Forwarded-Host", t.Host) 94 } 95 if len(t.Scheme) > 0 { 96 req.Header.Set("X-Forwarded-Proto", t.Scheme) 97 } 98 99 rt := t.RoundTripper 100 if rt == nil { 101 rt = http.DefaultTransport 102 } 103 resp, err := rt.RoundTrip(req) 104 105 if err != nil { 106 return nil, errors.NewServiceUnavailable(fmt.Sprintf("error trying to reach service: %v", err)) 107 } 108 109 if redirect := resp.Header.Get("Location"); redirect != "" { 110 targetURL, err := url.Parse(redirect) 111 if err != nil { 112 return nil, errors.NewInternalError(fmt.Errorf("error trying to parse Location header: %v", err)) 113 } 114 resp.Header.Set("Location", t.rewriteURL(targetURL, req.URL, req.Host)) 115 return resp, nil 116 } 117 118 cType := resp.Header.Get("Content-Type") 119 cType = strings.TrimSpace(strings.SplitN(cType, ";", 2)[0]) 120 if cType != "text/html" { 121 // Do nothing, simply pass through 122 return resp, nil 123 } 124 125 return t.rewriteResponse(req, resp) 126 } 127 128 var _ = net.RoundTripperWrapper(&Transport{}) 129 130 func (rt *Transport) WrappedRoundTripper() http.RoundTripper { 131 return rt.RoundTripper 132 } 133 134 // rewriteURL rewrites a single URL to go through the proxy, if the URL refers 135 // to the same host as sourceURL, which is the page on which the target URL 136 // occurred, or if the URL matches the sourceRequestHost. 137 func (t *Transport) rewriteURL(url *url.URL, sourceURL *url.URL, sourceRequestHost string) string { 138 // Example: 139 // When API server processes a proxy request to a service (e.g. /api/v1/namespace/foo/service/bar/proxy/), 140 // the sourceURL.Host (i.e. req.URL.Host) is the endpoint IP address of the service. The 141 // sourceRequestHost (i.e. req.Host) is the Host header that specifies the host on which the 142 // URL is sought, which can be different from sourceURL.Host. For example, if user sends the 143 // request through "kubectl proxy" locally (i.e. localhost:8001/api/v1/namespace/foo/service/bar/proxy/), 144 // sourceRequestHost is "localhost:8001". 145 // 146 // If the service's response URL contains non-empty host, and url.Host is equal to either sourceURL.Host 147 // or sourceRequestHost, we should not consider the returned URL to be a completely different host. 148 // It's the API server's responsibility to rewrite a same-host-and-absolute-path URL and append the 149 // necessary URL prefix (i.e. /api/v1/namespace/foo/service/bar/proxy/). 150 isDifferentHost := url.Host != "" && url.Host != sourceURL.Host && url.Host != sourceRequestHost 151 isRelative := !strings.HasPrefix(url.Path, "/") 152 if isDifferentHost || isRelative { 153 return url.String() 154 } 155 156 // Do not rewrite scheme and host if the Transport has empty scheme and host 157 // when targetURL already contains the sourceRequestHost 158 if !(url.Host == sourceRequestHost && t.Scheme == "" && t.Host == "") { 159 url.Scheme = t.Scheme 160 url.Host = t.Host 161 } 162 163 origPath := url.Path 164 // Do not rewrite URL if the sourceURL already contains the necessary prefix. 165 if strings.HasPrefix(url.Path, t.PathPrepend) { 166 return url.String() 167 } 168 url.Path = path.Join(t.PathPrepend, url.Path) 169 if strings.HasSuffix(origPath, "/") { 170 // Add back the trailing slash, which was stripped by path.Join(). 171 url.Path += "/" 172 } 173 174 return url.String() 175 } 176 177 // rewriteHTML scans the HTML for tags with url-valued attributes, and updates 178 // those values with the urlRewriter function. The updated HTML is output to the 179 // writer. 180 func rewriteHTML(reader io.Reader, writer io.Writer, urlRewriter func(*url.URL) string) error { 181 // Note: This assumes the content is UTF-8. 182 tokenizer := html.NewTokenizer(reader) 183 184 var err error 185 for err == nil { 186 tokenType := tokenizer.Next() 187 switch tokenType { 188 case html.ErrorToken: 189 err = tokenizer.Err() 190 case html.StartTagToken, html.SelfClosingTagToken: 191 token := tokenizer.Token() 192 if urlAttrs, ok := atomsToAttrs[token.DataAtom]; ok { 193 for i, attr := range token.Attr { 194 if urlAttrs.Has(attr.Key) { 195 url, err := url.Parse(attr.Val) 196 if err != nil { 197 // Do not rewrite the URL if it isn't valid. It is intended not 198 // to error here to prevent the inability to understand the 199 // content of the body to cause a fatal error. 200 continue 201 } 202 token.Attr[i].Val = urlRewriter(url) 203 } 204 } 205 } 206 _, err = writer.Write([]byte(token.String())) 207 default: 208 _, err = writer.Write(tokenizer.Raw()) 209 } 210 } 211 if err != io.EOF { 212 return err 213 } 214 return nil 215 } 216 217 // rewriteResponse modifies an HTML response by updating absolute links referring 218 // to the original host to instead refer to the proxy transport. 219 func (t *Transport) rewriteResponse(req *http.Request, resp *http.Response) (*http.Response, error) { 220 origBody := resp.Body 221 defer origBody.Close() 222 223 newContent := &bytes.Buffer{} 224 var reader io.Reader = origBody 225 var writer io.Writer = newContent 226 encoding := resp.Header.Get("Content-Encoding") 227 switch encoding { 228 case "gzip": 229 var err error 230 reader, err = gzip.NewReader(reader) 231 if err != nil { 232 return nil, fmt.Errorf("errorf making gzip reader: %v", err) 233 } 234 gzw := gzip.NewWriter(writer) 235 defer gzw.Close() 236 writer = gzw 237 case "deflate": 238 var err error 239 reader = flate.NewReader(reader) 240 flw, err := flate.NewWriter(writer, flate.BestCompression) 241 if err != nil { 242 return nil, fmt.Errorf("errorf making flate writer: %v", err) 243 } 244 defer func() { 245 flw.Close() 246 flw.Flush() 247 }() 248 writer = flw 249 case "": 250 // This is fine 251 default: 252 // Some encoding we don't understand-- don't try to parse this 253 klog.Errorf("Proxy encountered encoding %v for text/html; can't understand this so not fixing links.", encoding) 254 return resp, nil 255 } 256 257 urlRewriter := func(targetUrl *url.URL) string { 258 return t.rewriteURL(targetUrl, req.URL, req.Host) 259 } 260 err := rewriteHTML(reader, writer, urlRewriter) 261 if err != nil { 262 klog.Errorf("Failed to rewrite URLs: %v", err) 263 return resp, err 264 } 265 266 resp.Body = ioutil.NopCloser(newContent) 267 // Update header node with new content-length 268 // TODO: Remove any hash/signature headers here? 269 resp.Header.Del("Content-Length") 270 resp.ContentLength = int64(newContent.Len()) 271 272 return resp, err 273 }