Browse code

Add upgrade-aware HTTP proxy

Andy Goldstein authored on 2015/02/05 06:29:13
Showing 3 changed files
... ...
@@ -1,12 +1,12 @@
1 1
 package kubernetes
2 2
 
3 3
 import (
4
-	"net/http/httputil"
5 4
 	"net/url"
6 5
 
7 6
 	kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client"
8 7
 	restful "github.com/emicklei/go-restful"
9 8
 	"github.com/golang/glog"
9
+	"github.com/openshift/origin/pkg/util/httpproxy"
10 10
 )
11 11
 
12 12
 type ProxyConfig struct {
... ...
@@ -15,12 +15,11 @@ type ProxyConfig struct {
15 15
 }
16 16
 
17 17
 func (c *ProxyConfig) InstallAPI(container *restful.Container) []string {
18
-	transport, err := kclient.TransportFor(c.ClientConfig)
18
+	proxy, err := httpproxy.NewUpgradeAwareSingleHostReverseProxy(c.ClientConfig, c.KubernetesAddr)
19 19
 	if err != nil {
20 20
 		glog.Fatalf("Unable to initialize the Kubernetes proxy: %v", err)
21 21
 	}
22
-	proxy := httputil.NewSingleHostReverseProxy(c.KubernetesAddr)
23
-	proxy.Transport = transport
22
+
24 23
 	container.Handle("/api/", proxy)
25 24
 
26 25
 	return []string{
27 26
new file mode 100644
... ...
@@ -0,0 +1,2 @@
0
+// package httpproxy contains an upgrade-aware HTTP single-host reverse proxy.
1
+package httpproxy
0 2
new file mode 100644
... ...
@@ -0,0 +1,194 @@
0
+package httpproxy
1
+
2
+import (
3
+	"crypto/tls"
4
+	"fmt"
5
+	"io"
6
+	"net"
7
+	"net/http"
8
+	"net/http/httputil"
9
+	"net/url"
10
+	"strings"
11
+
12
+	kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client"
13
+	"github.com/golang/glog"
14
+)
15
+
16
+// UpgradeAwareSingleHostReverseProxy is capable of proxying both regular HTTP
17
+// connections and those that require upgrading (e.g. web sockets). It implements
18
+// the http.RoundTripper and http.Handler interfaces.
19
+type UpgradeAwareSingleHostReverseProxy struct {
20
+	clientConfig *kclient.Config
21
+	backendAddr  *url.URL
22
+	transport    http.RoundTripper
23
+	reverseProxy *httputil.ReverseProxy
24
+}
25
+
26
+// NewUpgradeAwareSingleHostReverseProxy creates a new UpgradeAwareSingleHostReverseProxy.
27
+func NewUpgradeAwareSingleHostReverseProxy(clientConfig *kclient.Config, backendAddr *url.URL) (*UpgradeAwareSingleHostReverseProxy, error) {
28
+	transport, err := kclient.TransportFor(clientConfig)
29
+	if err != nil {
30
+		return nil, err
31
+	}
32
+	p := &UpgradeAwareSingleHostReverseProxy{
33
+		clientConfig: clientConfig,
34
+		backendAddr:  backendAddr,
35
+		transport:    transport,
36
+		reverseProxy: httputil.NewSingleHostReverseProxy(backendAddr),
37
+	}
38
+	p.reverseProxy.Transport = p
39
+	return p, nil
40
+}
41
+
42
+// RoundTrip sends the request to the backend and strips off the CORS headers
43
+// before returning the response.
44
+func (p *UpgradeAwareSingleHostReverseProxy) RoundTrip(req *http.Request) (*http.Response, error) {
45
+	resp, err := p.transport.RoundTrip(req)
46
+	if err != nil {
47
+		return resp, err
48
+	}
49
+
50
+	// strip off CORS headers sent from the backend
51
+	resp.Header.Del("Access-Control-Allow-Credentials")
52
+	resp.Header.Del("Access-Control-Allow-Headers")
53
+	resp.Header.Del("Access-Control-Allow-Methods")
54
+	resp.Header.Del("Access-Control-Allow-Origin")
55
+
56
+	// TODO do we need to strip off anything else?
57
+
58
+	return resp, err
59
+}
60
+
61
+// borrowed from net/http/httputil/reverseproxy.go
62
+func singleJoiningSlash(a, b string) string {
63
+	aslash := strings.HasSuffix(a, "/")
64
+	bslash := strings.HasPrefix(b, "/")
65
+	switch {
66
+	case aslash && bslash:
67
+		return a + b[1:]
68
+	case !aslash && !bslash:
69
+		return a + "/" + b
70
+	}
71
+	return a + b
72
+}
73
+
74
+func (p *UpgradeAwareSingleHostReverseProxy) newProxyRequest(req *http.Request) (*http.Request, error) {
75
+	// TODO is this the best way to clone the original request and create
76
+	// the new request for the backend? Do we need to copy anything else?
77
+	//
78
+	backendURL := *p.backendAddr
79
+	// if backendAddr is http://host/base and req is for /foo, the resulting path
80
+	// for backendURL should be /base/foo
81
+	backendURL.Path = singleJoiningSlash(backendURL.Path, req.URL.Path)
82
+	backendURL.RawQuery = req.URL.RawQuery
83
+
84
+	newReq, err := http.NewRequest(req.Method, backendURL.String(), req.Body)
85
+	if err != nil {
86
+		return nil, err
87
+	}
88
+	// TODO is this the right way to copy headers?
89
+	newReq.Header = req.Header
90
+	// TODO do we need to exclude any headers?
91
+	return newReq, nil
92
+}
93
+
94
+func (p *UpgradeAwareSingleHostReverseProxy) isUpgradeRequest(req *http.Request) bool {
95
+	for _, h := range req.Header[http.CanonicalHeaderKey("Connection")] {
96
+		if strings.Contains(strings.ToLower(h), "upgrade") {
97
+			return true
98
+		}
99
+	}
100
+	return false
101
+}
102
+
103
+// ServeHTTP inspects the request and either proxies an upgraded connection directly,
104
+// or uses httputil.ReverseProxy to proxy the normal request.
105
+func (p *UpgradeAwareSingleHostReverseProxy) ServeHTTP(w http.ResponseWriter, req *http.Request) {
106
+	newReq, err := p.newProxyRequest(req)
107
+	if err != nil {
108
+		glog.Errorf("Error creating backend request: %s", err)
109
+		// TODO do we need to do more than this?
110
+		w.WriteHeader(http.StatusInternalServerError)
111
+		return
112
+	}
113
+
114
+	if !p.isUpgradeRequest(req) {
115
+		p.reverseProxy.ServeHTTP(w, newReq)
116
+		return
117
+	}
118
+
119
+	p.serveUpgrade(w, newReq)
120
+}
121
+
122
+func (p *UpgradeAwareSingleHostReverseProxy) dialBackend(req *http.Request) (net.Conn, error) {
123
+	switch p.backendAddr.Scheme {
124
+	case "http":
125
+		return net.Dial("tcp", req.URL.Host)
126
+	case "https":
127
+		tlsConfig, err := kclient.TLSConfigFor(p.clientConfig)
128
+		if err != nil {
129
+			return nil, err
130
+		}
131
+		tlsConn, err := tls.Dial("tcp", req.URL.Host, tlsConfig)
132
+		if err != nil {
133
+			return nil, err
134
+		}
135
+		hostToVerify := req.URL.Host
136
+		if index := strings.Index(hostToVerify, ":"); index > -1 {
137
+			hostToVerify = hostToVerify[0:index]
138
+		}
139
+		err = tlsConn.VerifyHostname(hostToVerify)
140
+		return tlsConn, err
141
+	default:
142
+		return nil, fmt.Errorf("Unknown scheme: %s", p.backendAddr.Scheme)
143
+	}
144
+}
145
+
146
+func (p *UpgradeAwareSingleHostReverseProxy) serveUpgrade(w http.ResponseWriter, req *http.Request) {
147
+	backendConn, err := p.dialBackend(req)
148
+	if err != nil {
149
+		glog.Errorf("Error connecting to backend: %s", err)
150
+		// TODO do we need to do more than this?
151
+		w.WriteHeader(http.StatusServiceUnavailable)
152
+		return
153
+	}
154
+	defer backendConn.Close()
155
+
156
+	requestHijackedConn, _, err := w.(http.Hijacker).Hijack()
157
+	if err != nil {
158
+		glog.Errorf("Error hijacking request connection: %s", err)
159
+		return
160
+	}
161
+	defer requestHijackedConn.Close()
162
+
163
+	// NOTE: from this point forward, we own the connection and we can't use
164
+	// w.Header(), w.Write(), or w.WriteHeader any more
165
+
166
+	err = req.Write(backendConn)
167
+	if err != nil {
168
+		glog.Errorf("Error writing request to backend: %s", err)
169
+	}
170
+
171
+	done := make(chan struct{}, 2)
172
+
173
+	go func() {
174
+		_, err := io.Copy(backendConn, requestHijackedConn)
175
+		if err != nil {
176
+			// TODO I see this printed at least whenever the client goes away from the page.
177
+			// Should we check for different types of errors and only log certain ones?
178
+			// Should we make this V(2+) ?
179
+			glog.Errorf("Error proxying data from client to backend: %v", err)
180
+		}
181
+		done <- struct{}{}
182
+	}()
183
+
184
+	go func() {
185
+		_, err := io.Copy(requestHijackedConn, backendConn)
186
+		if err != nil {
187
+			glog.Errorf("Error proxying data from backend to client: %v", err)
188
+		}
189
+		done <- struct{}{}
190
+	}()
191
+
192
+	<-done
193
+}