Files
vServer/Backend/WebServer/proxy_server.go
Falknat a6007a8906 Поддержка SSE и streaming responses
- Proxy: chunked streaming с Flush() для real-time данных
- PHP FastCGI: потоковая обработка ответов через streamFastCGIResponse()
- Удалена буферизация - данные отправляются сразу"
2025-11-26 22:30:54 +07:00

192 lines
6.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package webserver
import (
"bytes"
"crypto/tls"
"io"
"log"
"net/http"
"strings"
"sync"
"vServer/Backend/config"
tools "vServer/Backend/tools"
)
var (
configMutex sync.RWMutex
)
func StartHandlerProxy(w http.ResponseWriter, r *http.Request) (valid bool) {
valid = false
configMutex.RLock()
defer configMutex.RUnlock()
// Проверяем глобальный флаг прокси
if !config.ConfigData.Soft_Settings.Proxy_enabled {
return false
}
// Проходим по всем прокси конфигурациям
for _, proxyConfig := range config.ConfigData.Proxy_Service {
// Пропускаем отключенные прокси
if !proxyConfig.Enable {
continue
}
// Проверяем совпадение домена
if r.Host != proxyConfig.ExternalDomain {
continue
}
valid = true
// Проверяем vAccess для прокси
accessAllowed, errorPage := CheckProxyVAccess(r.URL.Path, proxyConfig.ExternalDomain, r)
if !accessAllowed {
// Доступ запрещён - обрабатываем страницу ошибки
HandleProxyVAccessError(w, r, errorPage)
return valid
}
// Проверяем AutoHTTPS - редирект с HTTP на HTTPS
https_check := !(r.TLS == nil)
if !https_check && proxyConfig.AutoHTTPS {
// Перенаправляем на HTTPS
httpsURL := "https://" + r.Host + r.URL.RequestURI()
http.Redirect(w, r, httpsURL, http.StatusMovedPermanently)
tools.Logs_file(0, "P-HTTP", "🔀 IP клиента: "+r.RemoteAddr+" Редирект HTTP → HTTPS: "+r.Host+r.URL.Path, "logs_http.log", false)
return valid
}
// Логирование прокси-запроса
if https_check {
tools.Logs_file(0, "P-HTTPS", "🔍 IP клиента: "+r.RemoteAddr+" Обработка запроса: https://"+r.Host+r.URL.Path+" → "+proxyConfig.LocalAddress+":"+proxyConfig.LocalPort, "logs_https.log", false)
} else {
tools.Logs_file(0, "P-HTTP", "🔍 IP клиента: "+r.RemoteAddr+" Обработка запроса: http://"+r.Host+r.URL.Path+" → "+proxyConfig.LocalAddress+":"+proxyConfig.LocalPort, "logs_http.log", false)
}
// Определяем протокол для локального соединения
protocol := "http"
if proxyConfig.ServiceHTTPSuse {
protocol = "https"
}
// Читаем тело запроса в буфер для корректной передачи POST данных
var bodyBuffer bytes.Buffer
if r.Body != nil {
if _, err := io.Copy(&bodyBuffer, r.Body); err != nil {
http.Error(w, "Ошибка чтения тела запроса", http.StatusInternalServerError)
return valid
}
r.Body.Close()
}
// Проксирование на локальный адрес
proxyURL := protocol + "://" + proxyConfig.LocalAddress + ":" + proxyConfig.LocalPort + r.URL.RequestURI()
proxyReq, err := http.NewRequest(r.Method, proxyURL, &bodyBuffer)
if err != nil {
http.Error(w, "Ошибка создания прокси-запроса", http.StatusInternalServerError)
return valid
}
// Копируем ВСЕ заголовки без изменений (кроме технических)
for name, values := range r.Header {
// Пропускаем только технические заголовки HTTP/1.1
lowerName := strings.ToLower(name)
if lowerName == "connection" || lowerName == "upgrade" ||
lowerName == "proxy-connection" || lowerName == "te" ||
lowerName == "trailers" || lowerName == "transfer-encoding" {
continue
}
// Копируем заголовок как есть
for _, value := range values {
proxyReq.Header.Add(name, value)
}
}
// Добавляем заголовки для передачи реального IP клиента
clientIP := r.RemoteAddr
if colonIndex := strings.LastIndex(clientIP, ":"); colonIndex != -1 {
clientIP = clientIP[:colonIndex]
}
proxyReq.Header.Set("X-Real-IP", clientIP)
proxyReq.Header.Set("X-Forwarded-For", clientIP)
proxyReq.Header.Set("X-Forwarded-Proto", protocol)
// Устанавливаем правильный Content-Length для POST/PUT запросов
if bodyBuffer.Len() > 0 {
proxyReq.ContentLength = int64(bodyBuffer.Len())
}
// Выполняем прокси-запрос
client := &http.Client{
// Отключаем автоматическое следование редиректам для корректной работы с авторизацией
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
// Для HTTPS соединений настраиваем TLS (если понадобится)
if proxyConfig.ServiceHTTPSuse {
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, // Простая настройка для внутренних соединений
},
}
}
resp, err := client.Do(proxyReq)
if err != nil {
http.Error(w, "Ошибка прокси-запроса", http.StatusBadGateway)
tools.Logs_file(1, "PROXY", "Ошибка прокси-запроса: "+err.Error(), "logs_proxy.log", false)
return valid
}
defer resp.Body.Close()
// Прозрачно копируем ВСЕ заголовки ответа без изменений
for name, values := range resp.Header {
for _, value := range values {
w.Header().Add(name, value)
}
}
// Устанавливаем статус код
w.WriteHeader(resp.StatusCode)
// Копируем тело ответа с поддержкой streaming (SSE, chunked responses)
// Используем буферизированное копирование с принудительной отправкой данных
flusher, canFlush := w.(http.Flusher)
// Буфер для чанков (32KB - оптимальный размер для баланса производительности)
buffer := make([]byte, 32*1024)
for {
n, err := resp.Body.Read(buffer)
if n > 0 {
// Записываем прочитанные данные
if _, writeErr := w.Write(buffer[:n]); writeErr != nil {
log.Printf("Ошибка записи тела ответа: %v", writeErr)
break
}
// Принудительно отправляем данные клиенту (критично для SSE)
if canFlush {
flusher.Flush()
}
}
if err != nil {
if err != io.EOF {
log.Printf("Ошибка чтения тела ответа: %v", err)
}
break
}
}
return valid
}
return valid
}