first commit
This commit is contained in:
commit
6e81b13adf
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Binaries
|
||||
log-proxy
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool
|
||||
*.out
|
||||
|
||||
# Dependency directories
|
||||
vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
|
||||
111
README.md
Normal file
111
README.md
Normal file
@ -0,0 +1,111 @@
|
||||
# Log Proxy - Логирующий реверс-прокси для реверс-инжиниринга API
|
||||
|
||||
Простой HTTP реверс-прокси на Go, который логирует все входящие запросы и исходящие ответы для анализа API.
|
||||
|
||||
## Возможности
|
||||
|
||||
- ✅ Перехват и логирование всех HTTP запросов (метод, URL, заголовки, тело)
|
||||
- ✅ Перехват и логирование всех HTTP ответов (статус, заголовки, тело)
|
||||
- ✅ Автоматическое форматирование JSON в логах
|
||||
- ✅ Вывод времени выполнения запросов
|
||||
- ✅ Красивый консольный вывод и JSON формат
|
||||
- ✅ Сохранение оригинальных заголовков и путей
|
||||
|
||||
## Установка
|
||||
|
||||
```bash
|
||||
go mod download
|
||||
```
|
||||
|
||||
## Использование
|
||||
|
||||
### Способ 1: Через аргументы командной строки
|
||||
|
||||
```bash
|
||||
go run main.go http://api.example.com
|
||||
```
|
||||
|
||||
### Способ 2: Через переменные окружения
|
||||
|
||||
```bash
|
||||
export TARGET_URL=http://api.example.com
|
||||
export PORT=8080 # опционально, по умолчанию 8080
|
||||
go run main.go
|
||||
```
|
||||
|
||||
### Способ 3: Сборка и запуск
|
||||
|
||||
```bash
|
||||
go build -o log-proxy
|
||||
./log-proxy http://api.example.com
|
||||
```
|
||||
|
||||
## Примеры
|
||||
|
||||
### Базовое использование
|
||||
|
||||
```bash
|
||||
# Запуск прокси на порту 8080, перенаправление на api.example.com
|
||||
go run main.go http://api.example.com
|
||||
|
||||
# В другом терминале отправляем запрос через прокси
|
||||
curl http://localhost:8080/api/users
|
||||
```
|
||||
|
||||
### С кастомным портом
|
||||
|
||||
```bash
|
||||
PORT=3000 go run main.go http://api.example.com
|
||||
```
|
||||
|
||||
### Логирование в файл
|
||||
|
||||
```bash
|
||||
go run main.go http://api.example.com 2>&1 | tee api-logs.txt
|
||||
```
|
||||
|
||||
## Формат логов
|
||||
|
||||
Прокси выводит два формата логов:
|
||||
|
||||
1. **Человекочитаемый формат** - красивый вывод в консоль с разделителями
|
||||
2. **JSON формат** - структурированные данные для дальнейшей обработки
|
||||
|
||||
Каждая запись содержит:
|
||||
- Временную метку
|
||||
- Метод и URL запроса
|
||||
- Заголовки запроса и ответа
|
||||
- Тело запроса и ответа
|
||||
- Время выполнения запроса
|
||||
|
||||
## Пример вывода
|
||||
|
||||
```
|
||||
================================================================================
|
||||
[2024-01-15 10:30:45] GET /api/users
|
||||
Remote: 127.0.0.1:52341
|
||||
|
||||
Request Headers:
|
||||
User-Agent: curl/7.68.0
|
||||
Accept: */*
|
||||
|
||||
[2024-01-15 10:30:45] Response: 200 (45ms)
|
||||
|
||||
Response Headers:
|
||||
Content-Type: application/json
|
||||
Content-Length: 1234
|
||||
|
||||
Response Body:
|
||||
{
|
||||
"users": [...]
|
||||
}
|
||||
================================================================================
|
||||
```
|
||||
|
||||
## Примечания
|
||||
|
||||
- Длинные тела ответов (>2000 символов) обрезаются для читаемости в консольном выводе
|
||||
- JSON в запросах автоматически форматируется для лучшей читаемости
|
||||
- Все оригинальные заголовки сохраняются и передаются на целевой сервер
|
||||
|
||||
|
||||
289
main.go
Normal file
289
main.go
Normal file
@ -0,0 +1,289 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LoggingProxy struct {
|
||||
target *url.URL
|
||||
proxy *httputil.ReverseProxy
|
||||
}
|
||||
|
||||
type RequestLog struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Method string `json:"method"`
|
||||
URL string `json:"url"`
|
||||
FullURL string `json:"full_url"`
|
||||
TargetURL string `json:"target_url"`
|
||||
Headers map[string][]string `json:"headers"`
|
||||
Body string `json:"body"`
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
}
|
||||
|
||||
type ResponseLog struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
StatusCode int `json:"status_code"`
|
||||
Headers map[string][]string `json:"headers"`
|
||||
Body string `json:"body"`
|
||||
Duration string `json:"duration"`
|
||||
}
|
||||
|
||||
type LogEntry struct {
|
||||
Request RequestLog `json:"request"`
|
||||
Response ResponseLog `json:"response"`
|
||||
}
|
||||
|
||||
func NewLoggingProxy(targetURL string) (*LoggingProxy, error) {
|
||||
target, err := url.Parse(targetURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid target URL: %w", err)
|
||||
}
|
||||
proxy := httputil.NewSingleHostReverseProxy(target)
|
||||
originalDirector := proxy.Director
|
||||
proxy.Director = func(req *http.Request) {
|
||||
originalDirector(req)
|
||||
// Устанавливаем правильный хост
|
||||
req.Host = target.Host
|
||||
// Убеждаемся, что схема правильная
|
||||
req.URL.Scheme = target.Scheme
|
||||
req.URL.Host = target.Host
|
||||
// Сохраняем оригинальный путь и query параметры
|
||||
// (они уже должны быть сохранены через originalDirector)
|
||||
}
|
||||
return &LoggingProxy{
|
||||
target: target,
|
||||
proxy: proxy,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (lp *LoggingProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
|
||||
// Логируем запрос перед отправкой
|
||||
reqLog := lp.logRequest(r)
|
||||
|
||||
// Создаем recorder для перехвата ответа
|
||||
recorder := &responseRecorder{
|
||||
ResponseWriter: w,
|
||||
statusCode: http.StatusOK,
|
||||
body: &bytes.Buffer{},
|
||||
headers: make(http.Header),
|
||||
}
|
||||
|
||||
// Настраиваем ModifyResponse для перехвата метаданных ответа
|
||||
originalModifyResponse := lp.proxy.ModifyResponse
|
||||
lp.proxy.ModifyResponse = func(resp *http.Response) error {
|
||||
if originalModifyResponse != nil {
|
||||
if err := originalModifyResponse(resp); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Сохраняем статус код из ответа для логирования
|
||||
recorder.statusCode = resp.StatusCode
|
||||
recorder.statusSetFromResp = true
|
||||
|
||||
// Сохраняем заголовки для логирования
|
||||
// ReverseProxy автоматически скопирует заголовки в ResponseWriter
|
||||
if recorder.headers == nil {
|
||||
recorder.headers = make(http.Header)
|
||||
}
|
||||
for k, v := range resp.Header {
|
||||
recorder.headers[k] = make([]string, len(v))
|
||||
copy(recorder.headers[k], v)
|
||||
}
|
||||
|
||||
// Важно: не трогаем resp.Body, чтобы ReverseProxy мог правильно передать его клиенту
|
||||
return nil
|
||||
}
|
||||
|
||||
// Выполняем проксирование запроса
|
||||
lp.proxy.ServeHTTP(recorder, r)
|
||||
|
||||
// Логируем ответ после получения
|
||||
duration := time.Since(start)
|
||||
respLog := ResponseLog{
|
||||
Timestamp: time.Now(),
|
||||
StatusCode: recorder.statusCode,
|
||||
Headers: recorder.headers,
|
||||
Body: recorder.body.String(),
|
||||
Duration: duration.String(),
|
||||
}
|
||||
logEntry := LogEntry{
|
||||
Request: reqLog,
|
||||
Response: respLog,
|
||||
}
|
||||
lp.printLog(logEntry)
|
||||
}
|
||||
|
||||
func (lp *LoggingProxy) logRequest(r *http.Request) RequestLog {
|
||||
// Читаем тело запроса
|
||||
bodyBytes, _ := io.ReadAll(r.Body)
|
||||
r.Body.Close()
|
||||
// Восстанавливаем тело для проксирования
|
||||
r.Body = io.NopCloser(bytes.NewReader(bodyBytes))
|
||||
|
||||
// Форматируем тело для логирования
|
||||
bodyStr := string(bodyBytes)
|
||||
if len(bodyStr) > 0 {
|
||||
var jsonData interface{}
|
||||
if err := json.Unmarshal(bodyBytes, &jsonData); err == nil {
|
||||
if formatted, err := json.MarshalIndent(jsonData, "", " "); err == nil {
|
||||
bodyStr = string(formatted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Формируем полный URL запроса
|
||||
fullURL := r.URL.String()
|
||||
if r.URL.Scheme == "" {
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
fullURL = fmt.Sprintf("%s://%s%s", scheme, r.Host, r.URL.String())
|
||||
}
|
||||
|
||||
// Формируем целевой URL
|
||||
targetURL := fmt.Sprintf("%s://%s%s", lp.target.Scheme, lp.target.Host, r.URL.Path)
|
||||
if r.URL.RawQuery != "" {
|
||||
targetURL += "?" + r.URL.RawQuery
|
||||
}
|
||||
|
||||
return RequestLog{
|
||||
Timestamp: time.Now(),
|
||||
Method: r.Method,
|
||||
URL: r.URL.String(),
|
||||
FullURL: fullURL,
|
||||
TargetURL: targetURL,
|
||||
Headers: r.Header,
|
||||
Body: bodyStr,
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
}
|
||||
}
|
||||
|
||||
func (lp *LoggingProxy) printLog(entry LogEntry) {
|
||||
fmt.Println(strings.Repeat("=", 80))
|
||||
fmt.Printf("[%s] %s %s\n", entry.Request.Timestamp.Format("2006-01-02 15:04:05"), entry.Request.Method, entry.Request.URL)
|
||||
fmt.Printf("From URL: %s\n", entry.Request.FullURL)
|
||||
fmt.Printf("To URL: %s\n", entry.Request.TargetURL)
|
||||
fmt.Printf("Remote: %s\n", entry.Request.RemoteAddr)
|
||||
if len(entry.Request.Headers) > 0 {
|
||||
fmt.Println("\nRequest Headers:")
|
||||
for k, v := range entry.Request.Headers {
|
||||
fmt.Printf(" %s: %s\n", k, strings.Join(v, ", "))
|
||||
}
|
||||
}
|
||||
if entry.Request.Body != "" {
|
||||
fmt.Println("\nRequest Body:")
|
||||
fmt.Println(entry.Request.Body)
|
||||
}
|
||||
fmt.Printf("\n[%s] Response: %d (%s)\n", entry.Response.Timestamp.Format("2006-01-02 15:04:05"), entry.Response.StatusCode, entry.Response.Duration)
|
||||
if len(entry.Response.Headers) > 0 {
|
||||
fmt.Println("\nResponse Headers:")
|
||||
for k, v := range entry.Response.Headers {
|
||||
fmt.Printf(" %s: %s\n", k, strings.Join(v, ", "))
|
||||
}
|
||||
}
|
||||
if entry.Response.Body != "" {
|
||||
fmt.Println("\nResponse Body:")
|
||||
fmt.Println(entry.Response.Body)
|
||||
}
|
||||
fmt.Println(strings.Repeat("=", 80))
|
||||
fmt.Println()
|
||||
if jsonData, err := json.MarshalIndent(entry, "", " "); err == nil {
|
||||
fmt.Println("JSON Log:")
|
||||
fmt.Println(string(jsonData))
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
type responseRecorder struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
body *bytes.Buffer
|
||||
headers http.Header
|
||||
wroteHeader bool
|
||||
statusSetFromResp bool
|
||||
}
|
||||
|
||||
func (rr *responseRecorder) Header() http.Header {
|
||||
return rr.ResponseWriter.Header()
|
||||
}
|
||||
|
||||
func (rr *responseRecorder) WriteHeader(code int) {
|
||||
if !rr.wroteHeader {
|
||||
// Используем статус код из ModifyResponse, если он был установлен, иначе используем переданный
|
||||
if !rr.statusSetFromResp {
|
||||
rr.statusCode = code
|
||||
}
|
||||
rr.wroteHeader = true
|
||||
|
||||
// Сохраняем все заголовки перед отправкой (для логирования)
|
||||
if rr.headers == nil {
|
||||
rr.headers = make(http.Header)
|
||||
}
|
||||
// Обновляем заголовки из ResponseWriter (они могут быть изменены ReverseProxy)
|
||||
for k, v := range rr.ResponseWriter.Header() {
|
||||
rr.headers[k] = make([]string, len(v))
|
||||
copy(rr.headers[k], v)
|
||||
}
|
||||
// Вызываем WriteHeader оригинального ResponseWriter с правильным статус кодом
|
||||
rr.ResponseWriter.WriteHeader(rr.statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func (rr *responseRecorder) Write(b []byte) (int, error) {
|
||||
// Если WriteHeader еще не вызван, вызываем его со статусом 200
|
||||
if !rr.wroteHeader {
|
||||
rr.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// Сохраняем тело для логирования
|
||||
if rr.body == nil {
|
||||
rr.body = &bytes.Buffer{}
|
||||
}
|
||||
rr.body.Write(b)
|
||||
|
||||
// Важно: записываем в оригинальный ResponseWriter, чтобы ответ вернулся клиенту
|
||||
return rr.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
func main() {
|
||||
targetURL := os.Getenv("TARGET_URL")
|
||||
if targetURL == "" {
|
||||
if len(os.Args) < 2 {
|
||||
log.Fatal("Usage: log-proxy <target_url> or set TARGET_URL environment variable")
|
||||
}
|
||||
targetURL = os.Args[1]
|
||||
}
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "8020"
|
||||
}
|
||||
proxy, err := NewLoggingProxy(targetURL)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create proxy: %v", err)
|
||||
}
|
||||
fmt.Printf("Starting log proxy server on port %s\n", port)
|
||||
fmt.Printf("Proxying requests 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