git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #92 from butzist/feature/wsproxy

Merge websocket proxy feature from openshift/oauth-proxy

commit056089bbcc7f0c229a4cb14ce573b164c3058fef authorJoel Speed <Joel.speed@hotmail.co.uk> dateMon 11 Mar 2019, 13:22 (8 years ago) parent21c9d38c7193b4
7 files changed+145−26
CHANGELOG.md+1 Gopkg.lock+18−3 README.md+1 main.go+2−1 oauthproxy.go+35−14 oauthproxy_test.go+78 options.go+10−8
CHANGELOG.md+1
@@ -2,6 +2,7 @@
22
33## Changes since v3.1.0
44
5+- [#92](https://github.com/pusher/oauth2_proxy/pull/92) Merge websocket proxy feature from openshift/oauth-proxy (@butzist)
56- [#57](https://github.com/pusher/oauth2_proxy/pull/57) Fall back to using OIDC Subject instead of Email (@aigarius)
67- [#85](https://github.com/pusher/oauth2_proxy/pull/85) Use non-root user in docker images (@kskewes)
78- [#68](https://github.com/pusher/oauth2_proxy/pull/68) forward X-Auth-Access-Token header (@davidholsgrove)
Gopkg.lock+18−3
@@ -59,11 +59,11 @@
5959
6060[[projects]]
6161 branch = "master"
62− digest = "1:9408fb9c637c103010e5147469c232ce6b68edc840879cc730a2a15918e6cae8"
62+ digest = "1:15c0562bca5d78ac087fb39c211071dc124e79fb18f8b7c3f8a0bc7ffcb2a38e"
6363 name = "github.com/mreiferson/go-options"
6464 packages = ["."]
6565 pruneopts = ""
66− revision = "77551d20752b54535462404ad9d877ebdb26e53d"
66+ revision = "20ba7d382d05facb01e02eb777af0c5f229c5c95"
6767
6868[[projects]]
6969 digest = "1:256484dbbcd271f9ecebc6795b2df8cad4c458dd0f5fd82a8c2fa0c29f233411"
@@ -87,11 +87,22 @@
8787[[projects]]
8888 digest = "1:3926a4ec9a4ff1a072458451aa2d9b98acd059a45b38f7335d31e06c3d6a0159"
8989 name = "github.com/stretchr/testify"
90− packages = ["assert"]
90+ packages = [
91+ "assert",
92+ "require",
93+ ]
9194 pruneopts = ""
9295 revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0"
9396 version = "v1.1.4"
9497
98+[[projects]]
99+ branch = "master"
100+ digest = "1:39630a0e2844fc4297c27caacb394a9fd342f869292284a62f856877adab65bc"
101+ name = "github.com/yhat/wsutil"
102+ packages = ["."]
103+ pruneopts = ""
104+ revision = "1d66fa95c997864ba4d8479f56609620fe542928"
105+
95106[[projects]]
96107 branch = "master"
97108 digest = "1:f6a006d27619a4d93bf9b66fe1999b8c8d1fa62bdc63af14f10fbe6fcaa2aa1a"
@@ -112,6 +123,7 @@
112123 packages = [
113124 "context",
114125 "context/ctxhttp",
126+ "websocket",
115127 ]
116128 pruneopts = ""
117129 revision = "9dfe39835686865bff950a07b394c12a98ddc811"
@@ -192,7 +204,10 @@
192204 "github.com/mbland/hmacauth",
193205 "github.com/mreiferson/go-options",
194206 "github.com/stretchr/testify/assert",
207+ "github.com/stretchr/testify/require",
208+ "github.com/yhat/wsutil",
195209 "golang.org/x/crypto/bcrypt",
210+ "golang.org/x/net/websocket",
196211 "golang.org/x/oauth2",
197212 "golang.org/x/oauth2/google",
198213 "google.golang.org/api/admin/directory/v1",
README.md+1
@@ -242,6 +242,7 @@ Usage of oauth2_proxy:
242242 -profile-url string: Profile access endpoint
243243 -provider string: OAuth provider (default "google")
244244 -proxy-prefix string: the url root path that this proxy should be nested under (e.g. /<oauth2>/sign_in) (default "/oauth2")
245+ -proxy-websockets: enables WebSocket proxying (default true)
245246 -redeem-url string: Token redemption endpoint
246247 -redirect-url string: the OAuth Redirect URL. ie: "https://internalapp.yourcompany.com/oauth2/callback"
247248 -request-logging: Log requests to stdout (default true)
main.go+2−1
@@ -10,7 +10,7 @@ import (
1010 "time"
1111
1212 "github.com/BurntSushi/toml"
13− "github.com/mreiferson/go-options"
13+ options "github.com/mreiferson/go-options"
1414)
1515
1616func main() {
@@ -62,6 +62,7 @@ func main() {
6262 flagSet.String("custom-templates-dir", "", "path to custom html templates")
6363 flagSet.String("footer", "", "custom footer string. Use \"-\" to disable default footer.")
6464 flagSet.String("proxy-prefix", "/oauth2", "the url root path that this proxy should be nested under (e.g. /<oauth2>/sign_in)")
65+ flagSet.Bool("proxy-websockets", true, "enables WebSocket proxying")
6566
6667 flagSet.String("cookie-name", "_oauth2_proxy", "the name of the cookie that the oauth_proxy creates")
6768 flagSet.String("cookie-secret", "", "the seed string for secure cookies (optionally base64 encoded)")
oauthproxy.go+35−14
@@ -17,6 +17,7 @@ import (
1717 "github.com/mbland/hmacauth"
1818 "github.com/pusher/oauth2_proxy/cookie"
1919 "github.com/pusher/oauth2_proxy/providers"
20+ "github.com/yhat/wsutil"
2021)
2122
2223const (
@@ -95,9 +96,10 @@ type OAuthProxy struct {
9596
9697// UpstreamProxy represents an upstream server to proxy to
9798type UpstreamProxy struct {
98− upstream string
99− handler http.Handler
100− auth hmacauth.HmacAuth
99+ upstream string
100+ handler http.Handler
101+ wsHandler http.Handler
102+ auth hmacauth.HmacAuth
101103}
102104
103105// ServeHTTP proxies requests to the upstream provider while signing the
@@ -108,7 +110,12 @@ func (u *UpstreamProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
108110 r.Header.Set("GAP-Auth", w.Header().Get("GAP-Auth"))
109111 u.auth.SignRequest(r)
110112 }
111− u.handler.ServeHTTP(w, r)
113+ if u.wsHandler != nil && r.Header.Get("Connection") == "Upgrade" && r.Header.Get("Upgrade") == "websocket" {
114+ u.wsHandler.ServeHTTP(w, r)
115+ } else {
116+ u.handler.ServeHTTP(w, r)
117+ }
118+
112119}
113120
114121// NewReverseProxy creates a new reverse proxy for proxying requests to upstream
@@ -145,6 +152,26 @@ func NewFileServer(path string, filesystemPath string) (proxy http.Handler) {
145152 return http.StripPrefix(path, http.FileServer(http.Dir(filesystemPath)))
146153}
147154
155+// NewWebSocketOrRestReverseProxy creates a reverse proxy for REST or websocket based on url
156+func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.HmacAuth) (restProxy http.Handler) {
157+ u.Path = ""
158+ proxy := NewReverseProxy(u, opts.FlushInterval)
159+ if !opts.PassHostHeader {
160+ setProxyUpstreamHostHeader(proxy, u)
161+ } else {
162+ setProxyDirector(proxy)
163+ }
164+
165+ // this should give us a wss:// scheme if the url is https:// based.
166+ var wsProxy *wsutil.ReverseProxy
167+ if opts.ProxyWebSockets {
168+ wsScheme := "ws" + strings.TrimPrefix(u.Scheme, "http")
169+ wsURL := &url.URL{Scheme: wsScheme, Host: u.Host}
170+ wsProxy = wsutil.NewSingleHostReverseProxy(wsURL)
171+ }
172+ return &UpstreamProxy{u.Host, proxy, wsProxy, auth}
173+}
174+
148175// NewOAuthProxy creates a new instance of OOuthProxy from the options provided
149176func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
150177 serveMux := http.NewServeMux()
@@ -157,23 +184,17 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
157184 path := u.Path
158185 switch u.Scheme {
159186 case httpScheme, httpsScheme:
160− u.Path = ""
161187 log.Printf("mapping path %q => upstream %q", path, u)
162− proxy := NewReverseProxy(u, opts.FlushInterval)
163− if !opts.PassHostHeader {
164− setProxyUpstreamHostHeader(proxy, u)
165− } else {
166− setProxyDirector(proxy)
167− }
168− serveMux.Handle(path,
169− &UpstreamProxy{u.Host, proxy, auth})
188+ proxy := NewWebSocketOrRestReverseProxy(u, opts, auth)
189+ serveMux.Handle(path, proxy)
190+
170191 case "file":
171192 if u.Fragment != "" {
172193 path = u.Fragment
173194 }
174195 log.Printf("mapping path %q => file system %q", path, u.Path)
175196 proxy := NewFileServer(path, u.Path)
176− serveMux.Handle(path, &UpstreamProxy{path, proxy, nil})
197+ serveMux.Handle(path, &UpstreamProxy{path, proxy, nil, nil})
177198 default:
178199 panic(fmt.Sprintf("unknown upstream protocol %s", u.Scheme))
179200 }
oauthproxy_test.go+78
@@ -19,6 +19,7 @@ import (
1919 "github.com/pusher/oauth2_proxy/providers"
2020 "github.com/stretchr/testify/assert"
2121 "github.com/stretchr/testify/require"
22+ "golang.org/x/net/websocket"
2223)
2324
2425func init() {
@@ -26,6 +27,83 @@ func init() {
2627
2728}
2829
30+type WebSocketOrRestHandler struct {
31+ restHandler http.Handler
32+ wsHandler http.Handler
33+}
34+
35+func (h *WebSocketOrRestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
36+ if r.Header.Get("Upgrade") == "websocket" {
37+ h.wsHandler.ServeHTTP(w, r)
38+ } else {
39+ h.restHandler.ServeHTTP(w, r)
40+ }
41+}
42+
43+func TestWebSocketProxy(t *testing.T) {
44+ handler := WebSocketOrRestHandler{
45+ restHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
46+ w.WriteHeader(200)
47+ hostname, _, _ := net.SplitHostPort(r.Host)
48+ w.Write([]byte(hostname))
49+ }),
50+ wsHandler: websocket.Handler(func(ws *websocket.Conn) {
51+ defer ws.Close()
52+ var data []byte
53+ err := websocket.Message.Receive(ws, &data)
54+ if err != nil {
55+ t.Fatalf("err %s", err)
56+ return
57+ }
58+ err = websocket.Message.Send(ws, data)
59+ if err != nil {
60+ t.Fatalf("err %s", err)
61+ }
62+ return
63+ }),
64+ }
65+ backend := httptest.NewServer(&handler)
66+ defer backend.Close()
67+
68+ backendURL, _ := url.Parse(backend.URL)
69+
70+ options := NewOptions()
71+ var auth hmacauth.HmacAuth
72+ options.PassHostHeader = true
73+ proxyHandler := NewWebSocketOrRestReverseProxy(backendURL, options, auth)
74+ frontend := httptest.NewServer(proxyHandler)
75+ defer frontend.Close()
76+
77+ frontendURL, _ := url.Parse(frontend.URL)
78+ frontendWSURL := "ws://" + frontendURL.Host + "/"
79+
80+ ws, err := websocket.Dial(frontendWSURL, "", "http://localhost/")
81+ if err != nil {
82+ t.Fatalf("err %s", err)
83+ }
84+ request := []byte("hello, world!")
85+ err = websocket.Message.Send(ws, request)
86+ if err != nil {
87+ t.Fatalf("err %s", err)
88+ }
89+ var response = make([]byte, 1024)
90+ websocket.Message.Receive(ws, &response)
91+ if err != nil {
92+ t.Fatalf("err %s", err)
93+ }
94+ if g, e := string(request), string(response); g != e {
95+ t.Errorf("got body %q; expected %q", g, e)
96+ }
97+
98+ getReq, _ := http.NewRequest("GET", frontend.URL, nil)
99+ res, _ := http.DefaultClient.Do(getReq)
100+ bodyBytes, _ := ioutil.ReadAll(res.Body)
101+ backendHostname, _, _ := net.SplitHostPort(backendURL.Host)
102+ if g, e := string(bodyBytes), backendHostname; g != e {
103+ t.Errorf("got body %q; expected %q", g, e)
104+ }
105+}
106+
29107func TestNewReverseProxy(t *testing.T) {
30108 backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
31109 w.WriteHeader(200)
options.go+10−8
@@ -21,14 +21,15 @@ import (
2121// Options holds Configuration Options that can be set by Command Line Flag,
2222// or Config File
2323type Options struct {
24− ProxyPrefix string `flag:"proxy-prefix" cfg:"proxy-prefix"`
25− HTTPAddress string `flag:"http-address" cfg:"http_address"`
26− HTTPSAddress string `flag:"https-address" cfg:"https_address"`
27− RedirectURL string `flag:"redirect-url" cfg:"redirect_url"`
28− ClientID string `flag:"client-id" cfg:"client_id" env:"OAUTH2_PROXY_CLIENT_ID"`
29− ClientSecret string `flag:"client-secret" cfg:"client_secret" env:"OAUTH2_PROXY_CLIENT_SECRET"`
30− TLSCertFile string `flag:"tls-cert" cfg:"tls_cert_file"`
31− TLSKeyFile string `flag:"tls-key" cfg:"tls_key_file"`
24+ ProxyPrefix string `flag:"proxy-prefix" cfg:"proxy-prefix"`
25+ ProxyWebSockets bool `flag:"proxy-websockets" cfg:"proxy_websockets"`
26+ HTTPAddress string `flag:"http-address" cfg:"http_address"`
27+ HTTPSAddress string `flag:"https-address" cfg:"https_address"`
28+ RedirectURL string `flag:"redirect-url" cfg:"redirect_url"`
29+ ClientID string `flag:"client-id" cfg:"client_id" env:"OAUTH2_PROXY_CLIENT_ID"`
30+ ClientSecret string `flag:"client-secret" cfg:"client_secret" env:"OAUTH2_PROXY_CLIENT_SECRET"`
31+ TLSCertFile string `flag:"tls-cert" cfg:"tls_cert_file"`
32+ TLSKeyFile string `flag:"tls-key" cfg:"tls_key_file"`
3233
3334 AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"`
3435 AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant"`
@@ -105,6 +106,7 @@ type SignatureData struct {
105106func NewOptions() *Options {
106107 return &Options{
107108 ProxyPrefix: "/oauth2",
109+ ProxyWebSockets: true,
108110 HTTPAddress: "127.0.0.1:4180",
109111 HTTPSAddress: ":443",
110112 DisplayHtpasswdForm: true,