commit 6e81b13adffba7d45a5c1732a725b81f3112eec2 Author: admin Date: Fri Jan 23 10:03:52 2026 +0700 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..074bd69 --- /dev/null +++ b/.gitignore @@ -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 + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..b3062be --- /dev/null +++ b/README.md @@ -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 в запросах автоматически форматируется для лучшей читаемости +- Все оригинальные заголовки сохраняются и передаются на целевой сервер + + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b7c5755 --- /dev/null +++ b/go.mod @@ -0,0 +1,4 @@ +module log-proxy + +go 1.21 + diff --git a/main.go b/main.go new file mode 100644 index 0000000..777c500 --- /dev/null +++ b/main.go @@ -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 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()) +} +