first commit
This commit is contained in:
commit
94736555b3
183
README.md
Normal file
183
README.md
Normal 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
7
go.mod
Normal 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
4
go.sum
Normal 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
362
main.go
Normal 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())
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user