Skip to content

Commit 7cf89dc

Browse files
Dariquestgeofffranks
authored andcommitted
Enable websockets for apps bound to route services
Add new spec for gorouter.route_services.enable_websockets Default value is true Unit & Integration tests with a websocket client
1 parent 0abfba9 commit 7cf89dc

File tree

15 files changed

+200
-38
lines changed

15 files changed

+200
-38
lines changed

jobs/gorouter/spec

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,10 @@ properties:
255255
Maximum number of attempts on failing requests against route service URLs.
256256
The minimum value for this setting is 1. This prevents gorouter from getting blocked by indefinite retries.
257257
default: 3
258+
router.route_services.enable_websockets:
259+
description: |
260+
Enable websocket connections for application routes bound to Route Services.
261+
default: true
258262
router.route_services.cert_chain:
259263
description: Certificate chain used for client authentication to TLS-registered route services. In PEM format.
260264
router.route_services.private_key:

jobs/gorouter/templates/gorouter.yml.erb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,12 +345,14 @@ if (route_service_attempts < 1 )
345345
end
346346

347347
strict_signature_validation = p('router.route_services_strict_signature_validation')
348+
enable_websockets = p('router.route_services.enable_websockets')
348349

349350
route_services = {
350351
'max_attempts' => route_service_attempts,
351352
'cert_chain' => route_services_cert_chain,
352353
'private_key' => route_services_private_key,
353354
'strict_signature_validation' => strict_signature_validation,
355+
'enable_websockets' => enable_websockets,
354356
}
355357

356358
params['route_services'] = route_services

spec/gorouter_templates_spec.rb

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,8 @@
228228
'max_attempts' => 3,
229229
'cert_chain' => ROUTE_SERVICES_CLIENT_TEST_CERT,
230230
'private_key' => ROUTE_SERVICES_CLIENT_TEST_KEY,
231-
'strict_signature_validation' => false
231+
'strict_signature_validation' => false,
232+
'enable_websockets' => true,
232233
},
233234
'frontend_idle_timeout' => 5,
234235
'ip_local_port_range' => '1024 65535',
@@ -919,6 +920,27 @@
919920
expect(parsed_yaml['route_services']['strict_signature_validation']).to eq(true)
920921
end
921922
end
923+
context 'when enable_websockets not set' do
924+
it 'defaults to true' do
925+
expect(parsed_yaml['route_services']['enable_websockets']).to eq(true)
926+
end
927+
end
928+
context 'when enable_websockets is enabled' do
929+
before do
930+
deployment_manifest_fragment['router']['route_services']['enable_websockets'] = true
931+
end
932+
it 'parses to true' do
933+
expect(parsed_yaml['route_services']['enable_websockets']).to eq(true)
934+
end
935+
end
936+
context 'when enable_websockets is disabled' do
937+
before do
938+
deployment_manifest_fragment['router']['route_services']['enable_websockets'] = false
939+
end
940+
it 'parses to true' do
941+
expect(parsed_yaml['route_services']['enable_websockets']).to eq(false)
942+
end
943+
end
922944
end
923945

924946
describe 'backends' do

src/code.cloudfoundry.org/gorouter/cmd/gorouter/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ func main() {
175175
cryptoPrev,
176176
c.RouteServiceRecommendHttps,
177177
c.RouteServiceConfig.StrictSignatureValidation,
178+
c.RouteServiceConfig.EnableWebsockets,
178179
)
179180

180181
// These TLS configs are just templates. If you add other keys you will

src/code.cloudfoundry.org/gorouter/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ type RouteServiceConfig struct {
194194
MaxAttempts int `yaml:"max_attempts"`
195195
StrictSignatureValidation bool `yaml:"strict_signature_validation"`
196196
TLSPem `yaml:",inline"` // embed to get cert_chain and private_key for client authentication
197+
EnableWebsockets bool `yaml:"enable_websockets"`
197198
}
198199

199200
type LoggingConfig struct {

src/code.cloudfoundry.org/gorouter/handlers/routeservice.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,7 @@ func (r *RouteService) ServeHTTP(rw http.ResponseWriter, req *http.Request, next
104104
)
105105
return
106106
}
107-
108-
if IsWebSocketUpgrade(req) {
107+
if IsWebSocketUpgrade(req) && !r.config.EnableWebsockets() {
109108
logger.Info("route-service-unsupported")
110109
AddRouterErrorHeader(rw, "route_service_unsupported")
111110
r.errorWriter.WriteError(

src/code.cloudfoundry.org/gorouter/handlers/routeservice_test.go

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,7 @@ var _ = Describe("Route Service Handler", func() {
9999
crypto, err = secure.NewAesGCM([]byte("ABCDEFGHIJKLMNOP"))
100100
Expect(err).NotTo(HaveOccurred())
101101
config = routeservice.NewRouteServiceConfig(
102-
logger.Logger, true, true, nil, 60*time.Second, crypto, nil, true, false,
103-
)
102+
logger.Logger, true, true, nil, 60*time.Second, crypto, nil, true, false, true)
104103

105104
nextCalled = false
106105
prevHandler = &PrevHandler{}
@@ -121,7 +120,7 @@ var _ = Describe("Route Service Handler", func() {
121120

122121
Context("with route services disabled", func() {
123122
BeforeEach(func() {
124-
config = routeservice.NewRouteServiceConfig(logger.Logger, false, false, nil, 0, nil, nil, false, false)
123+
config = routeservice.NewRouteServiceConfig(logger.Logger, false, false, nil, 0, nil, nil, false, false, true)
125124
})
126125

127126
Context("for normal routes", func() {
@@ -192,7 +191,7 @@ var _ = Describe("Route Service Handler", func() {
192191
Context("with strictSignatureValidation enabled", func() {
193192
BeforeEach(func() {
194193
config = routeservice.NewRouteServiceConfig(
195-
logger.Logger, true, false, nil, 60*time.Second, crypto, nil, false, true,
194+
logger.Logger, true, false, nil, 60*time.Second, crypto, nil, false, true, true,
196195
)
197196
})
198197

@@ -274,7 +273,7 @@ var _ = Describe("Route Service Handler", func() {
274273
BeforeEach(func() {
275274
hairpinning := false
276275
config = routeservice.NewRouteServiceConfig(
277-
logger.Logger, true, hairpinning, nil, 60*time.Second, crypto, nil, true, false,
276+
logger.Logger, true, hairpinning, nil, 60*time.Second, crypto, nil, true, false, true,
278277
)
279278
})
280279

@@ -305,7 +304,7 @@ var _ = Describe("Route Service Handler", func() {
305304
BeforeEach(func() {
306305
hairpinning := true
307306
config = routeservice.NewRouteServiceConfig(
308-
logger.Logger, true, hairpinning, nil, 60*time.Second, crypto, nil, true, false,
307+
logger.Logger, true, hairpinning, nil, 60*time.Second, crypto, nil, true, false, true,
309308
)
310309
})
311310

@@ -336,7 +335,7 @@ var _ = Describe("Route Service Handler", func() {
336335
BeforeEach(func() {
337336
hairpinning := true
338337
config = routeservice.NewRouteServiceConfig(
339-
logger.Logger, true, hairpinning, []string{"route-service.com"}, 60*time.Second, crypto, nil, true, false,
338+
logger.Logger, true, hairpinning, []string{"route-service.com"}, 60*time.Second, crypto, nil, true, false, true,
340339
)
341340
})
342341

@@ -368,7 +367,7 @@ var _ = Describe("Route Service Handler", func() {
368367
BeforeEach(func() {
369368
hairpinning := true
370369
config = routeservice.NewRouteServiceConfig(
371-
logger.Logger, true, hairpinning, []string{"example.com"}, 60*time.Second, crypto, nil, true, false,
370+
logger.Logger, true, hairpinning, []string{"example.com"}, 60*time.Second, crypto, nil, true, false, true,
372371
)
373372
})
374373

@@ -400,7 +399,7 @@ var _ = Describe("Route Service Handler", func() {
400399
BeforeEach(func() {
401400
hairpinning := true
402401
config = routeservice.NewRouteServiceConfig(
403-
logger.Logger, true, hairpinning, generateHugeAllowlist(1000000), 60*time.Second, crypto, nil, true, false,
402+
logger.Logger, true, hairpinning, generateHugeAllowlist(1000000), 60*time.Second, crypto, nil, true, false, true,
404403
)
405404
})
406405

@@ -438,7 +437,7 @@ var _ = Describe("Route Service Handler", func() {
438437
Context("when recommendHttps is set to false", func() {
439438
BeforeEach(func() {
440439
config = routeservice.NewRouteServiceConfig(
441-
logger.Logger, true, false, nil, 60*time.Second, crypto, nil, false, false,
440+
logger.Logger, true, false, nil, 60*time.Second, crypto, nil, false, false, true,
442441
)
443442
})
444443
It("sends the request to the route service with X-CF-Forwarded-Url using http scheme", func() {
@@ -609,7 +608,7 @@ var _ = Describe("Route Service Handler", func() {
609608
cryptoPrev, err = secure.NewAesGCM([]byte("QRSTUVWXYZ123456"))
610609
Expect(err).ToNot(HaveOccurred())
611610
config = routeservice.NewRouteServiceConfig(
612-
logger.Logger, true, false, nil, 60*time.Second, crypto, cryptoPrev, true, false,
611+
logger.Logger, true, false, nil, 60*time.Second, crypto, cryptoPrev, true, false, true,
613612
)
614613
})
615614

@@ -704,13 +703,17 @@ var _ = Describe("Route Service Handler", func() {
704703
req.Header.Set("upgrade", "websocket")
705704

706705
})
707-
It("returns a 503", func() {
706+
It("request contains correct route service URL", func() {
708707
handler.ServeHTTP(resp, req)
709708

710-
Expect(resp.Code).To(Equal(http.StatusServiceUnavailable))
711-
Expect(resp.Body.String()).To(ContainSubstring("Websocket requests are not supported for routes bound to Route Services."))
709+
var passedReq *http.Request
710+
Eventually(reqChan).Should(Receive(&passedReq))
712711

713-
Expect(nextCalled).To(BeFalse())
712+
reqInfo, err := handlers.ContextRequestInfo(passedReq)
713+
Expect(err).ToNot(HaveOccurred())
714+
Expect(reqInfo.RouteServiceURL.Scheme).To(Equal("https"))
715+
Expect(reqInfo.RouteServiceURL.Host).To(Equal("goodrouteservice.com"))
716+
Expect(nextCalled).To(BeTrue(), "Expected the next handler to be called.")
714717
})
715718
})
716719

@@ -888,7 +891,7 @@ var _ = Describe("Route Service Handler", func() {
888891
By(testCase.name)
889892

890893
config = routeservice.NewRouteServiceConfig(
891-
logger.Logger, true, true, testCase.allowlist, 60*time.Second, crypto, nil, true, false,
894+
logger.Logger, true, true, testCase.allowlist, 60*time.Second, crypto, nil, true, false, true,
892895
)
893896

894897
if testCase.err {

src/code.cloudfoundry.org/gorouter/integration/common_integration_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ func NewTestState() *testState {
8383
cfg.SuspendPruningIfNatsUnavailable = true
8484

8585
cfg.DisableKeepAlives = false
86+
cfg.RouteServiceEnabled = true
87+
cfg.RouteServiceConfig.EnableWebsockets = true
8688

8789
externalRouteServiceHostname := "external-route-service.localhost.routing.cf-app.com"
8890
routeServiceKey, routeServiceCert := test_util.CreateKeyPair(externalRouteServiceHostname)

src/code.cloudfoundry.org/gorouter/integration/route_services_test.go

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,52 @@ import (
44
"crypto/tls"
55
"fmt"
66
"io"
7+
"log"
8+
"net"
79
"net/http"
810
"net/http/httptest"
11+
"net/http/httputil"
12+
"net/url"
13+
"os"
914
"strings"
1015
"time"
1116

1217
. "github.com/onsi/ginkgo/v2"
1318
. "github.com/onsi/gomega"
19+
"golang.org/x/net/websocket"
1420

1521
"code.cloudfoundry.org/gorouter/route"
1622
"code.cloudfoundry.org/gorouter/test/common"
1723
)
1824

25+
func wsClient(conn net.Conn, urlStr string) (*websocket.Conn, error) {
26+
wsUrl, err := url.ParseRequestURI(urlStr)
27+
Expect(err).NotTo(HaveOccurred())
28+
29+
cfg := &websocket.Config{
30+
Location: wsUrl,
31+
Origin: wsUrl,
32+
Version: websocket.ProtocolVersionHybi13,
33+
}
34+
35+
wsConn, err := websocket.NewClient(cfg, conn)
36+
return wsConn, err
37+
}
38+
1939
var _ = Describe("Route services", func() {
2040

2141
var testState *testState
2242

2343
const (
24-
appHostname = "app-with-route-service.some.domain"
44+
appHostname = "app-with-route-service.some.domain"
45+
wsAppHostname = "ws-app-with-route-service.some.domain"
2546
)
2647

2748
var (
28-
testApp *httptest.Server
29-
routeService *httptest.Server
49+
testApp *httptest.Server
50+
routeService *httptest.Server
51+
wsTestApp *httptest.Server
52+
wsRouteService *httptest.Server
3053
)
3154

3255
BeforeEach(func() {
@@ -69,6 +92,30 @@ var _ = Describe("Route services", func() {
6992
Expect(err).ToNot(HaveOccurred())
7093
}))
7194

95+
wsRouteService = httptest.NewUnstartedServer(
96+
&httputil.ReverseProxy{
97+
Director: func(req *http.Request) {
98+
forwardedURLStr := req.Header.Get("X-Cf-Forwarded-Url")
99+
100+
forwardedURL, err := url.Parse(forwardedURLStr)
101+
if err != nil {
102+
log.Printf("ERROR: X-Cf-Forwarded-Url unparseable: %s\n", err.Error())
103+
return
104+
}
105+
106+
req.URL = &url.URL{
107+
Scheme: "http",
108+
Host: fmt.Sprintf("127.0.0.1:%d", testState.cfg.Port),
109+
}
110+
req.Host = forwardedURL.Host
111+
},
112+
Transport: &http.Transport{
113+
TLSClientConfig: &tls.Config{
114+
MinVersion: tls.VersionTLS12,
115+
InsecureSkipVerify: true,
116+
},
117+
},
118+
})
72119
})
73120

74121
AfterEach(func() {
@@ -84,6 +131,57 @@ var _ = Describe("Route services", func() {
84131
return fmt.Sprintf("https://%s:%s", testState.trustedExternalServiceHostname, port)
85132
}
86133

134+
Context("Happy Path with a web socket app with a route service", func() {
135+
Context("When an app is registered with a simple route service", func() {
136+
BeforeEach(func() {
137+
testState.EnableAccessLog()
138+
testState.StartGorouterOrFail()
139+
wsRouteService.Start()
140+
nilHandshake := func(c *websocket.Config, request *http.Request) error { return nil }
141+
wsHandler := websocket.Server{Handler: func(conn *websocket.Conn) {
142+
msgBuf := make([]byte, 100)
143+
n, err := conn.Read(msgBuf)
144+
Expect(err).NotTo(HaveOccurred())
145+
Expect(string(msgBuf[:n])).To(Equal("HELLO WEBSOCKET"))
146+
147+
_, _ = conn.Write([]byte("WEBSOCKET OK"))
148+
conn.Close()
149+
}, Handshake: nilHandshake}
150+
151+
wsTestApp = httptest.NewServer(wsHandler)
152+
153+
testState.registerWithInternalRouteService(
154+
wsTestApp,
155+
wsRouteService,
156+
wsAppHostname,
157+
testState.cfg.SSLPort,
158+
)
159+
})
160+
161+
It("succeeds", func() {
162+
conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", testState.cfg.Port))
163+
Expect(err).NotTo(HaveOccurred())
164+
165+
wsConn, err := wsClient(conn, "ws://"+wsAppHostname)
166+
Expect(err).NotTo(HaveOccurred())
167+
168+
num, err := wsConn.Write([]byte("HELLO WEBSOCKET"))
169+
Expect(err).NotTo(HaveOccurred())
170+
Expect(num).To(Equal(len([]byte("HELLO WEBSOCKET"))))
171+
172+
msgBuf := make([]byte, 100)
173+
num2, err := wsConn.Read(msgBuf)
174+
175+
Expect(err).NotTo(HaveOccurred())
176+
Expect(string(msgBuf[:num2])).To(Equal("WEBSOCKET OK"))
177+
178+
Eventually(func() ([]byte, error) {
179+
return os.ReadFile(testState.AccessLogFilePath())
180+
}).Should(ContainSubstring(`"GET / HTTP/1.1" 101 0 0`))
181+
})
182+
})
183+
})
184+
87185
Context("Happy Path", func() {
88186
Context("When an app is registered with a simple route service", func() {
89187
BeforeEach(func() {
@@ -102,7 +200,6 @@ var _ = Describe("Route services", func() {
102200
req := testState.newGetRequest(
103201
fmt.Sprintf("https://%s", appHostname),
104202
)
105-
106203
res, err := testState.client.Do(req)
107204
Expect(err).ToNot(HaveOccurred())
108205
Expect(res.StatusCode).To(Equal(http.StatusOK))

src/code.cloudfoundry.org/gorouter/proxy/proxy_suite_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ var _ = JustBeforeEach(func() {
121121
cryptoPrev,
122122
recommendHTTPS,
123123
strictSignatureValidation,
124+
conf.RouteServiceConfig.EnableWebsockets,
124125
)
125126

126127
proxyServer, err = net.Listen("tcp", "127.0.0.1:0")

src/code.cloudfoundry.org/gorouter/proxy/proxy_unit_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ var _ = Describe("Proxy Unit tests", func() {
6464
cryptoPrev,
6565
false,
6666
false,
67+
conf.RouteServiceConfig.EnableWebsockets,
6768
)
6869
varz := test_helpers.NullVarz{}
6970
sender := new(fakes.MetricSender)

src/code.cloudfoundry.org/gorouter/proxy/route_service_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ var _ = Describe("Route Services", func() {
8383
nil,
8484
recommendHTTPS,
8585
strictSignatureValidation,
86+
conf.RouteServiceConfig.EnableWebsockets,
8687
)
8788
reqArgs, err := config.CreateRequest("", forwardedUrl)
8889
Expect(err).ToNot(HaveOccurred())

0 commit comments

Comments
 (0)