Add WebSocket Polyfill
lemon-mint committed
Nov 1, 2025 at 01:49 UTC
5683d8c9908e6f0cb8580c4ff6c76d706442de33
4 files changed
+842
-2
cmd/webclient/inject.go
+6
-1
@@ -5,8 +5,13 @@ import (
5
6
"github.com/rs/zerolog/log"
7
"golang.org/x/net/html"
8
+
9
+ _ "embed"
10
)
11
12
+//go:embed polyfill.js
13
+var polyfillJS []byte
14
+
15
func InjectHTML(body []byte) []byte {
16
doc, err := html.Parse(bytes.NewReader(body))
17
if err != nil {
@@ -39,7 +44,7 @@ func InjectHTML(body []byte) []byte {
44
// Add the script content
45
scriptContent := &html.Node{
46
Type: html.TextNode,
42
- Data: `var PORTAL_VERSION = "v1.0.0";`,
47
+ Data: string(polyfillJS),
48
}
49
script.AppendChild(scriptContent)
50
cmd/webclient/main_js.go
+285
-1
@@ -2,6 +2,10 @@ package main
2
3
import (
4
"context"
5
+ "crypto/rand"
6
+ "encoding/base64"
7
+ "encoding/hex"
8
+ "encoding/json"
9
"fmt"
10
"io"
11
"mime"
@@ -10,10 +14,12 @@ import (
14
"os"
15
"runtime"
16
"strings"
17
+ "sync"
18
"syscall/js"
19
"time"
20
21
"github.com/gosuda/portal/cmd/webclient/httpjs"
22
+ "github.com/gosuda/portal/cmd/webclient/wsjs"
23
"github.com/gosuda/portal/sdk"
24
"github.com/rs/zerolog"
25
"github.com/rs/zerolog/log"
@@ -44,6 +50,125 @@ var client = &http.Client{
50
}
51
52
type Proxy struct {
53
+ wsManager *WebSocketManager
54
+}
55
+
56
+// WebSocket connection manager
57
+type WebSocketManager struct {
58
+ connections sync.Map // map[string]*WSConnection
59
+}
60
+
61
+type WSConnection struct {
62
+ id string
63
+ conn *wsjs.Conn
64
+ messageChan chan []byte
65
+ closeChan chan struct{}
66
+ closeOnce sync.Once
67
+ mu sync.Mutex
68
+}
69
+
70
+type ConnectRequest struct {
71
+ URL string `json:"url"`
72
+ Protocols []string `json:"protocols"`
73
+}
74
+
75
+type ConnectResponse struct {
76
+ ConnID string `json:"connId"`
77
+ Protocol string `json:"protocol"`
78
+}
79
+
80
+type SendRequest struct {
81
+ Type string `json:"type"` // "text", "binary", "close"
82
+ Data string `json:"data,omitempty"`
83
+ Code int `json:"code,omitempty"`
84
+ Reason string `json:"reason,omitempty"`
85
+}
86
+
87
+type StreamMessage struct {
88
+ Type string `json:"type"` // "message", "close"
89
+ Data string `json:"data,omitempty"`
90
+ Code int `json:"code,omitempty"`
91
+ Reason string `json:"reason,omitempty"`
92
+}
93
+
94
+func NewWebSocketManager() *WebSocketManager {
95
+ return &WebSocketManager{}
96
+}
97
+
98
+func generateConnID() string {
99
+ b := make([]byte, 16)
100
+ rand.Read(b)
101
+ return hex.EncodeToString(b)
102
+}
103
+
104
+func (m *WebSocketManager) CreateConnection(url string) (*WSConnection, error) {
105
+ conn, err := wsjs.Dial(url)
106
+ if err != nil {
107
+ return nil, err
108
+ }
109
+
110
+ wsConn := &WSConnection{
111
+ id: generateConnID(),
112
+ conn: conn,
113
+ messageChan: make(chan []byte, 100),
114
+ closeChan: make(chan struct{}),
115
+ }
116
+
117
+ m.connections.Store(wsConn.id, wsConn)
118
+
119
+ // Start message receiver
120
+ go wsConn.receiveMessages()
121
+
122
+ return wsConn, nil
123
+}
124
+
125
+func (m *WebSocketManager) GetConnection(id string) (*WSConnection, bool) {
126
+ conn, ok := m.connections.Load(id)
127
+ if !ok {
128
+ return nil, false
129
+ }
130
+ return conn.(*WSConnection), true
131
+}
132
+
133
+func (m *WebSocketManager) RemoveConnection(id string) {
134
+ m.connections.Delete(id)
135
+}
136
+
137
+func (c *WSConnection) receiveMessages() {
138
+ defer c.Close()
139
+
140
+ for {
141
+ msg, err := c.conn.NextMessage()
142
+ if err != nil {
143
+ log.Error().Err(err).Str("connId", c.id).Msg("Error receiving message")
144
+ return
145
+ }
146
+
147
+ select {
148
+ case c.messageChan <- msg:
149
+ case <-c.closeChan:
150
+ return
151
+ }
152
+ }
153
+}
154
+
155
+func (c *WSConnection) Send(data []byte) error {
156
+ c.mu.Lock()
157
+ defer c.mu.Unlock()
158
+
159
+ select {
160
+ case <-c.closeChan:
161
+ return fmt.Errorf("connection closed")
162
+ default:
163
+ return c.conn.Send(data)
164
+ }
165
+}
166
+
167
+func (c *WSConnection) Close() {
168
+ c.closeOnce.Do(func() {
169
+ close(c.closeChan)
170
+ c.conn.Close()
171
+ })
172
}
173
174
// IsHTMLContentType checks if the Content-Type header indicates HTML content
@@ -65,6 +190,12 @@ func IsHTMLContentType(contentType string) bool {
190
}
191
192
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
193
+ // Handle WebSocket polyfill endpoints
194
+ if strings.HasPrefix(r.URL.Path, "/sw-cgi/websocket/") {
195
+ p.handleWebSocketPolyfill(w, r)
196
+ return
197
+ }
198
+
199
log.Info().Msgf("Proxying request to %s", r.URL.String())
200
201
host, err := idna.ToUnicode(r.URL.Hostname())
@@ -107,6 +238,153 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
238
io.Copy(w, resp.Body)
239
}
240
241
+func (p *Proxy) handleWebSocketPolyfill(w http.ResponseWriter, r *http.Request) {
242
+ path := r.URL.Path
243
+
244
+ if path == "/sw-cgi/websocket/connect" && r.Method == http.MethodPost {
245
+ p.handleConnect(w, r)
246
+ return
247
+ }
248
+
249
+ if strings.HasPrefix(path, "/sw-cgi/websocket/stream/") && r.Method == http.MethodGet {
250
+ connID := strings.TrimPrefix(path, "/sw-cgi/websocket/stream/")
251
+ p.handleStream(w, r, connID)
252
+ return
253
+ }
254
+
255
+ if strings.HasPrefix(path, "/sw-cgi/websocket/send/") && r.Method == http.MethodPost {
256
+ connID := strings.TrimPrefix(path, "/sw-cgi/websocket/send/")
257
+ p.handleSend(w, r, connID)
258
+ return
259
+ }
260
+
261
+ http.Error(w, "Not found", http.StatusNotFound)
262
+}
263
+
264
+func (p *Proxy) handleConnect(w http.ResponseWriter, r *http.Request) {
265
+ var req ConnectRequest
266
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
267
+ http.Error(w, "Invalid request", http.StatusBadRequest)
268
+ return
269
+ }
270
+
271
+ log.Info().Str("url", req.URL).Msg("Creating WebSocket connection")
272
+
273
+ wsConn, err := p.wsManager.CreateConnection(req.URL)
274
+ if err != nil {
275
+ log.Error().Err(err).Msg("Failed to create WebSocket connection")
276
+ http.Error(w, fmt.Sprintf("Failed to connect: %v", err), http.StatusBadGateway)
277
+ return
278
+ }
279
+
280
+ resp := ConnectResponse{
281
+ ConnID: wsConn.id,
282
+ Protocol: "", // TODO: handle protocol negotiation
283
+ }
284
+
285
+ w.Header().Set("Content-Type", "application/json")
286
+ json.NewEncoder(w).Encode(resp)
287
+}
288
+
289
+func (p *Proxy) handleStream(w http.ResponseWriter, r *http.Request, connID string) {
290
+ wsConn, ok := p.wsManager.GetConnection(connID)
291
+ if !ok {
292
+ http.Error(w, "Connection not found", http.StatusNotFound)
293
+ return
294
+ }
295
+
296
+ log.Info().Str("connId", connID).Msg("Starting message stream")
297
+
298
+ // Set headers for streaming
299
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
300
+ w.Header().Set("Cache-Control", "no-cache")
301
+ w.Header().Set("Connection", "keep-alive")
302
+ w.Header().Set("X-Content-Type-Options", "nosniff")
303
+
304
+ flusher, ok := w.(http.Flusher)
305
+ if !ok {
306
+ http.Error(w, "Streaming not supported", http.StatusInternalServerError)
307
+ return
308
+ }
309
+
310
+ // Send messages as newline-delimited JSON
311
+ encoder := json.NewEncoder(w)
312
+ for {
313
+ select {
314
+ case msg := <-wsConn.messageChan:
315
+ streamMsg := StreamMessage{
316
+ Type: "message",
317
+ Data: base64.StdEncoding.EncodeToString(msg),
318
+ }
319
+ if err := encoder.Encode(streamMsg); err != nil {
320
+ log.Error().Err(err).Msg("Failed to encode message")
321
+ return
322
+ }
323
+ flusher.Flush()
324
+
325
+ case <-wsConn.closeChan:
326
+ streamMsg := StreamMessage{
327
+ Type: "close",
328
+ Code: 1000,
329
+ Reason: "Connection closed",
330
+ }
331
+ encoder.Encode(streamMsg)
332
+ flusher.Flush()
333
+ return
334
+
335
+ case <-r.Context().Done():
336
+ log.Info().Str("connId", connID).Msg("Stream context cancelled")
337
+ return
338
+ }
339
+ }
340
+}
341
+
342
+func (p *Proxy) handleSend(w http.ResponseWriter, r *http.Request, connID string) {
343
+ wsConn, ok := p.wsManager.GetConnection(connID)
344
+ if !ok {
345
+ http.Error(w, "Connection not found", http.StatusNotFound)
346
+ return
347
+ }
348
+
349
+ var req SendRequest
350
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
351
+ http.Error(w, "Invalid request", http.StatusBadRequest)
352
+ return
353
+ }
354
+
355
+ if req.Type == "close" {
356
+ log.Info().Str("connId", connID).Msg("Closing WebSocket connection")
357
+ wsConn.Close()
358
+ p.wsManager.RemoveConnection(connID)
359
+ w.WriteHeader(http.StatusOK)
360
+ return
361
+ }
362
+
363
+ var data []byte
364
+ var err error
365
+
366
+ if req.Type == "binary" {
367
+ data, err = base64.StdEncoding.DecodeString(req.Data)
368
+ if err != nil {
369
+ http.Error(w, "Invalid base64 data", http.StatusBadRequest)
370
+ return
371
+ }
372
+ } else if req.Type == "text" {
373
+ data = []byte(req.Data)
374
+ } else {
375
+ http.Error(w, "Invalid message type", http.StatusBadRequest)
376
+ return
377
+ }
378
+
379
+ if err := wsConn.Send(data); err != nil {
380
+ log.Error().Err(err).Msg("Failed to send message")
381
+ http.Error(w, fmt.Sprintf("Failed to send: %v", err), http.StatusInternalServerError)
382
+ return
383
+ }
384
+
385
+ w.WriteHeader(http.StatusOK)
386
+}
387
+
388
func main() {
389
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339})
390
var err error
@@ -120,6 +398,12 @@ func main() {
398
}
399
defer rdClient.Close()
400
401
+ // Initialize WebSocket manager
402
+ wsManager := NewWebSocketManager()
403
+ proxy := &Proxy{
404
+ wsManager: wsManager,
405
+ }
406
+
407
// Expose HTTP handler to JavaScript as __go_jshttp
408
js.Global().Set("__go_jshttp", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
409
if len(args) < 1 {
@@ -128,7 +412,7 @@ func main() {
412
}
413
414
jsReq := args[0]
131
- return httpjs.ServeHTTPAsyncWithStreaming(&Proxy{}, jsReq)
415
+ return httpjs.ServeHTTPAsyncWithStreaming(proxy, jsReq)
416
}))
417
log.Info().Msg("Portal proxy handler registered as __go_jshttp")
418
cmd/webclient/polyfill.js
new
+348
@@ -0,0 +1,348 @@
1
+(function() {
2
+ 'use strict';
3
+
4
+ // Save original WebSocket
5
+ const NativeWebSocket = window.WebSocket;
6
+
7
+ // WebSocket polyfill using HTTP
8
+ class WebSocketPolyfill {
9
+ constructor(url, protocols) {
10
+ this.url = url;
11
+ this.protocols = protocols;
12
+ this.readyState = WebSocket.CONNECTING;
13
+ this.bufferedAmount = 0;
14
+ this.extensions = '';
15
+ this.protocol = '';
16
+ this.binaryType = 'blob';
17
+
18
+ // Event handlers
19
+ this.onopen = null;
20
+ this.onmessage = null;
21
+ this.onerror = null;
22
+ this.onclose = null;
23
+
24
+ // Internal state
25
+ this._connId = null;
26
+ this._sendQueue = [];
27
+ this._isSending = false;
28
+ this._streamAbortController = null;
29
+ this._isClosed = false;
30
+
31
+ // Initialize connection
32
+ this._connect();
33
+ }
34
+
35
+ async _connect() {
36
+ try {
37
+ // Send connect request
38
+ const response = await fetch('/sw-cgi/websocket/connect', {
39
+ method: 'POST',
40
+ headers: {
41
+ 'Content-Type': 'application/json',
42
+ },
43
+ body: JSON.stringify({
44
+ url: this.url,
45
+ protocols: Array.isArray(this.protocols) ? this.protocols : (this.protocols ? [this.protocols] : [])
46
+ })
47
+ });
48
+
49
+ if (!response.ok) {
50
+ throw new Error(`Connection failed: ${response.status} ${response.statusText}`);
51
+ }
52
+
53
+ const result = await response.json();
54
+ this._connId = result.connId;
55
+ this.protocol = result.protocol || '';
56
+
57
+ // Update state
58
+ this.readyState = WebSocket.OPEN;
59
+
60
+ // Fire onopen event
61
+ if (this.onopen) {
62
+ this.onopen(new Event('open'));
63
+ }
64
+ this.dispatchEvent(new Event('open'));
65
+
66
+ // Start receiving messages
67
+ this._startStream();
68
+
69
+ } catch (error) {
70
+ this._handleError(error);
71
+ }
72
+ }
73
+
74
+ async _startStream() {
75
+ if (!this._connId || this._isClosed) return;
76
+
77
+ try {
78
+ this._streamAbortController = new AbortController();
79
+
80
+ const response = await fetch(`/sw-cgi/websocket/stream/${this._connId}`, {
81
+ method: 'GET',
82
+ signal: this._streamAbortController.signal
83
+ });
84
+
85
+ if (!response.ok) {
86
+ throw new Error(`Stream failed: ${response.status}`);
87
+ }
88
+
89
+ const reader = response.body.getReader();
90
+ const decoder = new TextDecoder();
91
+ let buffer = '';
92
+
93
+ while (!this._isClosed) {
94
+ const { done, value } = await reader.read();
95
+
96
+ if (done) {
97
+ // Connection closed by server
98
+ this._handleClose(1000, 'Connection closed by server');
99
+ break;
100
+ }
101
+
102
+ // Decode chunk
103
+ buffer += decoder.decode(value, { stream: true });
104
+
105
+ // Process complete messages (assuming newline-delimited JSON)
106
+ let newlineIndex;
107
+ while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
108
+ const line = buffer.slice(0, newlineIndex);
109
+ buffer = buffer.slice(newlineIndex + 1);
110
+
111
+ if (line.trim()) {
112
+ try {
113
+ const message = JSON.parse(line);
114
+ this._handleMessage(message);
115
+ } catch (e) {
116
+ console.error('Failed to parse message:', e);
117
+ }
118
+ }
119
+ }
120
+ }
121
+
122
+ } catch (error) {
123
+ if (error.name !== 'AbortError') {
124
+ this._handleError(error);
125
+ }
126
+ }
127
+ }
128
+
129
+ _handleMessage(message) {
130
+ if (this.readyState !== WebSocket.OPEN) return;
131
+
132
+ if (message.type === 'close') {
133
+ this._handleClose(message.code || 1000, message.reason || '');
134
+ return;
135
+ }
136
+
137
+ // Create MessageEvent
138
+ const event = new MessageEvent('message', {
139
+ data: message.data,
140
+ origin: new URL(this.url).origin
141
+ });
142
+
143
+ if (this.onmessage) {
144
+ this.onmessage(event);
145
+ }
146
+ this.dispatchEvent(event);
147
+ }
148
+
149
+ _handleError(error) {
150
+ console.error('WebSocket error:', error);
151
+
152
+ const event = new Event('error');
153
+ event.error = error;
154
+
155
+ if (this.onerror) {
156
+ this.onerror(event);
157
+ }
158
+ this.dispatchEvent(event);
159
+
160
+ // Close connection after error
161
+ this._handleClose(1006, error.message);
162
+ }
163
+
164
+ _handleClose(code, reason) {
165
+ if (this._isClosed) return;
166
+
167
+ this._isClosed = true;
168
+ this.readyState = WebSocket.CLOSED;
169
+
170
+ // Abort stream
171
+ if (this._streamAbortController) {
172
+ this._streamAbortController.abort();
173
+ }
174
+
175
+ // Create CloseEvent
176
+ const event = new CloseEvent('close', {
177
+ code: code,
178
+ reason: reason,
179
+ wasClean: code === 1000
180
+ });
181
+
182
+ if (this.onclose) {
183
+ this.onclose(event);
184
+ }
185
+ this.dispatchEvent(event);
186
+ }
187
+
188
+ async send(data) {
189
+ if (this.readyState !== WebSocket.OPEN) {
190
+ throw new Error('WebSocket is not open');
191
+ }
192
+
193
+ // Add to queue
194
+ this._sendQueue.push(data);
195
+
196
+ // Process queue
197
+ this._processSendQueue();
198
+ }
199
+
200
+ async _processSendQueue() {
201
+ // Ensure only one send operation at a time
202
+ if (this._isSending || this._sendQueue.length === 0) {
203
+ return;
204
+ }
205
+
206
+ this._isSending = true;
207
+
208
+ while (this._sendQueue.length > 0 && !this._isClosed) {
209
+ const data = this._sendQueue.shift();
210
+
211
+ try {
212
+ let payload;
213
+
214
+ if (typeof data === 'string') {
215
+ payload = JSON.stringify({ type: 'text', data: data });
216
+ } else if (data instanceof ArrayBuffer) {
217
+ // Convert ArrayBuffer to base64
218
+ const bytes = new Uint8Array(data);
219
+ const base64 = btoa(String.fromCharCode(...bytes));
220
+ payload = JSON.stringify({ type: 'binary', data: base64 });
221
+ } else if (data instanceof Blob) {
222
+ // Convert Blob to base64
223
+ const arrayBuffer = await data.arrayBuffer();
224
+ const bytes = new Uint8Array(arrayBuffer);
225
+ const base64 = btoa(String.fromCharCode(...bytes));
226
+ payload = JSON.stringify({ type: 'binary', data: base64 });
227
+ } else {
228
+ throw new Error('Unsupported data type');
229
+ }
230
+
231
+ const response = await fetch(`/sw-cgi/websocket/send/${this._connId}`, {
232
+ method: 'POST',
233
+ headers: {
234
+ 'Content-Type': 'application/json',
235
+ },
236
+ body: payload
237
+ });
238
+
239
+ if (!response.ok) {
240
+ throw new Error(`Send failed: ${response.status}`);
241
+ }
242
+
243
+ } catch (error) {
244
+ console.error('Failed to send message:', error);
245
+ this._handleError(error);
246
+ break;
247
+ }
248
+ }
249
+
250
+ this._isSending = false;
251
+ }
252
+
253
+ close(code = 1000, reason = '') {
254
+ if (this._isClosed || this.readyState === WebSocket.CLOSING) {
255
+ return;
256
+ }
257
+
258
+ this.readyState = WebSocket.CLOSING;
259
+
260
+ // Send close request
261
+ if (this._connId) {
262
+ fetch(`/sw-cgi/websocket/send/${this._connId}`, {
263
+ method: 'POST',
264
+ headers: {
265
+ 'Content-Type': 'application/json',
266
+ },
267
+ body: JSON.stringify({ type: 'close', code: code, reason: reason })
268
+ }).catch(err => {
269
+ console.error('Failed to send close frame:', err);
270
+ });
271
+ }
272
+
273
+ // Handle close locally
274
+ this._handleClose(code, reason);
275
+ }
276
+
277
+ // EventTarget implementation
278
+ addEventListener(type, listener) {
279
+ if (!this._listeners) {
280
+ this._listeners = {};
281
+ }
282
+ if (!this._listeners[type]) {
283
+ this._listeners[type] = [];
284
+ }
285
+ this._listeners[type].push(listener);
286
+ }
287
+
288
+ removeEventListener(type, listener) {
289
+ if (!this._listeners || !this._listeners[type]) {
290
+ return;
291
+ }
292
+ const index = this._listeners[type].indexOf(listener);
293
+ if (index !== -1) {
294
+ this._listeners[type].splice(index, 1);
295
+ }
296
+ }
297
+
298
+ dispatchEvent(event) {
299
+ if (!this._listeners || !this._listeners[event.type]) {
300
+ return true;
301
+ }
302
+ this._listeners[event.type].forEach(listener => {
303
+ listener.call(this, event);
304
+ });
305
+ return true;
306
+ }
307
+ }
308
+
309
+ // Check if URL is same-origin
310
+ function isSameOrigin(url) {
311
+ try {
312
+ const wsUrl = new URL(url, window.location.href);
313
+ const currentOrigin = window.location.origin;
314
+
315
+ // Convert ws:// to http:// and wss:// to https:// for comparison
316
+ let wsOrigin = wsUrl.origin;
317
+ if (wsUrl.protocol === 'ws:') {
318
+ wsOrigin = wsOrigin.replace('ws:', 'http:');
319
+ } else if (wsUrl.protocol === 'wss:') {
320
+ wsOrigin = wsOrigin.replace('wss:', 'https:');
321
+ }
322
+
323
+ return wsOrigin === currentOrigin;
324
+ } catch (e) {
325
+ return false;
326
+ }
327
+ }
328
+
329
+ // Replace WebSocket with polyfill
330
+ window.WebSocket = function(url, protocols) {
331
+ // Use polyfill for same-origin, native for cross-origin
332
+ if (isSameOrigin(url)) {
333
+ console.log('[WebSocket Polyfill] Using HTTP-based polyfill for same-origin connection:', url);
334
+ return new WebSocketPolyfill(url, protocols);
335
+ } else {
336
+ console.log('[WebSocket Polyfill] Using native WebSocket for cross-origin connection:', url);
337
+ return new NativeWebSocket(url, protocols);
338
+ }
339
+ };
340
+
341
+ // Copy static properties
342
+ window.WebSocket.CONNECTING = NativeWebSocket.CONNECTING;
343
+ window.WebSocket.OPEN = NativeWebSocket.OPEN;
344
+ window.WebSocket.CLOSING = NativeWebSocket.CLOSING;
345
+ window.WebSocket.CLOSED = NativeWebSocket.CLOSED;
346
+
347
+ console.log('[WebSocket Polyfill] Initialized');
348
+})();
\ No newline at end of file
cmd/webclient/test-websocket.html
new
+203
@@ -0,0 +1,203 @@
1
+<!DOCTYPE html>
2
+<html lang="en">
3
+<head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>WebSocket Polyfill Test</title>
7
+ <style>
8
+ body {
9
+ font-family: Arial, sans-serif;
10
+ max-width: 800px;
11
+ margin: 50px auto;
12
+ padding: 20px;
13
+ }
14
+ .status {
15
+ padding: 10px;
16
+ margin: 10px 0;
17
+ border-radius: 4px;
18
+ font-weight: bold;
19
+ }
20
+ .status.connecting {
21
+ background-color: #fff3cd;
22
+ color: #856404;
23
+ }
24
+ .status.open {
25
+ background-color: #d4edda;
26
+ color: #155724;
27
+ }
28
+ .status.closed {
29
+ background-color: #f8d7da;
30
+ color: #721c24;
31
+ }
32
+ #messages {
33
+ border: 1px solid #ccc;
34
+ padding: 10px;
35
+ height: 300px;
36
+ overflow-y: auto;
37
+ background-color: #f9f9f9;
38
+ margin: 10px 0;
39
+ }
40
+ .message {
41
+ padding: 5px;
42
+ margin: 5px 0;
43
+ border-left: 3px solid #007bff;
44
+ background-color: white;
45
+ }
46
+ .message.sent {
47
+ border-left-color: #28a745;
48
+ }
49
+ .message.received {
50
+ border-left-color: #007bff;
51
+ }
52
+ .message.error {
53
+ border-left-color: #dc3545;
54
+ background-color: #fff5f5;
55
+ }
56
+ input, button {
57
+ padding: 10px;
58
+ margin: 5px;
59
+ }
60
+ input[type="text"] {
61
+ width: 60%;
62
+ }
63
+ button {
64
+ background-color: #007bff;
65
+ color: white;
66
+ border: none;
67
+ cursor: pointer;
68
+ border-radius: 4px;
69
+ }
70
+ button:hover {
71
+ background-color: #0056b3;
72
+ }
73
+ button:disabled {
74
+ background-color: #6c757d;
75
+ cursor: not-allowed;
76
+ }
77
+ .controls {
78
+ margin: 20px 0;
79
+ }
80
+ </style>
81
+</head>
82
+<body>
83
+ <h1>WebSocket Polyfill Test</h1>
84
+
85
+ <div class="controls">
86
+ <div>
87
+ <label for="wsUrl">WebSocket URL:</label><br>
88
+ <input type="text" id="wsUrl" value="ws://localhost:8080/ws" placeholder="ws://localhost:8080/ws">
89
+ </div>
90
+ <div>
91
+ <button id="connectBtn" onclick="connect()">Connect</button>
92
+ <button id="disconnectBtn" onclick="disconnect()" disabled>Disconnect</button>
93
+ </div>
94
+ </div>
95
+
96
+ <div id="status" class="status connecting">Not Connected</div>
97
+
98
+ <div>
99
+ <h3>Messages</h3>
100
+ <div id="messages"></div>
101
+ </div>
102
+
103
+ <div class="controls">
104
+ <input type="text" id="messageInput" placeholder="Type a message..." disabled>
105
+ <button id="sendBtn" onclick="sendMessage()" disabled>Send</button>
106
+ </div>
107
+
108
+ <script>
109
+ let ws = null;
110
+
111
+ function updateStatus(state, text) {
112
+ const statusDiv = document.getElementById('status');
113
+ statusDiv.className = 'status ' + state;
114
+ statusDiv.textContent = text;
115
+ }
116
+
117
+ function addMessage(text, type = 'received') {
118
+ const messagesDiv = document.getElementById('messages');
119
+ const messageDiv = document.createElement('div');
120
+ messageDiv.className = 'message ' + type;
121
+ messageDiv.textContent = `[${new Date().toLocaleTimeString()}] ${text}`;
122
+ messagesDiv.appendChild(messageDiv);
123
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
124
+ }
125
+
126
+ function connect() {
127
+ const url = document.getElementById('wsUrl').value;
128
+
129
+ try {
130
+ updateStatus('connecting', 'Connecting...');
131
+ addMessage('Attempting to connect to: ' + url, 'sent');
132
+
133
+ ws = new WebSocket(url);
134
+
135
+ ws.onopen = function(event) {
136
+ updateStatus('open', 'Connected');
137
+ addMessage('Connection opened', 'received');
138
+
139
+ document.getElementById('connectBtn').disabled = true;
140
+ document.getElementById('disconnectBtn').disabled = false;
141
+ document.getElementById('messageInput').disabled = false;
142
+ document.getElementById('sendBtn').disabled = false;
143
+ };
144
+
145
+ ws.onmessage = function(event) {
146
+ addMessage('Received: ' + event.data, 'received');
147
+ };
148
+
149
+ ws.onerror = function(event) {
150
+ addMessage('Error occurred: ' + (event.error || 'Unknown error'), 'error');
151
+ };
152
+
153
+ ws.onclose = function(event) {
154
+ updateStatus('closed', 'Disconnected');
155
+ addMessage(`Connection closed (code: ${event.code}, reason: ${event.reason || 'none'})`, 'received');
156
+
157
+ document.getElementById('connectBtn').disabled = false;
158
+ document.getElementById('disconnectBtn').disabled = true;
159
+ document.getElementById('messageInput').disabled = true;
160
+ document.getElementById('sendBtn').disabled = true;
161
+ };
162
+
163
+ } catch (error) {
164
+ addMessage('Failed to create WebSocket: ' + error.message, 'error');
165
+ updateStatus('closed', 'Connection Failed');
166
+ }
167
+ }
168
+
169
+ function disconnect() {
170
+ if (ws) {
171
+ ws.close(1000, 'User initiated close');
172
+ }
173
+ }
174
+
175
+ function sendMessage() {
176
+ const input = document.getElementById('messageInput');
177
+ const message = input.value.trim();
178
+
179
+ if (message && ws && ws.readyState === WebSocket.OPEN) {
180
+ try {
181
+ ws.send(message);
182
+ addMessage('Sent: ' + message, 'sent');
183
+ input.value = '';
184
+ } catch (error) {
185
+ addMessage('Failed to send: ' + error.message, 'error');
186
+ }
187
+ }
188
+ }
189
+
190
+ // Allow Enter key to send message
191
+ document.getElementById('messageInput').addEventListener('keypress', function(event) {
192
+ if (event.key === 'Enter') {
193
+ sendMessage();
194
+ }
195
+ });
196
+
197
+ // Log WebSocket implementation being used
198
+ window.addEventListener('load', function() {
199
+ console.log('WebSocket implementation:', window.WebSocket);
200
+ });
201
+ </script>
202
+</body>
203
+</html>
\ No newline at end of file