From 27cb40e4213bea24f864fd7f019762d9c7f41ef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sun, 16 Aug 2026 08:18:18 +0400 Subject: [PATCH] =?UTF-8?q?feat(ws-probe):=20=D1=82=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=D1=8B=D0=B9=20WS=20echo-=D1=81=D0=B5=D1=80=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=BA=D0=B8=20wss=20=D0=BD=D0=B0=20=D0=BF=D0=BB?= =?UTF-8?q?=D0=B0=D1=82=D1=84=D0=BE=D1=80=D0=BC=D0=B5=20Nubes=20(v0.1.0,?= =?UTF-8?q?=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B7=20naeel/iot-ws-probe)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + ws-probe/Dockerfile | 17 +++++++ ws-probe/Makefile | 19 +++++++ ws-probe/go.mod | 5 ++ ws-probe/go.sum | 2 + ws-probe/main.go | 117 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 161 insertions(+) create mode 100644 ws-probe/Dockerfile create mode 100644 ws-probe/Makefile create mode 100644 ws-probe/go.mod create mode 100644 ws-probe/go.sum create mode 100644 ws-probe/main.go diff --git a/.gitignore b/.gitignore index fbe076c..4ee4276 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /kafka-consumer /sqs-consumer bin/ +ws-probe/ws-probe # Go *.exe diff --git a/ws-probe/Dockerfile b/ws-probe/Dockerfile new file mode 100644 index 0000000..21cb112 --- /dev/null +++ b/ws-probe/Dockerfile @@ -0,0 +1,17 @@ +# ws-probe — проверка WebSocket через платформенный ingress Nubes. +# Образ: naeel/iot-ws-probe (Docker Hub, публичный — платформа тянет только публичные). + +FROM golang:1.25 AS builder +WORKDIR /workspace +COPY go.mod go.sum ./ +RUN go mod download +COPY main.go ./ +ARG VERSION=dev +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -ldflags "-X main.version=${VERSION}" -o ws-probe . + +FROM gcr.io/distroless/static:nonroot +WORKDIR / +COPY --from=builder /workspace/ws-probe . +USER 65532:65532 +EXPOSE 8080 +ENTRYPOINT ["/ws-probe"] diff --git a/ws-probe/Makefile b/ws-probe/Makefile new file mode 100644 index 0000000..3a8e107 --- /dev/null +++ b/ws-probe/Makefile @@ -0,0 +1,19 @@ +# ws-probe — тестовый WS echo-сервер для проверки wss на платформе Nubes. + +VERSION ?= v0.1.0 +IMAGE ?= naeel/iot-ws-probe + +.PHONY: build tidy docker-build docker-push + +build: + CGO_ENABLED=0 go build -ldflags "-X main.version=$(VERSION)" -o ws-probe . + +tidy: + go mod tidy + +docker-build: + docker build --build-arg VERSION=$(VERSION) -t $(IMAGE):$(VERSION) -t $(IMAGE):latest . + +docker-push: + docker push $(IMAGE):$(VERSION) + docker push $(IMAGE):latest diff --git a/ws-probe/go.mod b/ws-probe/go.mod new file mode 100644 index 0000000..fd6464b --- /dev/null +++ b/ws-probe/go.mod @@ -0,0 +1,5 @@ +module ws-probe + +go 1.25 + +require github.com/gorilla/websocket v1.5.3 diff --git a/ws-probe/go.sum b/ws-probe/go.sum new file mode 100644 index 0000000..25a9fc4 --- /dev/null +++ b/ws-probe/go.sum @@ -0,0 +1,2 @@ +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= diff --git a/ws-probe/main.go b/ws-probe/main.go new file mode 100644 index 0000000..663eb62 --- /dev/null +++ b/ws-probe/main.go @@ -0,0 +1,117 @@ +// 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 + } + } + } +}