From 94736555b3c2179b17b4bc72d996d3efd70598ce Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 23 Jan 2026 10:05:12 +0700 Subject: [PATCH] first commit --- README.md | 183 +++++++++++++++++++++++++++ go.mod | 7 ++ go.sum | 4 + main.go | 362 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 556 insertions(+) create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..54eb9a6 --- /dev/null +++ b/README.md @@ -0,0 +1,183 @@ +# WebSocket Log Proxy - Логирующий WebSocket прокси для анализа WebSocket соединений + +WebSocket реверс-прокси на Go, который логирует все WebSocket соединения и сообщения для анализа WebSocket API. + +## Возможности + +- ✅ Перехват и логирование WebSocket подключений (connect/disconnect) +- ✅ Логирование всех входящих сообщений (от клиента к серверу) +- ✅ Логирование всех исходящих сообщений (от сервера к клиенту) +- ✅ Поддержка текстовых и бинарных сообщений +- ✅ Автоматическое форматирование JSON в логах +- ✅ Красивый консольный вывод и JSON формат +- ✅ Сохранение оригинальных заголовков и путей +- ✅ Двунаправленная пересылка сообщений в реальном времени + +## Установка + +```bash +go mod download +``` + +## Использование + +### Способ 1: Через аргументы командной строки + +```bash +go run main.go ws://echo.websocket.org +``` + +### Способ 2: Через переменные окружения + +```bash +export TARGET_URL=ws://echo.websocket.org +export PORT=8080 # опционально, по умолчанию 8020 +go run main.go +``` + +### Способ 3: Сборка и запуск + +```bash +go build -o ws-log-proxy +./ws-log-proxy ws://echo.websocket.org +``` + +## Примеры + +### Базовое использование + +```bash +# Запуск прокси на порту 8020, перенаправление на ws://echo.websocket.org +go run main.go ws://echo.websocket.org + +# В другом терминале подключаемся через прокси +# Используя wscat (npm install -g wscat) +wscat -c ws://localhost:8020 + +# Или используя Python +python3 -c "import websocket; ws = websocket.create_connection('ws://localhost:8020'); ws.send('Hello'); print(ws.recv())" +``` + +### С кастомным портом + +```bash +PORT=3000 go run main.go ws://echo.websocket.org +``` + +### С защищенным WebSocket (WSS) + +```bash +go run main.go wss://secure-websocket-server.com +``` + +### Логирование в файл + +```bash +go run main.go ws://echo.websocket.org 2>&1 | tee ws-logs.txt +``` + +## Формат логов + +Прокси выводит логи в двух форматах: + +1. **Человекочитаемый формат** - красивый вывод в консоль с разделителями +2. **JSON формат** - структурированные данные для дальнейшей обработки + +Каждая запись содержит: +- Временную метку +- События подключения/отключения +- Направление сообщений (incoming/outgoing) +- Тип сообщения (text/binary) +- Содержимое сообщений +- Заголовки HTTP запроса + +## Пример вывода + +``` +================================================================================ +[2024-01-15 10:30:45] WebSocket CONNECT +Remote: 127.0.0.1:52341 +URL: /echo + +Headers: + Upgrade: websocket + Connection: Upgrade + Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== +================================================================================ + +-------------------------------------------------------------------------------- +[2024-01-15 10:30:46] ← INCOMING Message (Text) +Remote: 127.0.0.1:52341 + +Message: +{ + "type": "hello", + "message": "Hello, WebSocket!" +} +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- +[2024-01-15 10:30:46] → OUTGOING Message (Text) +Remote: 127.0.0.1:52341 + +Message: +{ + "type": "echo", + "message": "Hello, WebSocket!" +} +-------------------------------------------------------------------------------- + +================================================================================ +[2024-01-15 10:30:50] WebSocket DISCONNECT +Remote: 127.0.0.1:52341 +URL: /echo +================================================================================ +``` + +## JSON формат лога + +В конце каждой сессии выводится полный JSON лог со всеми сообщениями: + +```json +{ + "connection": { + "timestamp": "2024-01-15T10:30:45Z", + "event": "connect", + "remote_addr": "127.0.0.1:52341", + "url": "/echo", + "headers": {...} + }, + "messages": [ + { + "timestamp": "2024-01-15T10:30:46Z", + "direction": "incoming", + "message": "{\"type\":\"hello\"}", + "message_type": 1, + "remote_addr": "127.0.0.1:52341" + }, + { + "timestamp": "2024-01-15T10:30:46Z", + "direction": "outgoing", + "message": "{\"type\":\"echo\"}", + "message_type": 1, + "remote_addr": "127.0.0.1:52341" + } + ] +} +``` + +## Примечания + +- JSON сообщения автоматически форматируются для лучшей читаемости +- Бинарные сообщения отображаются как `[Binary: N bytes]` +- Все оригинальные заголовки сохраняются и передаются на целевой сервер +- Прокси поддерживает как обычные WebSocket (ws://), так и защищенные (wss://) +- Сообщения логируются в реальном времени по мере их передачи + +## Отличия от HTTP версии + +- Логирует WebSocket соединения вместо HTTP запросов/ответов +- Поддерживает двунаправленный поток сообщений +- Логирует каждое сообщение отдельно +- Отслеживает события подключения и отключения + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fbbd57c --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module ws-log-proxy + +go 1.21 + +require github.com/gorilla/websocket v1.5.1 + +require golang.org/x/net v0.17.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..272772f --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= diff --git a/main.go b/main.go new file mode 100644 index 0000000..496caf0 --- /dev/null +++ b/main.go @@ -0,0 +1,362 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "net/url" + "os" + "strings" + "time" + + "github.com/gorilla/websocket" +) + +type WebSocketLoggingProxy struct { + targetURL *url.URL + upgrader websocket.Upgrader +} + +type ConnectionLog struct { + Timestamp time.Time `json:"timestamp"` + Event string `json:"event"` // "connect", "disconnect" + RemoteAddr string `json:"remote_addr"` + URL string `json:"url"` + Headers map[string][]string `json:"headers"` +} + +type MessageLog struct { + Timestamp time.Time `json:"timestamp"` + Direction string `json:"direction"` // "incoming" (от клиента), "outgoing" (к клиенту) + Message string `json:"message"` + MessageType int `json:"message_type"` // 1=text, 2=binary + RemoteAddr string `json:"remote_addr"` +} + +type LogEntry struct { + Connection ConnectionLog `json:"connection"` + Messages []MessageLog `json:"messages,omitempty"` +} + +func NewWebSocketLoggingProxy(targetURL string) (*WebSocketLoggingProxy, error) { + target, err := url.Parse(targetURL) + if err != nil { + return nil, fmt.Errorf("invalid target URL: %w", err) + } + + // Проверяем, что схема правильная для WebSocket + if target.Scheme != "ws" && target.Scheme != "wss" { + return nil, fmt.Errorf("target URL must use ws:// or wss:// scheme") + } + + upgrader := websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + // Разрешаем все источники для прокси + return true + }, + } + + return &WebSocketLoggingProxy{ + targetURL: target, + upgrader: upgrader, + }, nil +} + +func (wsp *WebSocketLoggingProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Логируем подключение + connLog := ConnectionLog{ + Timestamp: time.Now(), + Event: "connect", + RemoteAddr: r.RemoteAddr, + URL: r.URL.String(), + Headers: r.Header, + } + wsp.printConnectionLog(connLog) + + // Обновляем HTTP соединение до WebSocket + clientConn, err := wsp.upgrader.Upgrade(w, r, nil) + if err != nil { + log.Printf("Failed to upgrade connection: %v", err) + return + } + defer clientConn.Close() + + // Формируем целевой URL для WebSocket + targetWSURL := wsp.buildTargetURL(r) + + // Фильтруем заголовки, исключая те, которые конфликтуют с WebSocket протоколом + targetHeaders := wsp.filterHeaders(r.Header) + + // Подключаемся к целевому WebSocket серверу + targetConn, _, err := websocket.DefaultDialer.Dial(targetWSURL, targetHeaders) + if err != nil { + log.Printf("Failed to connect to target: %v", err) + clientConn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseInternalServerErr, "Failed to connect to target")) + return + } + defer targetConn.Close() + + // Создаем канал для сообщений + messages := make([]MessageLog, 0) + + // Канал для завершения + done := make(chan struct{}) + + // Горутина для пересылки сообщений от клиента к целевому серверу + go func() { + defer close(done) + for { + messageType, message, err := clientConn.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { + log.Printf("Error reading from client: %v", err) + } + return + } + + // Логируем входящее сообщение + msgLog := MessageLog{ + Timestamp: time.Now(), + Direction: "incoming", + Message: wsp.formatMessage(message, messageType), + MessageType: messageType, + RemoteAddr: r.RemoteAddr, + } + messages = append(messages, msgLog) + wsp.printMessageLog(msgLog) + + // Пересылаем сообщение целевому серверу + if err := targetConn.WriteMessage(messageType, message); err != nil { + log.Printf("Error writing to target: %v", err) + return + } + } + }() + + // Горутина для пересылки сообщений от целевого сервера к клиенту + go func() { + for { + messageType, message, err := targetConn.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { + log.Printf("Error reading from target: %v", err) + } + return + } + + // Логируем исходящее сообщение + msgLog := MessageLog{ + Timestamp: time.Now(), + Direction: "outgoing", + Message: wsp.formatMessage(message, messageType), + MessageType: messageType, + RemoteAddr: r.RemoteAddr, + } + messages = append(messages, msgLog) + wsp.printMessageLog(msgLog) + + // Пересылаем сообщение клиенту + if err := clientConn.WriteMessage(messageType, message); err != nil { + log.Printf("Error writing to client: %v", err) + return + } + } + }() + + // Ждем завершения одной из горутин + <-done + + // Логируем отключение + disconnectLog := ConnectionLog{ + Timestamp: time.Now(), + Event: "disconnect", + RemoteAddr: r.RemoteAddr, + URL: r.URL.String(), + Headers: r.Header, + } + wsp.printConnectionLog(disconnectLog) + + // Выводим итоговую запись с всеми сообщениями + if len(messages) > 0 { + logEntry := LogEntry{ + Connection: connLog, + Messages: messages, + } + wsp.printFullLog(logEntry) + } +} + +func (wsp *WebSocketLoggingProxy) buildTargetURL(r *http.Request) string { + // Формируем целевой URL, сохраняя путь и query параметры + targetPath := r.URL.Path + if r.URL.RawQuery != "" { + targetPath += "?" + r.URL.RawQuery + } + + return fmt.Sprintf("%s://%s%s", wsp.targetURL.Scheme, wsp.targetURL.Host, targetPath) +} + +func (wsp *WebSocketLoggingProxy) filterHeaders(originalHeaders http.Header) http.Header { + // Создаем новый набор заголовков, исключая те, которые конфликтуют с WebSocket + filtered := make(http.Header) + + // Заголовки, которые нужно исключить (они устанавливаются автоматически библиотекой) + // Используем lowercase для case-insensitive сравнения + excludedHeadersLower := map[string]bool{ + "upgrade": true, + "connection": true, + "sec-websocket-key": true, + "sec-websocket-version": true, + "sec-websocket-extensions": true, + "sec-websocket-protocol": true, + } + + // Копируем только разрешенные заголовки + for k, v := range originalHeaders { + // Преобразуем в lowercase для сравнения + keyLower := strings.ToLower(k) + if !excludedHeadersLower[keyLower] { + // Сохраняем оригинальное имя заголовка + filtered[k] = make([]string, len(v)) + copy(filtered[k], v) + } + } + + return filtered +} + +func (wsp *WebSocketLoggingProxy) formatMessage(message []byte, messageType int) string { + if messageType == websocket.TextMessage { + // Пытаемся форматировать как JSON для лучшей читаемости + // Сохраняем полное сообщение без обрезки + var jsonData interface{} + if err := json.Unmarshal(message, &jsonData); err == nil { + // Форматируем JSON с отступами, сохраняя все данные полностью + if formatted, err := json.MarshalIndent(jsonData, "", " "); err == nil { + return string(formatted) + } + } + // Если не JSON или ошибка форматирования, возвращаем как есть (полностью) + return string(message) + } else { + // Для бинарных сообщений показываем размер + return fmt.Sprintf("[Binary: %d bytes]", len(message)) + } +} + +func (wsp *WebSocketLoggingProxy) printConnectionLog(connLog ConnectionLog) { + fmt.Println(strings.Repeat("=", 80)) + fmt.Printf("[%s] WebSocket %s\n", connLog.Timestamp.Format("2006-01-02 15:04:05"), strings.ToUpper(connLog.Event)) + fmt.Printf("Remote: %s\n", connLog.RemoteAddr) + fmt.Printf("URL: %s\n", connLog.URL) + if len(connLog.Headers) > 0 { + fmt.Println("\nHeaders:") + for k, v := range connLog.Headers { + fmt.Printf(" %s: %s\n", k, strings.Join(v, ", ")) + } + } + fmt.Println(strings.Repeat("=", 80)) + fmt.Println() +} + +func (wsp *WebSocketLoggingProxy) printMessageLog(msgLog MessageLog) { + direction := "→" + if msgLog.Direction == "incoming" { + direction = "←" + } + msgType := "Text" + if msgLog.MessageType == websocket.BinaryMessage { + msgType = "Binary" + } + + fmt.Println(strings.Repeat("-", 80)) + fmt.Printf("[%s] %s %s Message (%s)\n", + msgLog.Timestamp.Format("2006-01-02 15:04:05"), + direction, + strings.ToUpper(msgLog.Direction), + msgType) + fmt.Printf("Remote: %s\n", msgLog.RemoteAddr) + fmt.Println("\nMessage:") + fmt.Println(msgLog.Message) + fmt.Println(strings.Repeat("-", 80)) + fmt.Println() +} + +func (wsp *WebSocketLoggingProxy) printFullLog(logEntry LogEntry) { + fmt.Println(strings.Repeat("=", 80)) + fmt.Println("FULL SESSION LOG") + fmt.Println(strings.Repeat("=", 80)) + + // Выводим информацию о подключении + fmt.Printf("[%s] WebSocket %s\n", + logEntry.Connection.Timestamp.Format("2006-01-02 15:04:05"), + strings.ToUpper(logEntry.Connection.Event)) + fmt.Printf("Remote: %s\n", logEntry.Connection.RemoteAddr) + fmt.Printf("URL: %s\n", logEntry.Connection.URL) + fmt.Printf("Total Messages: %d\n", len(logEntry.Messages)) + + // Выводим все сообщения + for i, msg := range logEntry.Messages { + direction := "→" + if msg.Direction == "incoming" { + direction = "←" + } + msgType := "Text" + if msg.MessageType == websocket.BinaryMessage { + msgType = "Binary" + } + + fmt.Printf("\n[%d] [%s] %s %s (%s)\n", + i+1, + msg.Timestamp.Format("2006-01-02 15:04:05"), + direction, + strings.ToUpper(msg.Direction), + msgType) + fmt.Println(msg.Message) + } + + fmt.Println(strings.Repeat("=", 80)) + + // Выводим JSON версию + if jsonData, err := json.MarshalIndent(logEntry, "", " "); err == nil { + fmt.Println("\nJSON Log:") + fmt.Println(string(jsonData)) + fmt.Println() + } +} + +func main() { + targetURL := os.Getenv("TARGET_URL") + if targetURL == "" { + if len(os.Args) < 2 { + log.Fatal("Usage: ws-log-proxy or set TARGET_URL environment variable") + } + targetURL = os.Args[1] + } + + port := os.Getenv("PORT") + if port == "" { + port = "8020" + } + + proxy, err := NewWebSocketLoggingProxy(targetURL) + if err != nil { + log.Fatalf("Failed to create WebSocket proxy: %v", err) + } + + fmt.Printf("Starting WebSocket log proxy server on port %s\n", port) + fmt.Printf("Proxying WebSocket connections to: %s\n", targetURL) + fmt.Println("Press Ctrl+C to stop") + fmt.Println() + + server := &http.Server{ + Addr: ":" + port, + Handler: proxy, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + } + + log.Fatal(server.ListenAndServe()) +} +