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