snapshot of https://github.com/nats-io/nats-server.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
NATS Server Tests / Lint (push) Failing after 3s
NATS Server Tests / Build (Latest Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2022) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2025) (push) Has been skipped
NATS Server Tests / Test Stores (push) Has been skipped
NATS Server Tests / Test JetStream (push) Has been skipped
NATS Server Tests / Test Raft (push) Has been skipped
NATS Server Tests / Test JetStream Consumers (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 1 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 2 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 3 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 4 (push) Has been skipped
NATS Server Tests / Test JetStream Supercluster (push) Has been skipped
NATS Server Tests / Test No-Race 1 (push) Has been skipped
NATS Server Tests / Test No-Race 2 (push) Has been skipped
NATS Server Tests / Test MQTT (push) Has been skipped
NATS Server Tests / Test Message Tracing (push) Has been skipped
NATS Server Tests / Test JWT (push) Has been skipped
NATS Server Tests / Test Remaining Server Tests (push) Has been skipped
NATS Server Tests / Test Other Packages (push) Has been skipped
Vulnerability Scan / govulncheck (push) Failing after 12m2s
Some checks failed
NATS Server Tests / Lint (push) Failing after 3s
NATS Server Tests / Build (Latest Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2022) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2025) (push) Has been skipped
NATS Server Tests / Test Stores (push) Has been skipped
NATS Server Tests / Test JetStream (push) Has been skipped
NATS Server Tests / Test Raft (push) Has been skipped
NATS Server Tests / Test JetStream Consumers (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 1 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 2 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 3 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 4 (push) Has been skipped
NATS Server Tests / Test JetStream Supercluster (push) Has been skipped
NATS Server Tests / Test No-Race 1 (push) Has been skipped
NATS Server Tests / Test No-Race 2 (push) Has been skipped
NATS Server Tests / Test MQTT (push) Has been skipped
NATS Server Tests / Test Message Tracing (push) Has been skipped
NATS Server Tests / Test JWT (push) Has been skipped
NATS Server Tests / Test Remaining Server Tests (push) Has been skipped
NATS Server Tests / Test Other Packages (push) Has been skipped
Vulnerability Scan / govulncheck (push) Failing after 12m2s
This commit is contained in:
929
server/leafnode_proxy_test.go
Normal file
929
server/leafnode_proxy_test.go
Normal file
@@ -0,0 +1,929 @@
|
||||
// Copyright 2025-2026 The NATS Authors
|
||||
// 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 server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Basic HTTP proxy for testing
|
||||
type testHTTPProxy struct {
|
||||
listener net.Listener
|
||||
port int
|
||||
username string
|
||||
password string
|
||||
started bool
|
||||
closeDelay time.Duration // Delay before closing connections for robustness
|
||||
connections []net.Conn // Track connections for cleanup
|
||||
}
|
||||
|
||||
func createTestHTTPProxy(username, password string) *testHTTPProxy {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
port := l.Addr().(*net.TCPAddr).Port
|
||||
|
||||
proxy := &testHTTPProxy{
|
||||
listener: l,
|
||||
port: port,
|
||||
username: username,
|
||||
password: password,
|
||||
closeDelay: 100 * time.Millisecond, // Default delay for test robustness
|
||||
connections: make([]net.Conn, 0),
|
||||
}
|
||||
|
||||
return proxy
|
||||
}
|
||||
|
||||
func (p *testHTTPProxy) setCloseDelay(delay time.Duration) {
|
||||
p.closeDelay = delay
|
||||
}
|
||||
|
||||
func (p *testHTTPProxy) start() {
|
||||
if p.started {
|
||||
return
|
||||
}
|
||||
p.started = true
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := p.listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p.connections = append(p.connections, conn)
|
||||
go p.handleConnection(conn)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *testHTTPProxy) handleConnection(conn net.Conn) {
|
||||
defer func() {
|
||||
if p.closeDelay > 0 {
|
||||
time.Sleep(p.closeDelay)
|
||||
}
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
// Set read timeout to prevent hanging on malformed requests
|
||||
conn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||
|
||||
// Read the CONNECT request
|
||||
buffer := make([]byte, 4096)
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
request := string(buffer[:n])
|
||||
lines := strings.Split(request, "\r\n")
|
||||
|
||||
if len(lines) == 0 || !strings.HasPrefix(lines[0], "CONNECT ") {
|
||||
conn.Write([]byte("HTTP/1.1 400 Bad Request\r\n\r\n"))
|
||||
return
|
||||
}
|
||||
|
||||
// Check authentication if required
|
||||
if p.username != _EMPTY_ || p.password != _EMPTY_ {
|
||||
authFound := false
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "Proxy-Authorization: Basic ") {
|
||||
authFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !authFound {
|
||||
conn.Write([]byte("HTTP/1.1 407 Proxy Authentication Required\r\n\r\n"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Extract target host from CONNECT line
|
||||
parts := strings.Fields(lines[0])
|
||||
if len(parts) < 3 {
|
||||
conn.Write([]byte("HTTP/1.1 400 Bad Request\r\n\r\n"))
|
||||
return
|
||||
}
|
||||
|
||||
targetHost := parts[1]
|
||||
|
||||
// Connect to target with timeout
|
||||
target, err := net.DialTimeout("tcp", targetHost, 5*time.Second)
|
||||
if err != nil {
|
||||
conn.Write([]byte("HTTP/1.1 502 Bad Gateway\r\n\r\n"))
|
||||
return
|
||||
}
|
||||
defer target.Close()
|
||||
|
||||
// Send success response
|
||||
conn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n"))
|
||||
|
||||
// Clear read deadline for ongoing connection
|
||||
conn.SetReadDeadline(time.Time{})
|
||||
|
||||
// Relay data between client and target with proper error handling
|
||||
done := make(chan bool, 2)
|
||||
|
||||
// Client to target
|
||||
go func() {
|
||||
defer func() {
|
||||
done <- true
|
||||
target.Close()
|
||||
}()
|
||||
buffer := make([]byte, 32*1024)
|
||||
for {
|
||||
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
target.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
||||
_, err = target.Write(buffer[:n])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Target to client
|
||||
go func() {
|
||||
defer func() {
|
||||
done <- true
|
||||
conn.Close()
|
||||
}()
|
||||
buffer := make([]byte, 32*1024)
|
||||
for {
|
||||
target.SetReadDeadline(time.Now().Add(30 * time.Second))
|
||||
n, err := target.Read(buffer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
||||
_, err = conn.Write(buffer[:n])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for either direction to finish
|
||||
<-done
|
||||
}
|
||||
|
||||
func (p *testHTTPProxy) stop() {
|
||||
if p.listener != nil {
|
||||
p.listener.Close()
|
||||
}
|
||||
// Close all tracked connections with delay for robustness
|
||||
for _, conn := range p.connections {
|
||||
go func(c net.Conn) {
|
||||
if p.closeDelay > 0 {
|
||||
time.Sleep(p.closeDelay)
|
||||
}
|
||||
c.Close()
|
||||
}(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *testHTTPProxy) url() string {
|
||||
return fmt.Sprintf("http://127.0.0.1:%d", p.port)
|
||||
}
|
||||
|
||||
func TestLeafNodeHttpProxyConfigParsing(t *testing.T) {
|
||||
// Test valid proxy configuration
|
||||
conf := `
|
||||
leafnodes {
|
||||
remotes = [
|
||||
{
|
||||
url: "ws://127.0.0.1:7422"
|
||||
proxy {
|
||||
url: "http://proxy.example.com:8080"
|
||||
username: "user"
|
||||
password: "pass"
|
||||
timeout: "10s"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
configFile := createConfFile(t, []byte(conf))
|
||||
|
||||
opts, err := ProcessConfigFile(configFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Error parsing config: %v", err)
|
||||
}
|
||||
|
||||
if len(opts.LeafNode.Remotes) != 1 {
|
||||
t.Fatalf("Expected 1 remote, got %d", len(opts.LeafNode.Remotes))
|
||||
}
|
||||
|
||||
remote := opts.LeafNode.Remotes[0]
|
||||
if remote.Proxy.URL != "http://proxy.example.com:8080" {
|
||||
t.Errorf("Expected proxy URL 'http://proxy.example.com:8080', got '%s'", remote.Proxy.URL)
|
||||
}
|
||||
if remote.Proxy.Username != "user" {
|
||||
t.Errorf("Expected proxy username 'user', got '%s'", remote.Proxy.Username)
|
||||
}
|
||||
if remote.Proxy.Password != "pass" {
|
||||
t.Errorf("Expected proxy password 'pass', got '%s'", remote.Proxy.Password)
|
||||
}
|
||||
if remote.Proxy.Timeout != 10*time.Second {
|
||||
t.Errorf("Expected proxy timeout 10s, got %v", remote.Proxy.Timeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLeafNodeHttpProxyConfigWarnings(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
config string
|
||||
expectWarning bool
|
||||
warningMatch string
|
||||
}{
|
||||
{
|
||||
name: "proxy with only TCP URLs",
|
||||
config: `
|
||||
leafnodes {
|
||||
remotes = [
|
||||
{
|
||||
url: "nats://127.0.0.1:7422"
|
||||
proxy {
|
||||
url: "http://proxy.example.com:8080"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
`,
|
||||
expectWarning: true,
|
||||
warningMatch: "proxy configuration will be ignored",
|
||||
},
|
||||
{
|
||||
name: "proxy with mixed TCP and WebSocket URLs",
|
||||
config: `
|
||||
leafnodes {
|
||||
remotes = [
|
||||
{
|
||||
urls: ["nats://127.0.0.1:7422", "ws://127.0.0.1:8080"]
|
||||
proxy {
|
||||
url: "http://proxy.example.com:8080"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
`,
|
||||
expectWarning: true,
|
||||
warningMatch: "proxy configuration will only be used for WebSocket URLs",
|
||||
},
|
||||
{
|
||||
name: "proxy with only WebSocket URLs",
|
||||
config: `
|
||||
leafnodes {
|
||||
remotes = [
|
||||
{
|
||||
url: "ws://127.0.0.1:7422"
|
||||
proxy {
|
||||
url: "http://proxy.example.com:8080"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
`,
|
||||
expectWarning: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
configFile := createConfFile(t, []byte(tc.config))
|
||||
|
||||
opts, err := ProcessConfigFile(configFile)
|
||||
|
||||
if tc.expectWarning {
|
||||
// With ProcessConfigFile, warnings don't cause errors
|
||||
// The configuration should be valid but might log warnings
|
||||
if err != nil {
|
||||
t.Fatalf("Expected valid configuration with warnings, but got error: %v", err)
|
||||
}
|
||||
if opts == nil {
|
||||
t.Fatal("Expected valid options but got nil")
|
||||
}
|
||||
// Note: With ProcessConfigFile, warnings are filtered out and not returned as errors
|
||||
// The test verifies that the configuration is valid despite having warning conditions
|
||||
} else {
|
||||
// No warnings expected - should parse successfully
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error but got: %v", err)
|
||||
}
|
||||
if opts == nil {
|
||||
t.Fatal("Expected valid options but got nil")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLeafNodeHttpProxyConnection(t *testing.T) {
|
||||
// Create a hub server with WebSocket support using config file
|
||||
hubConfig := createConfFile(t, []byte(`
|
||||
listen: "127.0.0.1:-1"
|
||||
websocket {
|
||||
listen: "127.0.0.1:-1"
|
||||
no_tls: true
|
||||
}
|
||||
leafnodes {
|
||||
listen: "127.0.0.1:-1"
|
||||
}
|
||||
`))
|
||||
|
||||
hub, hubOpts := RunServerWithConfig(hubConfig)
|
||||
defer hub.Shutdown()
|
||||
|
||||
// Create HTTP proxy
|
||||
proxy := createTestHTTPProxy(_EMPTY_, _EMPTY_)
|
||||
proxy.start()
|
||||
defer proxy.stop()
|
||||
|
||||
// Create spoke server with proxy configuration via config file
|
||||
configContent := fmt.Sprintf(`
|
||||
listen: "127.0.0.1:-1"
|
||||
leafnodes {
|
||||
reconnect_interval: "50ms"
|
||||
remotes = [
|
||||
{
|
||||
url: "ws://127.0.0.1:%d"
|
||||
proxy {
|
||||
url: "%s"
|
||||
timeout: 5s
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
`, hubOpts.Websocket.Port, proxy.url())
|
||||
|
||||
configFile := createConfFile(t, []byte(configContent))
|
||||
|
||||
spoke, _ := RunServerWithConfig(configFile)
|
||||
defer spoke.Shutdown()
|
||||
|
||||
// Verify leafnode connections are established
|
||||
checkLeafNodeConnected(t, spoke)
|
||||
checkLeafNodeConnected(t, hub)
|
||||
}
|
||||
|
||||
func TestLeafNodeHttpProxyConnectionToTCP(t *testing.T) {
|
||||
// Create a hub server using config file
|
||||
hubConfig := createConfFile(t, []byte(`
|
||||
listen: "127.0.0.1:-1"
|
||||
leafnodes {
|
||||
listen: "127.0.0.1:-1"
|
||||
}
|
||||
`))
|
||||
|
||||
hub, hubOpts := RunServerWithConfig(hubConfig)
|
||||
defer hub.Shutdown()
|
||||
|
||||
// Create HTTP proxy
|
||||
proxy := createTestHTTPProxy(_EMPTY_, _EMPTY_)
|
||||
proxy.start()
|
||||
defer proxy.stop()
|
||||
|
||||
// Create spoke server with proxy configuration via config file
|
||||
configContent := fmt.Sprintf(`
|
||||
listen: "127.0.0.1:-1"
|
||||
leafnodes {
|
||||
reconnect_interval: "50ms"
|
||||
remotes = [
|
||||
{
|
||||
url: "nats://127.0.0.1:%d"
|
||||
proxy {
|
||||
url: "%s"
|
||||
timeout: 5s
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
`, hubOpts.LeafNode.Port, proxy.url())
|
||||
|
||||
configFile := createConfFile(t, []byte(configContent))
|
||||
|
||||
spoke, _ := RunServerWithConfig(configFile)
|
||||
defer spoke.Shutdown()
|
||||
|
||||
// Verify leafnode connections are established
|
||||
checkLeafNodeConnected(t, spoke)
|
||||
checkLeafNodeConnected(t, hub)
|
||||
}
|
||||
|
||||
func TestLeafNodeHttpProxyWithAuthentication(t *testing.T) {
|
||||
// Create a hub server with WebSocket support using config file
|
||||
hubConfig := createConfFile(t, []byte(`
|
||||
listen: "127.0.0.1:-1"
|
||||
websocket {
|
||||
listen: "127.0.0.1:-1"
|
||||
no_tls: true
|
||||
}
|
||||
leafnodes {
|
||||
listen: "127.0.0.1:-1"
|
||||
}
|
||||
`))
|
||||
|
||||
hub, hubOpts := RunServerWithConfig(hubConfig)
|
||||
defer hub.Shutdown()
|
||||
|
||||
// Create HTTP proxy with authentication
|
||||
proxy := createTestHTTPProxy("testuser", "testpass")
|
||||
proxy.start()
|
||||
defer proxy.stop()
|
||||
|
||||
// Create spoke server with proxy configuration including auth via config file
|
||||
configContent := fmt.Sprintf(`
|
||||
listen: "127.0.0.1:-1"
|
||||
leafnodes {
|
||||
reconnect_interval: "50ms"
|
||||
remotes = [
|
||||
{
|
||||
url: "ws://127.0.0.1:%d"
|
||||
proxy {
|
||||
url: "%s"
|
||||
username: "testuser"
|
||||
password: "testpass"
|
||||
timeout: 5s
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
`, hubOpts.Websocket.Port, proxy.url())
|
||||
|
||||
configFile := createConfFile(t, []byte(configContent))
|
||||
|
||||
spoke, _ := RunServerWithConfig(configFile)
|
||||
defer spoke.Shutdown()
|
||||
|
||||
// Verify leafnode connections are established
|
||||
checkLeafNodeConnected(t, spoke)
|
||||
checkLeafNodeConnected(t, hub)
|
||||
}
|
||||
|
||||
func TestLeafNodeHttpProxyTLSMismatchDetection(t *testing.T) {
|
||||
// This test simulates the TLS mismatch scenario described in the feedback:
|
||||
// - Leafnode configured with proxy but no TLS
|
||||
// - Hub requires TLS
|
||||
// - Connection should fail with appropriate error message
|
||||
|
||||
// Create hub server with TLS required using config file
|
||||
hubConfig := createConfFile(t, []byte(`
|
||||
listen: "127.0.0.1:-1"
|
||||
leafnodes {
|
||||
listen: "127.0.0.1:-1"
|
||||
tls: {
|
||||
cert_file: "../test/configs/certs/server-cert.pem"
|
||||
key_file: "../test/configs/certs/server-key.pem"
|
||||
}
|
||||
}
|
||||
`))
|
||||
|
||||
hub, hubOpts := RunServerWithConfig(hubConfig)
|
||||
defer hub.Shutdown()
|
||||
|
||||
// Create HTTP proxy
|
||||
proxy := createTestHTTPProxy(_EMPTY_, _EMPTY_)
|
||||
proxy.start()
|
||||
defer proxy.stop()
|
||||
|
||||
// Create spoke server with proxy but no TLS configuration (intentional mismatch)
|
||||
spokeConfigContent := fmt.Sprintf(`
|
||||
listen: "127.0.0.1:-1"
|
||||
leafnodes {
|
||||
reconnect_interval: "50ms"
|
||||
remotes = [
|
||||
{
|
||||
url: "ws://127.0.0.1:%d"
|
||||
proxy {
|
||||
url: "%s"
|
||||
timeout: 5s
|
||||
}
|
||||
# Intentionally no TLS configuration to create mismatch
|
||||
}
|
||||
]
|
||||
}
|
||||
`, hubOpts.LeafNode.Port, proxy.url())
|
||||
|
||||
spokeConfig := createConfFile(t, []byte(spokeConfigContent))
|
||||
spoke, _ := RunServerWithConfig(spokeConfig)
|
||||
defer spoke.Shutdown()
|
||||
|
||||
// Wait and verify that connection was NOT established due to TLS mismatch
|
||||
// First attempt happens during RunServerWithConfig(), then retries every 50ms
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
|
||||
if spoke.NumLeafNodes() != 0 {
|
||||
t.Errorf("Expected 0 leafnode connections due to TLS mismatch, got %d", spoke.NumLeafNodes())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLeafNodeHttpProxyTunnelBasic(t *testing.T) {
|
||||
// Create HTTP proxy with longer delay for robustness
|
||||
proxy := createTestHTTPProxy(_EMPTY_, _EMPTY_)
|
||||
proxy.setCloseDelay(200 * time.Millisecond)
|
||||
proxy.start()
|
||||
defer proxy.stop()
|
||||
|
||||
// Create a simple TCP server to connect to through proxy
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test server: %v", err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
targetPort := listener.Addr().(*net.TCPAddr).Port
|
||||
targetHost := fmt.Sprintf("127.0.0.1:%d", targetPort)
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
|
||||
// Accept one connection with proper timeout handling
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("unable to accept: %v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Set read deadline to prevent hanging forever
|
||||
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
|
||||
// Read the incoming data first
|
||||
buffer := make([]byte, 1024)
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("server failed to read: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
receivedMsg := string(buffer[:n])
|
||||
if receivedMsg != "Hello" {
|
||||
errCh <- fmt.Errorf("server expected 'Hello', got '%s'", receivedMsg)
|
||||
return
|
||||
}
|
||||
|
||||
// Send response
|
||||
conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
|
||||
_, err = conn.Write([]byte("Hello from target server"))
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("server failed to write: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Wait a bit to ensure the client has time to read before closing
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
errCh <- nil
|
||||
}()
|
||||
|
||||
// Test establishing proxy tunnel with timeout
|
||||
conn, err := establishHTTPProxyTunnel(proxy.url(), targetHost, 10*time.Second, _EMPTY_, _EMPTY_)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to establish proxy tunnel: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Test that we can communicate through the tunnel with deadlines
|
||||
conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
|
||||
_, err = conn.Write([]byte("Hello"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write to proxy tunnel: %v", err)
|
||||
}
|
||||
|
||||
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
buffer := make([]byte, 1024)
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read from proxy tunnel: %v", err)
|
||||
}
|
||||
|
||||
response := string(buffer[:n])
|
||||
if response != "Hello from target server" {
|
||||
t.Errorf("Unexpected response: '%s', expected 'Hello from target server'", response)
|
||||
}
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("Server goroutine didn't complete in time, but test data was exchanged successfully")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLeafNodeHttpProxyTunnelWithAuth(t *testing.T) {
|
||||
// Create HTTP proxy with authentication and delay for robustness
|
||||
proxy := createTestHTTPProxy("testuser", "testpass")
|
||||
proxy.setCloseDelay(200 * time.Millisecond)
|
||||
proxy.start()
|
||||
defer proxy.stop()
|
||||
|
||||
// Create a simple TCP server to connect to through proxy
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test server: %v", err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
targetPort := listener.Addr().(*net.TCPAddr).Port
|
||||
targetHost := fmt.Sprintf("127.0.0.1:%d", targetPort)
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
|
||||
// Accept one connection with proper timeout handling
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("unable to accept: %v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Set read deadline to prevent hanging
|
||||
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
|
||||
// Read the incoming data first
|
||||
buffer := make([]byte, 1024)
|
||||
_, err = conn.Read(buffer)
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("unable to read: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Send response
|
||||
conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
|
||||
if _, err := conn.Write([]byte("Hello from authenticated server")); err != nil {
|
||||
errCh <- fmt.Errorf("unable to write: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
errCh <- nil
|
||||
}()
|
||||
|
||||
// Test establishing proxy tunnel with authentication and timeout
|
||||
conn, err := establishHTTPProxyTunnel(proxy.url(), targetHost, 10*time.Second, "testuser", "testpass")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to establish proxy tunnel with auth: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Test that we can communicate through the tunnel with deadlines
|
||||
conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
|
||||
_, err = conn.Write([]byte("Hello"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write to proxy tunnel: %v", err)
|
||||
}
|
||||
|
||||
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
buffer := make([]byte, 1024)
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read from proxy tunnel: %v", err)
|
||||
}
|
||||
|
||||
response := string(buffer[:n])
|
||||
if response != "Hello from authenticated server" {
|
||||
t.Errorf("Unexpected response: %s", response)
|
||||
}
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("Server goroutine didn't complete in time, but test data was exchanged successfully")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLeafNodeHttpProxyTunnelFailsWithoutAuth(t *testing.T) {
|
||||
// Create HTTP proxy with authentication required and delay for robustness
|
||||
proxy := createTestHTTPProxy("testuser", "testpass")
|
||||
proxy.setCloseDelay(200 * time.Millisecond)
|
||||
proxy.start()
|
||||
defer proxy.stop()
|
||||
|
||||
// Try to establish tunnel without providing credentials (should fail quickly)
|
||||
_, err := establishHTTPProxyTunnel(proxy.url(), "127.0.0.1:80", 10*time.Second, _EMPTY_, _EMPTY_)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error when connecting without authentication")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "proxy CONNECT failed") {
|
||||
t.Errorf("Expected proxy authentication error, got: %v", err)
|
||||
}
|
||||
|
||||
// Verify the error contains the expected HTTP response code
|
||||
if !strings.Contains(err.Error(), "407") {
|
||||
t.Errorf("Expected HTTP 407 error in response, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLeafNodeProxyValidationProgrammatic tests proxy validation when configuring server programmatically
|
||||
func TestLeafNodeHttpProxyValidationProgrammatic(t *testing.T) {
|
||||
tests := []struct {
|
||||
// name is the name of the test.
|
||||
name string
|
||||
|
||||
// setupOptions creates the Options configuration for the test.
|
||||
setupOptions func() *Options
|
||||
|
||||
// err is the expected error. nil means no error expected.
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "invalid proxy scheme",
|
||||
setupOptions: func() *Options {
|
||||
opts := &Options{}
|
||||
opts.LeafNode.Remotes = []*RemoteLeafOpts{
|
||||
{
|
||||
URLs: []*url.URL{{Scheme: wsSchemePrefix, Host: "127.0.0.1:7422"}},
|
||||
},
|
||||
}
|
||||
opts.LeafNode.Remotes[0].Proxy.URL = "ftp://proxy.example.com:21"
|
||||
return opts
|
||||
},
|
||||
err: errors.New("proxy URL scheme must be http or https"),
|
||||
},
|
||||
{
|
||||
name: "empty proxy URL - no validation performed",
|
||||
setupOptions: func() *Options {
|
||||
opts := &Options{}
|
||||
opts.LeafNode.Remotes = []*RemoteLeafOpts{
|
||||
{
|
||||
URLs: []*url.URL{{Scheme: wsSchemePrefix, Host: "127.0.0.1:7422"}},
|
||||
},
|
||||
}
|
||||
opts.LeafNode.Remotes[0].Proxy.URL = _EMPTY_
|
||||
return opts
|
||||
},
|
||||
err: nil, // No error expected for empty URL
|
||||
},
|
||||
{
|
||||
name: "invalid proxy URL parse failure",
|
||||
setupOptions: func() *Options {
|
||||
opts := &Options{}
|
||||
opts.LeafNode.Remotes = []*RemoteLeafOpts{
|
||||
{
|
||||
URLs: []*url.URL{{Scheme: wsSchemePrefix, Host: "127.0.0.1:7422"}},
|
||||
},
|
||||
}
|
||||
opts.LeafNode.Remotes[0].Proxy.URL = "ht!tp://invalid-url-with-bad-characters"
|
||||
return opts
|
||||
},
|
||||
err: errors.New("invalid proxy URL"),
|
||||
},
|
||||
{
|
||||
name: "missing proxy host",
|
||||
setupOptions: func() *Options {
|
||||
opts := &Options{}
|
||||
opts.LeafNode.Remotes = []*RemoteLeafOpts{
|
||||
{
|
||||
URLs: []*url.URL{{Scheme: wsSchemePrefix, Host: "127.0.0.1:7422"}},
|
||||
},
|
||||
}
|
||||
opts.LeafNode.Remotes[0].Proxy.URL = "http://"
|
||||
return opts
|
||||
},
|
||||
err: errors.New("proxy URL must specify a host"),
|
||||
},
|
||||
{
|
||||
name: "username without password",
|
||||
setupOptions: func() *Options {
|
||||
opts := &Options{}
|
||||
opts.LeafNode.Remotes = []*RemoteLeafOpts{
|
||||
{
|
||||
URLs: []*url.URL{{Scheme: wsSchemePrefix, Host: "127.0.0.1:7422"}},
|
||||
},
|
||||
}
|
||||
opts.LeafNode.Remotes[0].Proxy.URL = "http://proxy.example.com:8080"
|
||||
opts.LeafNode.Remotes[0].Proxy.Username = "user"
|
||||
return opts
|
||||
},
|
||||
err: errors.New("proxy username and password must both be specified"),
|
||||
},
|
||||
{
|
||||
name: "password without username",
|
||||
setupOptions: func() *Options {
|
||||
opts := &Options{}
|
||||
opts.LeafNode.Remotes = []*RemoteLeafOpts{
|
||||
{
|
||||
URLs: []*url.URL{{Scheme: wsSchemePrefix, Host: "127.0.0.1:7422"}},
|
||||
},
|
||||
}
|
||||
opts.LeafNode.Remotes[0].Proxy.URL = "http://proxy.example.com:8080"
|
||||
opts.LeafNode.Remotes[0].Proxy.Password = "pass"
|
||||
return opts
|
||||
},
|
||||
err: errors.New("proxy username and password must both be specified"),
|
||||
},
|
||||
{
|
||||
name: "negative timeout value",
|
||||
setupOptions: func() *Options {
|
||||
opts := &Options{}
|
||||
opts.LeafNode.Remotes = []*RemoteLeafOpts{
|
||||
{
|
||||
URLs: []*url.URL{{Scheme: wsSchemePrefix, Host: "127.0.0.1:7422"}},
|
||||
},
|
||||
}
|
||||
opts.LeafNode.Remotes[0].Proxy.URL = "http://proxy.example.com:8080"
|
||||
opts.LeafNode.Remotes[0].Proxy.Timeout = -5 * time.Second
|
||||
return opts
|
||||
},
|
||||
err: errors.New("proxy timeout must be >= 0"),
|
||||
},
|
||||
{
|
||||
name: "zero timeout value - valid",
|
||||
setupOptions: func() *Options {
|
||||
opts := &Options{}
|
||||
opts.LeafNode.Remotes = []*RemoteLeafOpts{
|
||||
{
|
||||
URLs: []*url.URL{{Scheme: wsSchemePrefix, Host: "127.0.0.1:7422"}},
|
||||
},
|
||||
}
|
||||
opts.LeafNode.Remotes[0].Proxy.URL = "http://proxy.example.com:8080"
|
||||
opts.LeafNode.Remotes[0].Proxy.Timeout = 0
|
||||
return opts
|
||||
},
|
||||
err: nil, // No error expected for zero timeout
|
||||
},
|
||||
{
|
||||
name: "valid proxy configuration with authentication",
|
||||
setupOptions: func() *Options {
|
||||
opts := &Options{}
|
||||
opts.LeafNode.Remotes = []*RemoteLeafOpts{
|
||||
{
|
||||
URLs: []*url.URL{{Scheme: wsSchemePrefix, Host: "127.0.0.1:7422"}},
|
||||
},
|
||||
}
|
||||
opts.LeafNode.Remotes[0].Proxy.URL = "http://proxy.example.com:8080"
|
||||
opts.LeafNode.Remotes[0].Proxy.Username = "user"
|
||||
opts.LeafNode.Remotes[0].Proxy.Password = "pass"
|
||||
opts.LeafNode.Remotes[0].Proxy.Timeout = 10 * time.Second
|
||||
return opts
|
||||
},
|
||||
err: nil, // No error expected
|
||||
},
|
||||
{
|
||||
name: "valid proxy configuration without authentication",
|
||||
setupOptions: func() *Options {
|
||||
opts := &Options{}
|
||||
opts.LeafNode.Remotes = []*RemoteLeafOpts{
|
||||
{
|
||||
URLs: []*url.URL{{Scheme: wsSchemePrefix, Host: "127.0.0.1:7422"}},
|
||||
},
|
||||
}
|
||||
opts.LeafNode.Remotes[0].Proxy.URL = "https://proxy.example.com:3128"
|
||||
opts.LeafNode.Remotes[0].Proxy.Timeout = 30 * time.Second
|
||||
return opts
|
||||
},
|
||||
err: nil, // No error expected
|
||||
},
|
||||
}
|
||||
|
||||
checkErr := func(t *testing.T, err, expectedErr error) {
|
||||
t.Helper()
|
||||
switch {
|
||||
case err == nil && expectedErr == nil:
|
||||
// OK
|
||||
case err != nil && expectedErr == nil:
|
||||
t.Errorf("Unexpected error after validating options: %s", err)
|
||||
case err == nil && expectedErr != nil:
|
||||
t.Errorf("Expected %q error after validating invalid options but got nothing", expectedErr)
|
||||
case err != nil && expectedErr != nil:
|
||||
if !strings.Contains(err.Error(), expectedErr.Error()) {
|
||||
t.Errorf("Expected error containing %q, got: %q", expectedErr.Error(), err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
opts := test.setupOptions()
|
||||
err := validateLeafNode(opts)
|
||||
checkErr(t, err, test.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user