first commit

This commit is contained in:
admin 2026-01-23 10:05:12 +07:00
commit 94736555b3
4 changed files with 556 additions and 0 deletions

183
README.md Normal file
View File

@ -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 запросов/ответов
- Поддерживает двунаправленный поток сообщений
- Логирует каждое сообщение отдельно
- Отслеживает события подключения и отключения

7
go.mod Normal file
View File

@ -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

4
go.sum Normal file
View File

@ -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=

362
main.go Normal file
View File

@ -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 <target_ws_url> 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())
}