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));