// ws-probe — тестовый контейнер для проверки WebSocket (wss) через платформенный ingress Nubes. // // Назначение: подтвердить, что «Простой HTTP контейнер» платформы пропускает // WebSocket upgrade (101 Switching Protocols) и держит долгоживущие соединения. // // Эндпоинты: // // GET / — инфо-страница (HTML) // GET /health — {"status":"ok","version":...} // GET /ws — WebSocket echo: отвечает на сообщения, шлёт ping каждые 20с // // Без кредов, без внешних зависимостей — только echo. package main import ( "encoding/json" "log" "net/http" "os" "time" "github.com/gorilla/websocket" ) var version = "dev" const pageHTML = ` ws-probe

ws-probe

WebSocket echo: /ws

Health: /health

` var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, CheckOrigin: func(r *http.Request) bool { return true }, } func main() { port := os.Getenv("PORT") if port == "" { port = "8080" } mux := http.NewServeMux() mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]string{"status": "ok", "version": version}) }) mux.HandleFunc("/ws", wsEcho) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write([]byte(pageHTML)) }) log.Printf("ws-probe %s listening on :%s", version, port) if err := http.ListenAndServe(":"+port, mux); err != nil { log.Fatal(err) } } // wsEcho — апгрейд до WebSocket, echo сообщений, ping каждые 20с. func wsEcho(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Printf("upgrade failed: %v", err) return } defer conn.Close() log.Printf("ws connected from %s (host=%s)", conn.RemoteAddr(), r.Host) // Read deadline сбрасывается pong'ами — мёртвые соединения закрываются через 60с. conn.SetReadDeadline(time.Now().Add(60 * time.Second)) conn.SetPongHandler(func(string) error { conn.SetReadDeadline(time.Now().Add(60 * time.Second)) return nil }) pingTicker := time.NewTicker(20 * time.Second) defer pingTicker.Stop() done := make(chan struct{}) go func() { defer close(done) for { mt, msg, err := conn.ReadMessage() if err != nil { log.Printf("ws read error: %v", err) return } log.Printf("ws recv %d bytes (type %d)", len(msg), mt) if err := conn.WriteMessage(mt, msg); err != nil { log.Printf("ws write error: %v", err) return } log.Printf("ws echo sent") } }() for { select { case <-done: log.Printf("ws closed from %s", conn.RemoteAddr()) return case t := <-pingTicker.C: deadline := time.Now().Add(10 * time.Second) if err := conn.WriteControl(websocket.PingMessage, []byte(t.Format(time.RFC3339)), deadline); err != nil { log.Printf("ws ping error: %v", err) return } } } }