merge: openclaw protocol fix
This commit is contained in:
commit
4e0751d7b1
@ -137,6 +137,7 @@ func applyProductionGatewayRouting(
|
||||
Host: parsed.Hostname(),
|
||||
Port: port,
|
||||
TLS: tls,
|
||||
Path: parsed.EscapedPath(),
|
||||
}
|
||||
request.Auth.Token = bridgeSharedAuthToken()
|
||||
request.Auth.Password = ""
|
||||
|
||||
37
internal/acp/gateway_test.go
Normal file
37
internal/acp/gateway_test.go
Normal file
@ -0,0 +1,37 @@
|
||||
package acp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"xworkmate-bridge/internal/gatewayruntime"
|
||||
)
|
||||
|
||||
func TestApplyProductionGatewayRoutingPreservesGatewayURLPath(t *testing.T) {
|
||||
t.Setenv("GATEWAY_RPC_URL", "ws://127.0.0.1:18789/gateway/openclaw")
|
||||
server := NewServer()
|
||||
|
||||
request := applyProductionGatewayRouting(
|
||||
server,
|
||||
gatewayruntime.ConnectRequest{
|
||||
Mode: "openclaw",
|
||||
Endpoint: gatewayruntime.Endpoint{
|
||||
Host: "xworkmate-bridge.svc.plus",
|
||||
Port: 443,
|
||||
TLS: true,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if request.Endpoint.Host != "127.0.0.1" {
|
||||
t.Fatalf("expected gateway host from env, got %#v", request.Endpoint)
|
||||
}
|
||||
if request.Endpoint.Port != 18789 {
|
||||
t.Fatalf("expected gateway port from env, got %#v", request.Endpoint)
|
||||
}
|
||||
if request.Endpoint.TLS {
|
||||
t.Fatalf("expected plaintext local gateway endpoint, got %#v", request.Endpoint)
|
||||
}
|
||||
if request.Endpoint.Path != "/gateway/openclaw" {
|
||||
t.Fatalf("expected gateway URL path to be preserved, got %#v", request.Endpoint)
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,9 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@ -340,16 +342,12 @@ func sameConnectTarget(current ConnectRequest, next ConnectRequest) bool {
|
||||
return strings.TrimSpace(current.Mode) == strings.TrimSpace(next.Mode) &&
|
||||
strings.TrimSpace(current.Endpoint.Host) == strings.TrimSpace(next.Endpoint.Host) &&
|
||||
current.Endpoint.Port == next.Endpoint.Port &&
|
||||
current.Endpoint.TLS == next.Endpoint.TLS
|
||||
current.Endpoint.TLS == next.Endpoint.TLS &&
|
||||
normalizeEndpointPath(current.Endpoint.Path) == normalizeEndpointPath(next.Endpoint.Path)
|
||||
}
|
||||
|
||||
func (s *session) connectAttempt() (ConnectResult, *GatewayError) {
|
||||
url := fmt.Sprintf(
|
||||
"%s://%s:%d",
|
||||
resolveRemoteScheme(s.config.Endpoint.TLS),
|
||||
s.config.Endpoint.Host,
|
||||
s.config.Endpoint.Port,
|
||||
)
|
||||
url := remoteEndpointURL(s.config.Endpoint)
|
||||
dialer := websocket.Dialer{
|
||||
HandshakeTimeout: s.manager.ConnectTimeout,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
@ -581,6 +579,25 @@ func (s *session) requestRemoteOnConn(
|
||||
}
|
||||
}
|
||||
|
||||
func remoteEndpointURL(endpoint Endpoint) string {
|
||||
return (&url.URL{
|
||||
Scheme: resolveRemoteScheme(endpoint.TLS),
|
||||
Host: net.JoinHostPort(endpoint.Host, fmt.Sprintf("%d", endpoint.Port)),
|
||||
Path: normalizeEndpointPath(endpoint.Path),
|
||||
}).String()
|
||||
}
|
||||
|
||||
func normalizeEndpointPath(path string) string {
|
||||
trimmed := strings.TrimSpace(path)
|
||||
if trimmed == "" || trimmed == "/" {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "/") {
|
||||
return trimmed
|
||||
}
|
||||
return "/" + trimmed
|
||||
}
|
||||
|
||||
func (s *session) resetConnAfterProtocolError(conn *websocket.Conn, err *GatewayError) {
|
||||
if conn == nil {
|
||||
return
|
||||
|
||||
@ -2,6 +2,7 @@ package gatewayruntime
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
@ -57,6 +58,96 @@ func TestManagerConnectAndRequest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerConnectAdvertisesCurrentOpenClawProtocol(t *testing.T) {
|
||||
server := newFakeGatewayServer(t)
|
||||
server.expectedProtocol = defaultProtocolVersion
|
||||
defer server.Close()
|
||||
|
||||
manager := NewManager()
|
||||
result := manager.Connect(buildTestConnectRequest(server.Port()), func(map[string]any) {})
|
||||
if !result.OK {
|
||||
t.Fatalf("expected connect success, got %#v", result.Error)
|
||||
}
|
||||
|
||||
params := server.LastConnectParams()
|
||||
if params["minProtocol"] != float64(defaultProtocolVersion) {
|
||||
t.Fatalf("expected minProtocol %d, got %#v", defaultProtocolVersion, params["minProtocol"])
|
||||
}
|
||||
if params["maxProtocol"] != float64(defaultProtocolVersion) {
|
||||
t.Fatalf("expected maxProtocol %d, got %#v", defaultProtocolVersion, params["maxProtocol"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayFakeRejectsProtocol3AndAcceptsCurrentProtocol(t *testing.T) {
|
||||
server := newFakeGatewayServer(t)
|
||||
server.expectedProtocol = defaultProtocolVersion
|
||||
defer server.Close()
|
||||
|
||||
manager := NewManager()
|
||||
result := manager.Connect(buildTestConnectRequest(server.Port()), func(map[string]any) {})
|
||||
if !result.OK {
|
||||
t.Fatalf("expected current bridge protocol to connect, got %#v", result.Error)
|
||||
}
|
||||
|
||||
conn, _, err := websocket.DefaultDialer.Dial(
|
||||
fmt.Sprintf("ws://127.0.0.1:%d", server.Port()),
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("dial fake gateway: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = conn.Close()
|
||||
}()
|
||||
|
||||
var challenge map[string]any
|
||||
if err := conn.ReadJSON(&challenge); err != nil {
|
||||
t.Fatalf("read challenge: %v", err)
|
||||
}
|
||||
if challenge["event"] != "connect.challenge" {
|
||||
t.Fatalf("expected connect challenge, got %#v", challenge)
|
||||
}
|
||||
|
||||
if err := conn.WriteJSON(map[string]any{
|
||||
"type": "req",
|
||||
"id": "legacy-connect",
|
||||
"method": "connect",
|
||||
"params": map[string]any{
|
||||
"minProtocol": float64(3),
|
||||
"maxProtocol": float64(3),
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("write legacy connect: %v", err)
|
||||
}
|
||||
|
||||
var response map[string]any
|
||||
if err := conn.ReadJSON(&response); err != nil {
|
||||
t.Fatalf("read legacy connect response: %v", err)
|
||||
}
|
||||
if response["ok"] != false {
|
||||
t.Fatalf("expected protocol 3 rejection, got %#v", response)
|
||||
}
|
||||
errorPayload := asMap(response["error"])
|
||||
if stringValue(errorPayload["message"]) != "protocol mismatch" {
|
||||
t.Fatalf("expected protocol mismatch error, got %#v", response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerConnectPreservesEndpointPath(t *testing.T) {
|
||||
server := newFakeGatewayServer(t)
|
||||
server.expectedPath = "/gateway/openclaw"
|
||||
defer server.Close()
|
||||
|
||||
manager := NewManager()
|
||||
request := buildTestConnectRequest(server.Port())
|
||||
request.Endpoint.Path = "/gateway/openclaw"
|
||||
|
||||
result := manager.Connect(request, func(map[string]any) {})
|
||||
if !result.OK {
|
||||
t.Fatalf("expected connect success through path-scoped endpoint, got %#v", result.Error)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerReconnectsAfterSocketClose(t *testing.T) {
|
||||
server := newFakeGatewayServer(t)
|
||||
server.closeAfterConnect.Store(true)
|
||||
@ -281,6 +372,9 @@ type fakeGatewayServer struct {
|
||||
invalidNextRequest atomic.Bool
|
||||
connectErrorCode string
|
||||
connectErrorDetailCode string
|
||||
expectedProtocol int
|
||||
expectedPath string
|
||||
lastConnectParams atomic.Value
|
||||
}
|
||||
|
||||
func newFakeGatewayServer(t *testing.T) *fakeGatewayServer {
|
||||
@ -293,6 +387,10 @@ func newFakeGatewayServer(t *testing.T) *fakeGatewayServer {
|
||||
upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }}
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if fake.expectedPath != "" && r.URL.Path != fake.expectedPath {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return
|
||||
@ -352,6 +450,29 @@ func newFakeGatewayServer(t *testing.T) *fakeGatewayServer {
|
||||
case "connect":
|
||||
fake.connectCount.Add(1)
|
||||
connected = true
|
||||
params := asMap(frame["params"])
|
||||
fake.lastConnectParams.Store(params)
|
||||
if fake.expectedProtocol > 0 &&
|
||||
(params["minProtocol"] != float64(fake.expectedProtocol) ||
|
||||
params["maxProtocol"] != float64(fake.expectedProtocol)) {
|
||||
_ = conn.WriteJSON(map[string]any{
|
||||
"type": "res",
|
||||
"id": id,
|
||||
"ok": false,
|
||||
"error": map[string]any{
|
||||
"code": "INVALID_REQUEST",
|
||||
"message": "protocol mismatch",
|
||||
"details": map[string]any{
|
||||
"code": "PROTOCOL_MISMATCH",
|
||||
"clientMinProtocol": params["minProtocol"],
|
||||
"clientMaxProtocol": params["maxProtocol"],
|
||||
"expectedProtocol": fake.expectedProtocol,
|
||||
"minimumProbeProtocol": fake.expectedProtocol,
|
||||
},
|
||||
},
|
||||
})
|
||||
continue
|
||||
}
|
||||
if fake.connectErrorCode != "" {
|
||||
_ = conn.WriteJSON(map[string]any{
|
||||
"type": "res",
|
||||
@ -419,6 +540,15 @@ func (f *fakeGatewayServer) Port() int {
|
||||
return f.listener.Addr().(*net.TCPAddr).Port
|
||||
}
|
||||
|
||||
func (f *fakeGatewayServer) LastConnectParams() map[string]any {
|
||||
value := f.lastConnectParams.Load()
|
||||
if value == nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
params, _ := value.(map[string]any)
|
||||
return params
|
||||
}
|
||||
|
||||
func (f *fakeGatewayServer) ConnectCount() int {
|
||||
return int(f.connectCount.Load())
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ package gatewayruntime
|
||||
import "time"
|
||||
|
||||
const (
|
||||
defaultProtocolVersion = 3
|
||||
defaultProtocolVersion = 4
|
||||
defaultReconnectDelay = 2 * time.Second
|
||||
defaultConnectTimeout = 10 * time.Second
|
||||
defaultChallengeWait = 2 * time.Second
|
||||
|
||||
Loading…
Reference in New Issue
Block a user