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  }