Skip to content

Commit 4f4f4ea

Browse files
authored
chore: port HTTP/2 downgrade test to current e2e test framework (#539)
Signed-off-by: Chetan Banavikalmutt <[email protected]>
1 parent 50dbd11 commit 4f4f4ea

File tree

5 files changed

+243
-50
lines changed

5 files changed

+243
-50
lines changed

hack/dev-env/start-agent-managed.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export ARGOCD_AGENT_REMOTE_PORT=${ARGOCD_AGENT_REMOTE_PORT:-8443}
2727
E2E_ENV_FILE="/tmp/argocd-agent-e2e"
2828
if [ -f "$E2E_ENV_FILE" ]; then
2929
source "$E2E_ENV_FILE"
30+
export ARGOCD_PRINCIPAL_ENABLE_WEBSOCKET=${ARGOCD_PRINCIPAL_ENABLE_WEBSOCKET:-false}
3031
fi
3132

3233
go run github.com/argoproj-labs/argocd-agent/cmd/argocd-agent agent \

hack/dev-env/start-principal.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ if test "${ARGOCD_PRINCIPAL_REDIS_SERVER_ADDRESS}" = ""; then
3535
export ARGOCD_PRINCIPAL_REDIS_SERVER_ADDRESS
3636
fi
3737

38+
# Point the principal to the e2e test configuration if it exists
39+
E2E_ENV_FILE="/tmp/argocd-agent-e2e"
40+
if [ -f "$E2E_ENV_FILE" ]; then
41+
source "$E2E_ENV_FILE"
42+
export ARGOCD_PRINCIPAL_ENABLE_WEBSOCKET=${ARGOCD_PRINCIPAL_ENABLE_WEBSOCKET:-false}
43+
fi
44+
3845
SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
3946
go run github.com/argoproj-labs/argocd-agent/cmd/argocd-agent principal \
4047
--allowed-namespaces '*' \

test/e2e/fixture/toxyproxy.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,23 @@ func CheckReadiness(t require.TestingT, compName string) {
125125
return resp.StatusCode == http.StatusOK
126126
}, 120*time.Second, 2*time.Second)
127127
}
128+
129+
func IsNotReady(t require.TestingT, compName string) {
130+
healthzAddr := "http://localhost:8002/healthz"
131+
if compName == "agent-managed" {
132+
healthzAddr = "http://localhost:8001/healthz"
133+
}
134+
135+
if compName == "principal" {
136+
healthzAddr = "http://localhost:8003/healthz"
137+
}
138+
139+
require.Never(t, func() bool {
140+
resp, err := http.Get(healthzAddr)
141+
if err != nil {
142+
return false
143+
}
144+
defer resp.Body.Close()
145+
return resp.StatusCode == http.StatusOK
146+
}, 10*time.Second, 1*time.Second)
147+
}

test/e2e/websocket_test.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Copyright 2025 The argocd-agent Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package e2e
16+
17+
import (
18+
"context"
19+
"fmt"
20+
"os"
21+
"testing"
22+
23+
"github.com/argoproj-labs/argocd-agent/internal/config"
24+
"github.com/argoproj-labs/argocd-agent/internal/tlsutil"
25+
"github.com/argoproj-labs/argocd-agent/test/e2e/fixture"
26+
"github.com/argoproj-labs/argocd-agent/test/proxy"
27+
"github.com/stretchr/testify/suite"
28+
"k8s.io/client-go/kubernetes"
29+
)
30+
31+
type HTTP1DowngradeTestSuite struct {
32+
fixture.BaseSuite
33+
}
34+
35+
func (suite *HTTP1DowngradeTestSuite) TearDownTest() {
36+
suite.BaseSuite.TearDownTest()
37+
requires := suite.Require()
38+
39+
if _, err := os.Stat(fixture.EnvVariablesFromE2EFile); err == nil {
40+
requires.NoError(os.Remove(fixture.EnvVariablesFromE2EFile))
41+
fixture.RestartAgent(suite.T(), "agent-managed")
42+
fixture.RestartAgent(suite.T(), "agent-autonomous")
43+
}
44+
45+
// Ensure that all the components are running after runnings the tests
46+
if !fixture.IsProcessRunning("process") {
47+
err := fixture.StartProcess("principal")
48+
requires.NoError(err)
49+
fixture.CheckReadiness(suite.T(), "principal")
50+
}
51+
52+
if !fixture.IsProcessRunning("agent-managed") {
53+
err := fixture.StartProcess("agent-managed")
54+
requires.NoError(err)
55+
fixture.CheckReadiness(suite.T(), "agent-managed")
56+
}
57+
58+
if !fixture.IsProcessRunning("agent-autonomous") {
59+
err := fixture.StartProcess("agent-autonomous")
60+
requires.NoError(err)
61+
fixture.CheckReadiness(suite.T(), "agent-autonomous")
62+
}
63+
}
64+
65+
func (suite *HTTP1DowngradeTestSuite) Test_WithHTTP1Downgrade() {
66+
requires := suite.Require()
67+
68+
// Verify that the agent has connected to the principal
69+
fixture.CheckReadiness(suite.T(), "agent-managed")
70+
71+
// Create a reverse proxy that downgrades the incoming requests to HTTP/1.1
72+
hostPort := func(host string, port int) string {
73+
return fmt.Sprintf("%s:%d", host, port)
74+
}
75+
76+
principalClient, err := kubernetes.NewForConfig(suite.PrincipalClient.Config)
77+
requires.NoError(err)
78+
79+
agentClient, err := kubernetes.NewForConfig(suite.ManagedAgentClient.Config)
80+
requires.NoError(err)
81+
82+
ctx := context.Background()
83+
84+
// Load the principal's certificate for incoming connections (agent → proxy)
85+
principalServerCert, err := tlsutil.TLSCertFromSecret(ctx, principalClient, "argocd", config.SecretNamePrincipalTLS)
86+
requires.NoError(err)
87+
88+
// Load the agent's client certificate for outgoing connections (proxy → principal)
89+
agentClientCert, err := tlsutil.TLSCertFromSecret(ctx, agentClient, "argocd", config.SecretNameAgentClientCert)
90+
requires.NoError(err)
91+
92+
proxyPort := 9091
93+
principalPort := 8443
94+
95+
// Configure the proxy with TLS certificates
96+
http1Proxy := proxy.StartHTTP2DowngradingProxy(suite.T(), hostPort("", proxyPort), hostPort("", principalPort), agentClientCert, principalServerCert)
97+
defer http1Proxy.Close()
98+
99+
// The agent must connect to the principal via the proxy and explicitly disable WebSocket
100+
envVar := fmt.Sprintf(`ARGOCD_AGENT_REMOTE_PORT=%d
101+
ARGOCD_AGENT_ENABLE_WEBSOCKET=false
102+
ARGOCD_PRINCIPAL_ENABLE_WEBSOCKET=false`, proxyPort)
103+
err = os.WriteFile(fixture.EnvVariablesFromE2EFile, []byte(envVar+"\n"), 0644)
104+
requires.NoError(err)
105+
106+
defer func() {
107+
if err := os.Remove(fixture.EnvVariablesFromE2EFile); err != nil {
108+
suite.T().Errorf("failed to remove env file: %v", err)
109+
}
110+
111+
// Restart the agent process
112+
fixture.RestartAgent(suite.T(), "agent-managed")
113+
114+
// Give some time for the agent to be ready
115+
fixture.CheckReadiness(suite.T(), "agent-managed")
116+
117+
// Restart the principal process
118+
fixture.RestartAgent(suite.T(), "principal")
119+
120+
// Give some time for the principal to be ready
121+
fixture.CheckReadiness(suite.T(), "principal")
122+
}()
123+
124+
// Restart the agent process
125+
fixture.RestartAgent(suite.T(), "agent-managed")
126+
127+
// Agent should not be able to connect to the principal when the Websocket is disabled because the
128+
// proxy downgrades the requests to HTTP/1.1
129+
fixture.IsNotReady(suite.T(), "agent-managed")
130+
131+
// Restart the principal and the agent with the Websocket enabled
132+
envVar = fmt.Sprintf(`ARGOCD_AGENT_REMOTE_PORT=%d
133+
ARGOCD_AGENT_ENABLE_WEBSOCKET=true
134+
ARGOCD_PRINCIPAL_ENABLE_WEBSOCKET=true`, proxyPort)
135+
err = os.WriteFile(fixture.EnvVariablesFromE2EFile, []byte(envVar+"\n"), 0644)
136+
requires.NoError(err)
137+
138+
fixture.RestartAgent(suite.T(), "principal")
139+
140+
fixture.RestartAgent(suite.T(), "agent-managed")
141+
142+
// Give some time for the principal to be ready
143+
fixture.CheckReadiness(suite.T(), "principal")
144+
145+
// Give some time for the agent to be ready
146+
fixture.CheckReadiness(suite.T(), "agent-managed")
147+
}
148+
149+
func TestHTTP1DowngradeTestSuite(t *testing.T) {
150+
suite.Run(t, new(HTTP1DowngradeTestSuite))
151+
}

test/proxy/proxy.go

Lines changed: 64 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,17 @@ package proxy
22

33
import (
44
"crypto/tls"
5-
"crypto/x509"
65
"fmt"
76
"log"
8-
"math/big"
97
"net"
108
"net/http"
119
"net/http/httputil"
12-
"path"
1310
"testing"
14-
15-
"github.com/argoproj-labs/argocd-agent/test/fake/testcerts"
1611
)
1712

18-
// Start a reverse proxy that downgrades the incoming HTTP/2 requests to HTTP/1.1
19-
func StartHTTP2DowngradingProxy(t *testing.T, addr string, target string) *http.Server {
20-
tempDir := t.TempDir()
21-
basePath := path.Join(tempDir, "certs")
22-
testcerts.WriteSelfSignedCert(t, "rsa", basePath, x509.Certificate{SerialNumber: big.NewInt(1)})
13+
// StartHTTP2DowngradingProxy starts a proxy that downgrades HTTP/2.0 to HTTP/1.1
14+
// This simulates a real HTTP/1.1-only proxy environment.
15+
func StartHTTP2DowngradingProxy(t *testing.T, addr string, target string, agentClientCert, principalServerCert tls.Certificate) *http.Server {
2316

2417
lis, err := net.Listen("tcp", addr)
2518
if err != nil {
@@ -28,58 +21,79 @@ func StartHTTP2DowngradingProxy(t *testing.T, addr string, target string) *http.
2821

2922
server := &http.Server{
3023
TLSConfig: &tls.Config{
31-
InsecureSkipVerify: true,
24+
// Use principal's server certificate for incoming connections
25+
// This makes the agent think it's connecting to the real principal
26+
Certificates: []tls.Certificate{principalServerCert},
27+
NextProtos: []string{"h2", "http/1.1"},
3228
},
33-
Handler: downgradeToHTTP1Handler(target),
29+
Handler: downgradeToHTTP1Handler(target, agentClientCert),
3430
Addr: lis.Addr().String(),
3531
}
3632

3733
//nolint:errcheck
38-
go server.ServeTLS(lis, basePath+".crt", basePath+".key")
34+
go func() {
35+
tlsListener := tls.NewListener(lis, server.TLSConfig)
36+
if err := server.Serve(tlsListener); err != nil {
37+
log.Printf("Proxy server error: %v", err)
38+
}
39+
}()
3940

4041
return server
4142
}
4243

43-
func downgradeToHTTP1Handler(target string) http.Handler {
44-
printHeaders := func(header http.Header) {
45-
for name, values := range header {
46-
for _, value := range values {
47-
fmt.Printf("%s: %s\n", name, value)
48-
}
49-
}
50-
}
51-
return &httputil.ReverseProxy{
52-
Director: func(req *http.Request) {
53-
fmt.Println("Intercepting a request")
54-
req.URL.Host = target
55-
if req.URL.Scheme == "" {
56-
req.URL.Scheme = "https"
57-
}
44+
func downgradeToHTTP1Handler(target string, agentCert tls.Certificate) http.Handler {
45+
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
46+
fmt.Printf("Intercepting a %s request to %s\n", req.Method, req.URL)
47+
48+
// Check if this is a gRPC request
49+
isGRPC := req.Header.Get("Content-Type") == "application/grpc"
50+
fmt.Printf("Is gRPC request: %t, Protocol: %s\n", isGRPC, req.Proto)
5851

59-
// Request is downgraded to HTTP/1.1
52+
// Downgrade all requests to HTTP/1.1
53+
if req.Proto == "HTTP/2.0" {
54+
fmt.Println("Downgrading request from HTTP/2.0 to HTTP/1.1")
6055
req.ProtoMajor, req.ProtoMinor, req.Proto = 1, 1, "HTTP/1.1"
56+
}
6157

62-
// Log headers for debugging
63-
fmt.Println("Request Headers:")
64-
printHeaders(req.Header)
65-
},
66-
Transport: &http.Transport{
67-
TLSClientConfig: &tls.Config{
68-
InsecureSkipVerify: true,
69-
},
70-
ForceAttemptHTTP2: false,
71-
},
72-
ModifyResponse: func(res *http.Response) error {
73-
// Ensure the response is sent as HTTP/1.1
74-
res.ProtoMajor = 1
75-
res.ProtoMinor = 1
76-
res.Proto = "HTTP/1.1"
58+
// For WebSocket upgrade requests, allow them through
59+
if req.Header.Get("Upgrade") == "websocket" {
60+
fmt.Println("Allowing WebSocket upgrade request")
61+
} else if isGRPC && req.Proto == "HTTP/1.1" {
62+
fmt.Println("Forwarding downgraded gRPC request over HTTP/1.1")
63+
}
7764

78-
// Log response headers for debugging
79-
fmt.Println("Response Headers:")
80-
printHeaders(res.Header)
65+
// Use reverse proxy with HTTP/1.1 enforcement
66+
proxy := &httputil.ReverseProxy{
67+
Director: func(req *http.Request) {
68+
req.URL.Host = target
69+
if req.URL.Scheme == "" {
70+
req.URL.Scheme = "https"
71+
}
8172

82-
return nil
83-
},
84-
}
73+
// Ensure the request is downgraded to HTTP/1.1
74+
req.ProtoMajor, req.ProtoMinor, req.Proto = 1, 1, "HTTP/1.1"
75+
76+
fmt.Printf("Proxying %s request to %s (downgraded to %s)\n", req.Method, req.URL, req.Proto)
77+
},
78+
Transport: &http.Transport{
79+
TLSClientConfig: &tls.Config{
80+
InsecureSkipVerify: true,
81+
// Use agent's certificate for outgoing connections
82+
// This makes the principal think it's connecting to the real agent
83+
Certificates: []tls.Certificate{agentCert},
84+
},
85+
ForceAttemptHTTP2: false,
86+
},
87+
ModifyResponse: func(res *http.Response) error {
88+
if res != nil {
89+
// Ensure response is also HTTP/1.1
90+
res.ProtoMajor, res.ProtoMinor, res.Proto = 1, 1, "HTTP/1.1"
91+
fmt.Printf("Response: %s (proto: %s)\n", res.Status, res.Proto)
92+
}
93+
return nil
94+
},
95+
}
96+
97+
proxy.ServeHTTP(w, req)
98+
})
8599
}

0 commit comments

Comments
 (0)