first commit

This commit is contained in:
admin 2026-01-23 10:03:52 +07:00
commit 6e81b13adf
4 changed files with 425 additions and 0 deletions

21
.gitignore vendored Normal file
View 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
View 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 в запросах автоматически форматируется для лучшей читаемости
- Все оригинальные заголовки сохраняются и передаются на целевой сервер

4
go.mod Normal file
View File

@ -0,0 +1,4 @@
module log-proxy
go 1.21

289
main.go Normal file
View 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())
}