mirror of https://github.com/k3s-io/k3s
399 lines
12 KiB
Go
399 lines
12 KiB
Go
/*
|
|
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"
|
|
"compress/gzip"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
|
|
"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"
|
|
|
|
"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 {
|
|
prefix string
|
|
storage map[string]RESTStorage
|
|
codec runtime.Codec
|
|
context api.RequestContextMapper
|
|
apiRequestInfoResolver *APIRequestInfoResolver
|
|
}
|
|
|
|
func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
var verb string
|
|
var apiResource string
|
|
var httpCode int
|
|
reqStart := time.Now()
|
|
defer monitor("proxy", &verb, &apiResource, &httpCode, reqStart)
|
|
|
|
requestInfo, err := r.apiRequestInfoResolver.GetAPIRequestInfo(req)
|
|
if err != nil {
|
|
notFound(w, req)
|
|
httpCode = http.StatusNotFound
|
|
return
|
|
}
|
|
verb = requestInfo.Verb
|
|
namespace, resource, parts := requestInfo.Namespace, requestInfo.Resource, requestInfo.Parts
|
|
|
|
ctx, ok := r.context.Get(req)
|
|
if !ok {
|
|
ctx = api.NewContext()
|
|
}
|
|
ctx = api.WithNamespace(ctx, namespace)
|
|
if len(parts) < 2 {
|
|
notFound(w, req)
|
|
httpCode = http.StatusNotFound
|
|
return
|
|
}
|
|
id := parts[1]
|
|
rest := ""
|
|
if len(parts) > 2 {
|
|
proxyParts := parts[2:]
|
|
rest = strings.Join(proxyParts, "/")
|
|
if strings.HasSuffix(req.URL.Path, "/") {
|
|
// The original path had a trailing slash, which has been stripped
|
|
// by KindAndNamespace(). We should add it back because some
|
|
// servers (like etcd) require it.
|
|
rest = rest + "/"
|
|
}
|
|
}
|
|
storage, ok := r.storage[resource]
|
|
if !ok {
|
|
httplog.LogOf(req, w).Addf("'%v' has no storage object", resource)
|
|
notFound(w, req)
|
|
httpCode = http.StatusNotFound
|
|
return
|
|
}
|
|
apiResource = resource
|
|
|
|
redirector, ok := storage.(Redirector)
|
|
if !ok {
|
|
httplog.LogOf(req, w).Addf("'%v' is not a redirector", resource)
|
|
httpCode = errorJSON(errors.NewMethodNotSupported(resource, "proxy"), r.codec, w)
|
|
return
|
|
}
|
|
|
|
location, err := redirector.ResourceLocation(ctx, id)
|
|
if err != nil {
|
|
httplog.LogOf(req, w).Addf("Error getting ResourceLocation: %v", err)
|
|
status := errToAPIStatus(err)
|
|
writeJSON(status.Code, r.codec, status, w)
|
|
httpCode = status.Code
|
|
return
|
|
}
|
|
if location == "" {
|
|
httplog.LogOf(req, w).Addf("ResourceLocation for %v returned ''", id)
|
|
notFound(w, req)
|
|
httpCode = http.StatusNotFound
|
|
return
|
|
}
|
|
|
|
destURL, err := url.Parse(location)
|
|
if err != nil {
|
|
status := errToAPIStatus(err)
|
|
writeJSON(status.Code, r.codec, status, w)
|
|
httpCode = status.Code
|
|
return
|
|
}
|
|
if destURL.Scheme == "" {
|
|
// If no scheme was present in location, url.Parse sometimes mistakes
|
|
// hosts for paths.
|
|
destURL.Host = location
|
|
}
|
|
destURL.Path = rest
|
|
destURL.RawQuery = req.URL.RawQuery
|
|
newReq, err := http.NewRequest(req.Method, destURL.String(), req.Body)
|
|
if err != nil {
|
|
status := errToAPIStatus(err)
|
|
writeJSON(status.Code, r.codec, status, w)
|
|
notFound(w, req)
|
|
httpCode = status.Code
|
|
return
|
|
}
|
|
httpCode = http.StatusOK
|
|
newReq.Header = req.Header
|
|
|
|
// TODO convert this entire proxy to an UpgradeAwareProxy similar to
|
|
// https://github.com/openshift/origin/blob/master/pkg/util/httpproxy/upgradeawareproxy.go.
|
|
// That proxy needs to be modified to support multiple backends, not just 1.
|
|
if r.tryUpgrade(w, req, newReq, destURL) {
|
|
return
|
|
}
|
|
|
|
proxy := httputil.NewSingleHostReverseProxy(&url.URL{Scheme: "http", Host: destURL.Host})
|
|
proxy.Transport = &proxyTransport{
|
|
proxyScheme: req.URL.Scheme,
|
|
proxyHost: req.URL.Host,
|
|
proxyPathPrepend: requestInfo.URLPath(),
|
|
}
|
|
proxy.FlushInterval = 200 * time.Millisecond
|
|
proxy.ServeHTTP(w, newReq)
|
|
}
|
|
|
|
// tryUpgrade returns true if the request was handled.
|
|
func (r *ProxyHandler) tryUpgrade(w http.ResponseWriter, req, newReq *http.Request, destURL *url.URL) bool {
|
|
connectionHeader := strings.ToLower(req.Header.Get(httpstream.HeaderConnection))
|
|
if !strings.Contains(connectionHeader, strings.ToLower(httpstream.HeaderUpgrade)) || len(req.Header.Get(httpstream.HeaderUpgrade)) == 0 {
|
|
return false
|
|
}
|
|
//TODO support TLS? Doesn't look like proxyTransport does anything special ...
|
|
dialAddr := netutil.CanonicalAddr(destURL)
|
|
backendConn, err := net.Dial("tcp", dialAddr)
|
|
if err != nil {
|
|
status := errToAPIStatus(err)
|
|
writeJSON(status.Code, r.codec, status, w)
|
|
return true
|
|
}
|
|
defer backendConn.Close()
|
|
|
|
// TODO should we use _ (a bufio.ReadWriter) instead of requestHijackedConn
|
|
// when copying between the client and the backend? Docker doesn't when they
|
|
// hijack, just for reference...
|
|
requestHijackedConn, _, err := w.(http.Hijacker).Hijack()
|
|
if err != nil {
|
|
status := errToAPIStatus(err)
|
|
writeJSON(status.Code, r.codec, status, w)
|
|
return true
|
|
}
|
|
defer requestHijackedConn.Close()
|
|
|
|
if err = newReq.Write(backendConn); err != nil {
|
|
status := errToAPIStatus(err)
|
|
writeJSON(status.Code, r.codec, status, w)
|
|
return true
|
|
}
|
|
|
|
done := make(chan struct{}, 2)
|
|
|
|
go func() {
|
|
_, err := io.Copy(backendConn, requestHijackedConn)
|
|
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
|
|
glog.Errorf("Error proxying data from client to backend: %v", err)
|
|
}
|
|
done <- struct{}{}
|
|
}()
|
|
|
|
go func() {
|
|
_, err := io.Copy(requestHijackedConn, backendConn)
|
|
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
|
|
glog.Errorf("Error proxying data from backend to client: %v", err)
|
|
}
|
|
done <- struct{}{}
|
|
}()
|
|
|
|
<-done
|
|
return true
|
|
}
|
|
|
|
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
|
|
}
|