merge: openclaw protocol fix

This commit is contained in:
Haitao Pan 2026-06-01 11:28:03 +08:00
commit 4e0751d7b1
5 changed files with 193 additions and 8 deletions

View File

@ -137,6 +137,7 @@ func applyProductionGatewayRouting(
Host: parsed.Hostname(),
Port: port,
TLS: tls,
Path: parsed.EscapedPath(),
}
request.Auth.Token = bridgeSharedAuthToken()
request.Auth.Password = ""

View 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)
}
}

View File

@ -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

View File

@ -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())
}

View File

@ -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