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