refactor(webclient): enhance pipelining with validation and cache unification

Enhancements: - Add isValidUpgradeRequest() to validate pipelined upgrade requests (checks GET prefix, CRLF suffix, Upgrade header presence) - Apply DNS cache to handleSDKConnect for WebSocket connections (previously only rdDialer used the cache) - Remove dead _sendWebSocketUpgrade() method (replaced by _buildUpgradeRequest) Safety: Invalid upgrade requests are now logged and skipped rather than blindly forwarded, preventing potential protocol confusion attacks.

cognitive-glitch committed Dec 9, 2025 at 14:56 UTC 64c2fadc4179330415d3e5d7d0cf15ac060da70b
2 files changed +56 -63
cmd/webclient/main_js.go
+56 -25
@@ -105,6 +105,26 @@ func storeDNSCache(name, leaseID string) {
105 })
106 }
107
108 +// isValidUpgradeRequest validates that the upgrade request is a well-formed HTTP WebSocket upgrade
109 +func isValidUpgradeRequest(req []byte) bool {
110 + if len(req) < 20 { // Minimum: "GET / HTTP/1.1\r\n\r\n"
111 + return false
112 + }
113 + s := string(req)
114 + // Must start with GET and end with double CRLF
115 + if !strings.HasPrefix(s, "GET ") {
116 + return false
117 + }
118 + if !strings.HasSuffix(s, "\r\n\r\n") {
119 + return false
120 + }
121 + // Must contain Upgrade header (case-insensitive check)
122 + if !strings.Contains(strings.ToLower(s), "upgrade:") {
123 + return false
124 + }
125 + return true
126 +}
127 +
128 var rdDialer = func(ctx context.Context, network, address string) (net.Conn, error) {
129 originalAddr := address
130 address = strings.TrimSuffix(address, ":80")
@@ -681,21 +701,28 @@ func handleSDKConnect(data js.Value) {
701 normalizedLeaseName := getLeaseID(leaseName)
702 log.Debug().Str("original", leaseName).Str("normalized", normalizedLeaseName).Msg("[SDK Connect] Normalized lease name")
703
684 - // Lookup lease by name to get lease ID
685 - lease, err := client.LookupName(normalizedLeaseName)
686 - if err != nil {
687 - log.Error().Err(err).Str("leaseName", leaseName).Msg("[SDK Connect] Lease lookup failed")
688 - js.Global().Call("__sdk_post_message", map[string]interface{}{
689 - "type": "SDK_CONNECT_ERROR",
690 - "clientId": clientId,
691 - "error": err.Error(),
692 - })
693 - return
704 + // Check DNS cache first, then lookup if needed
705 + var leaseID string
706 + if cachedID, ok := lookupDNSCache(normalizedLeaseName); ok {
707 + log.Debug().Str("name", normalizedLeaseName).Str("id", cachedID).Msg("[SDK Connect] DNS cache hit")
708 + leaseID = cachedID
709 + } else {
710 + // Cache miss - perform lookup
711 + lease, err := client.LookupName(normalizedLeaseName)
712 + if err != nil {
713 + log.Error().Err(err).Str("leaseName", leaseName).Msg("[SDK Connect] Lease lookup failed")
714 + js.Global().Call("__sdk_post_message", map[string]interface{}{
715 + "type": "SDK_CONNECT_ERROR",
716 + "clientId": clientId,
717 + "error": err.Error(),
718 + })
719 + return
720 + }
721 + leaseID = lease.GetIdentity().GetId()
722 + storeDNSCache(normalizedLeaseName, leaseID)
723 + log.Info().Str("leaseName", leaseName).Str("leaseID", leaseID).Msg("[SDK Connect] Lease found, cached")
724 }
725
696 - leaseID := lease.GetIdentity().GetId()
697 - log.Info().Str("leaseName", leaseName).Str("leaseID", leaseID).Msg("[SDK Connect] Lease found")
698 -
726 // Create E2EE connection using SDK with lease ID
727 cred := sdk.NewCredential()
728 conn, err := client.Dial(cred, leaseID, "http/1.1")
@@ -711,20 +738,24 @@ func handleSDKConnect(data js.Value) {
738 return
739 }
740
714 - // If pipelined upgrade request is present, send it immediately (saves 1 RTT)
741 + // If pipelined upgrade request is present, validate and send it immediately (saves 1 RTT)
742 if len(upgradeRequest) > 0 {
716 - _, err := conn.Write(upgradeRequest)
717 - if err != nil {
718 - log.Error().Err(err).Str("leaseID", leaseID).Msg("[SDK Connect] Failed to send pipelined upgrade request")
719 - conn.Close()
720 - js.Global().Call("__sdk_post_message", map[string]interface{}{
721 - "type": "SDK_CONNECT_ERROR",
722 - "clientId": clientId,
723 - "error": err.Error(),
724 - })
725 - return
743 + if !isValidUpgradeRequest(upgradeRequest) {
744 + log.Warn().Str("leaseID", leaseID).Msg("[SDK Connect] Invalid pipelined upgrade request, skipping")
745 + } else {
746 + _, err := conn.Write(upgradeRequest)
747 + if err != nil {
748 + log.Error().Err(err).Str("leaseID", leaseID).Msg("[SDK Connect] Failed to send pipelined upgrade request")
749 + conn.Close()
750 + js.Global().Call("__sdk_post_message", map[string]interface{}{
751 + "type": "SDK_CONNECT_ERROR",
752 + "clientId": clientId,
753 + "error": err.Error(),
754 + })
755 + return
756 + }
757 + log.Debug().Str("leaseID", leaseID).Msg("[SDK Connect] Pipelined upgrade request sent")
758 }
727 - log.Debug().Str("leaseID", leaseID).Msg("[SDK Connect] Pipelined upgrade request sent")
759 }
760
761 // Generate connection ID
cmd/webclient/polyfill.js
-38
@@ -266,44 +266,6 @@
266 debugLog("[WebSocket Polyfill] E2EE tunnel established, upgrade request already sent (pipelined)");
267 }
268
269 - _sendWebSocketUpgrade() {
270 - // Parse URL to get path with query parameters
271 - const path = (this._parsedUrl.pathname || "/") + (this._parsedUrl.search || "");
272 - const host = this._parsedUrl.host;
273 -
274 - // Build HTTP Upgrade request
275 - let upgradeRequest = `GET ${path} HTTP/1.1\r\n`;
276 - upgradeRequest += `Host: ${host}\r\n`;
277 - upgradeRequest += `Upgrade: websocket\r\n`;
278 - upgradeRequest += `Connection: Upgrade\r\n`;
279 - upgradeRequest += `Sec-WebSocket-Key: ${this._wsKey}\r\n`;
280 - upgradeRequest += `Sec-WebSocket-Version: 13\r\n`;
281 -
282 - if (this._protocols) {
283 - const protocolStr = Array.isArray(this._protocols)
284 - ? this._protocols.join(', ')
285 - : this._protocols;
286 - upgradeRequest += `Sec-WebSocket-Protocol: ${protocolStr}\r\n`;
287 - }
288 -
289 - upgradeRequest += `\r\n`;
290 -
291 - debugLog("[WebSocket Polyfill] Sending upgrade request:", upgradeRequest);
292 -
293 - // Convert to bytes and send
294 - const encoder = new TextEncoder();
295 - const bytes = encoder.encode(upgradeRequest);
296 -
297 - this._postToServiceWorker({
298 - type: "SDK_SEND",
299 - data: bytes,
300 - });
301 -
302 - // Wait for upgrade response in _handleData
303 - this._waitingForUpgrade = true;
304 - this._upgradeBuffer = new Uint8Array(0);
305 - }
306 -
269 _handleConnectError(data) {
270 console.error("[WebSocket Polyfill] Connection error:", data.error);
271 this._handleError(new Error(data.error));