506 lines
15 KiB
Go
506 lines
15 KiB
Go
package webserver
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/binary"
|
||
"fmt"
|
||
"io"
|
||
"net"
|
||
"net/http"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
config "vServer/Backend/config"
|
||
tools "vServer/Backend/tools"
|
||
)
|
||
|
||
var (
|
||
phpProcesses []*exec.Cmd
|
||
fcgiPorts []int
|
||
portIndex int
|
||
portMutex sync.Mutex
|
||
maxWorkers = 4
|
||
stopping = false // Флаг остановки
|
||
)
|
||
|
||
var address_php string
|
||
var Сonsole_php bool = false
|
||
|
||
// FastCGI константы
|
||
const (
|
||
FCGI_VERSION_1 = 1
|
||
FCGI_BEGIN_REQUEST = 1
|
||
FCGI_ABORT_REQUEST = 2
|
||
FCGI_END_REQUEST = 3
|
||
FCGI_PARAMS = 4
|
||
FCGI_STDIN = 5
|
||
FCGI_STDOUT = 6
|
||
FCGI_STDERR = 7
|
||
FCGI_DATA = 8
|
||
FCGI_GET_VALUES = 9
|
||
FCGI_GET_VALUES_RESULT = 10
|
||
FCGI_UNKNOWN_TYPE = 11
|
||
FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE
|
||
|
||
FCGI_NULL_REQUEST_ID = 0
|
||
|
||
FCGI_KEEP_CONN = 1
|
||
|
||
FCGI_RESPONDER = 1
|
||
FCGI_AUTHORIZER = 2
|
||
FCGI_FILTER = 3
|
||
)
|
||
|
||
// FastCGI заголовок
|
||
type FCGIHeader struct {
|
||
Version byte
|
||
Type byte
|
||
RequestID uint16
|
||
ContentLength uint16
|
||
PaddingLength byte
|
||
Reserved byte
|
||
}
|
||
|
||
// FastCGI BeginRequest body
|
||
type FCGIBeginRequestBody struct {
|
||
Role uint16
|
||
Flags byte
|
||
Reserved [5]byte
|
||
}
|
||
|
||
func PHP_Start() {
|
||
// Сбрасываем флаг остановки
|
||
stopping = false
|
||
|
||
// Читаем настройки из конфига
|
||
address_php = config.ConfigData.Soft_Settings.Php_host
|
||
|
||
// Запускаем FastCGI процессы
|
||
for i := 0; i < maxWorkers; i++ {
|
||
port := config.ConfigData.Soft_Settings.Php_port + i
|
||
fcgiPorts = append(fcgiPorts, port)
|
||
go startFastCGIWorker(port, i)
|
||
time.Sleep(200 * time.Millisecond) // Задержка между запусками
|
||
}
|
||
|
||
tools.Logs_file(0, "PHP ", fmt.Sprintf("💻 PHP FastCGI пул запущен (%d процессов на портах %d-%d)", maxWorkers, config.ConfigData.Soft_Settings.Php_port, config.ConfigData.Soft_Settings.Php_port+maxWorkers-1), "logs_php.log", true)
|
||
}
|
||
|
||
func startFastCGIWorker(port int, workerID int) {
|
||
phpPath := "WebServer/soft/PHP/php_v_8/php-cgi.exe"
|
||
|
||
cmd := exec.Command(phpPath, "-b", fmt.Sprintf("%s:%d", address_php, port))
|
||
cmd.Env = append(os.Environ(),
|
||
"PHP_FCGI_CHILDREN=0", // Один процесс на порт
|
||
"PHP_FCGI_MAX_REQUESTS=1000", // Перезапуск после 1000 запросов
|
||
)
|
||
|
||
if !Сonsole_php {
|
||
cmd.Stdout = nil
|
||
cmd.Stderr = nil
|
||
}
|
||
|
||
err := cmd.Start()
|
||
if err != nil {
|
||
tools.Logs_file(1, "PHP", fmt.Sprintf("❌ Ошибка запуска FastCGI worker %d на порту %d: %v", workerID, port, err), "logs_php.log", true)
|
||
return
|
||
}
|
||
|
||
phpProcesses = append(phpProcesses, cmd)
|
||
tools.Logs_file(0, "PHP", fmt.Sprintf("✅ PHP FastCGI %d запущен на %s:%d", workerID, address_php, port), "logs_php.log", false)
|
||
|
||
// Ждём завершения процесса и перезапускаем
|
||
go func() {
|
||
cmd.Wait()
|
||
|
||
// Проверяем, не останавливается ли сервер
|
||
if stopping {
|
||
return // Не перезапускаем если сервер останавливается
|
||
}
|
||
|
||
tools.Logs_file(1, "PHP", fmt.Sprintf("⚠️ FastCGI worker %d завершился, перезапускаем...", workerID), "logs_php.log", true)
|
||
time.Sleep(1 * time.Second)
|
||
startFastCGIWorker(port, workerID) // Перезапуск
|
||
}()
|
||
}
|
||
|
||
// Получение следующего порта из пула (round-robin)
|
||
func getNextFCGIPort() int {
|
||
portMutex.Lock()
|
||
defer portMutex.Unlock()
|
||
|
||
port := fcgiPorts[portIndex]
|
||
portIndex = (portIndex + 1) % len(fcgiPorts)
|
||
return port
|
||
}
|
||
|
||
// Создание FastCGI пакета
|
||
func createFCGIPacket(requestType byte, requestID uint16, content []byte) []byte {
|
||
contentLength := len(content)
|
||
paddingLength := 8 - (contentLength % 8)
|
||
if paddingLength == 8 {
|
||
paddingLength = 0
|
||
}
|
||
|
||
header := FCGIHeader{
|
||
Version: FCGI_VERSION_1,
|
||
Type: requestType,
|
||
RequestID: requestID,
|
||
ContentLength: uint16(contentLength),
|
||
PaddingLength: byte(paddingLength),
|
||
Reserved: 0,
|
||
}
|
||
|
||
var buf bytes.Buffer
|
||
binary.Write(&buf, binary.BigEndian, header)
|
||
buf.Write(content)
|
||
buf.Write(make([]byte, paddingLength)) // Padding
|
||
|
||
return buf.Bytes()
|
||
}
|
||
|
||
// Кодирование FastCGI параметров
|
||
func encodeFCGIParams(params map[string]string) []byte {
|
||
var buf bytes.Buffer
|
||
|
||
for key, value := range params {
|
||
keyLen := len(key)
|
||
valueLen := len(value)
|
||
|
||
// Длина ключа
|
||
if keyLen < 128 {
|
||
buf.WriteByte(byte(keyLen))
|
||
} else {
|
||
binary.Write(&buf, binary.BigEndian, uint32(keyLen)|0x80000000)
|
||
}
|
||
|
||
// Длина значения
|
||
if valueLen < 128 {
|
||
buf.WriteByte(byte(valueLen))
|
||
} else {
|
||
binary.Write(&buf, binary.BigEndian, uint32(valueLen)|0x80000000)
|
||
}
|
||
|
||
// Ключ и значение
|
||
buf.WriteString(key)
|
||
buf.WriteString(value)
|
||
}
|
||
|
||
return buf.Bytes()
|
||
}
|
||
|
||
// HandlePHPRequest - универсальная функция для обработки файлов
|
||
// Проверяет является ли файл PHP и обрабатывает соответственно
|
||
// Возвращает true если файл был обработан (PHP или статический), false если нужна обработка ошибки
|
||
func HandlePHPRequest(w http.ResponseWriter, r *http.Request, host string, filePath string, originalURI string, originalPath string) bool {
|
||
// Импортируем path/filepath для проверки расширения
|
||
if filepath.Ext(filePath) == ".php" {
|
||
// Сохраняем оригинальные значения URL
|
||
originalURL := r.URL.Path
|
||
originalRawQuery := r.URL.RawQuery
|
||
|
||
// Устанавливаем путь к PHP файлу
|
||
r.URL.Path = filePath
|
||
|
||
// Вызываем существующий PHPHandler
|
||
PHPHandler(w, r, host, originalURI, originalPath)
|
||
|
||
// Восстанавливаем оригинальные значения
|
||
r.URL.Path = originalURL
|
||
r.URL.RawQuery = originalRawQuery
|
||
return true
|
||
} else {
|
||
// Это не PHP файл - обрабатываем как статический
|
||
fullPath := "WebServer/www/" + host + "/public_www" + filePath
|
||
http.ServeFile(w, r, fullPath)
|
||
return true
|
||
}
|
||
}
|
||
|
||
// PHPHandler с FastCGI
|
||
func PHPHandler(w http.ResponseWriter, r *http.Request, host string, originalURI string, originalPath string) {
|
||
phpPath := "WebServer/www/" + host + "/public_www" + r.URL.Path
|
||
|
||
// Проверяем существование файла
|
||
if _, err := os.Stat(phpPath); os.IsNotExist(err) {
|
||
http.ServeFile(w, r, "WebServer/tools/error_page/index.html")
|
||
tools.Logs_file(2, "PHP_404", "🔍 PHP файл не найден: "+phpPath, "logs_php.log", false)
|
||
return
|
||
}
|
||
|
||
// Получаем абсолютный путь для SCRIPT_FILENAME
|
||
absPath, err := filepath.Abs(phpPath)
|
||
if err != nil {
|
||
tools.Logs_file(1, "PHP", "❌ Ошибка получения абсолютного пути: "+err.Error(), "logs_php.log", false)
|
||
absPath = phpPath
|
||
}
|
||
|
||
// Получаем порт FastCGI
|
||
port := getNextFCGIPort()
|
||
|
||
// Подключаемся к FastCGI процессу
|
||
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", address_php, port), 5*time.Second)
|
||
if err != nil {
|
||
tools.Logs_file(1, "PHP", fmt.Sprintf("❌ Ошибка подключения к FastCGI порт %d: %v", port, err), "logs_php.log", false)
|
||
http.Error(w, "Service Unavailable", http.StatusServiceUnavailable)
|
||
return
|
||
}
|
||
defer conn.Close()
|
||
|
||
// Читаем POST данные
|
||
var postData []byte
|
||
if r.Method == "POST" {
|
||
postData, _ = io.ReadAll(r.Body)
|
||
r.Body.Close()
|
||
}
|
||
|
||
// Формируем параметры FastCGI
|
||
serverPort := "80"
|
||
if r.TLS != nil {
|
||
serverPort = "443"
|
||
}
|
||
|
||
// Используем переданные оригинальные значения или текущие если не переданы
|
||
requestURI := r.URL.RequestURI()
|
||
if originalURI != "" {
|
||
requestURI = originalURI
|
||
}
|
||
|
||
pathInfo := r.URL.Path
|
||
if originalPath != "" {
|
||
pathInfo = originalPath
|
||
}
|
||
|
||
params := map[string]string{
|
||
"REQUEST_METHOD": r.Method,
|
||
"REQUEST_URI": requestURI,
|
||
"QUERY_STRING": r.URL.RawQuery,
|
||
"CONTENT_TYPE": r.Header.Get("Content-Type"),
|
||
"CONTENT_LENGTH": fmt.Sprintf("%d", len(postData)),
|
||
"SCRIPT_FILENAME": absPath,
|
||
"SCRIPT_NAME": r.URL.Path,
|
||
"DOCUMENT_ROOT": "WebServer/www/" + host + "/public_www",
|
||
"SERVER_NAME": host,
|
||
"HTTP_HOST": host,
|
||
"SERVER_PORT": serverPort,
|
||
"SERVER_PROTOCOL": "HTTP/1.1",
|
||
"GATEWAY_INTERFACE": "CGI/1.1",
|
||
"REDIRECT_STATUS": "200",
|
||
"REMOTE_ADDR": strings.Split(r.RemoteAddr, ":")[0],
|
||
"REMOTE_HOST": strings.Split(r.RemoteAddr, ":")[0],
|
||
"PATH_INFO": pathInfo,
|
||
"PATH_TRANSLATED": absPath,
|
||
}
|
||
|
||
// Добавляем HTTP заголовки
|
||
for name, values := range r.Header {
|
||
if len(values) > 0 {
|
||
httpName := "HTTP_" + strings.ToUpper(strings.ReplaceAll(name, "-", "_"))
|
||
params[httpName] = values[0]
|
||
}
|
||
}
|
||
|
||
requestID := uint16(1)
|
||
|
||
// 1. Отправляем BEGIN_REQUEST
|
||
beginRequest := FCGIBeginRequestBody{
|
||
Role: FCGI_RESPONDER,
|
||
Flags: 0,
|
||
}
|
||
var beginBuf bytes.Buffer
|
||
binary.Write(&beginBuf, binary.BigEndian, beginRequest)
|
||
packet := createFCGIPacket(FCGI_BEGIN_REQUEST, requestID, beginBuf.Bytes())
|
||
conn.Write(packet)
|
||
|
||
// 2. Отправляем PARAMS с разбивкой на чанки
|
||
paramsData := encodeFCGIParams(params)
|
||
if len(paramsData) > 0 {
|
||
const maxChunkSize = 65535 // Максимальный размер FastCGI пакета
|
||
|
||
for offset := 0; offset < len(paramsData); offset += maxChunkSize {
|
||
end := offset + maxChunkSize
|
||
if end > len(paramsData) {
|
||
end = len(paramsData)
|
||
}
|
||
|
||
chunk := paramsData[offset:end]
|
||
packet = createFCGIPacket(FCGI_PARAMS, requestID, chunk)
|
||
conn.Write(packet)
|
||
}
|
||
}
|
||
|
||
// 3. Пустой PARAMS (конец параметров)
|
||
packet = createFCGIPacket(FCGI_PARAMS, requestID, []byte{})
|
||
conn.Write(packet)
|
||
|
||
// 4. Отправляем STDIN (POST данные) с разбивкой на чанки
|
||
if len(postData) > 0 {
|
||
const maxChunkSize = 65535 // Максимальный размер FastCGI пакета
|
||
|
||
for offset := 0; offset < len(postData); offset += maxChunkSize {
|
||
end := offset + maxChunkSize
|
||
if end > len(postData) {
|
||
end = len(postData)
|
||
}
|
||
|
||
chunk := postData[offset:end]
|
||
packet = createFCGIPacket(FCGI_STDIN, requestID, chunk)
|
||
conn.Write(packet)
|
||
}
|
||
}
|
||
|
||
// 5. Пустой STDIN (конец данных)
|
||
packet = createFCGIPacket(FCGI_STDIN, requestID, []byte{})
|
||
conn.Write(packet)
|
||
|
||
// Читаем ответ
|
||
response, err := readFastCGIResponse(conn, requestID)
|
||
if err != nil {
|
||
tools.Logs_file(1, "PHP", "❌ Ошибка чтения FastCGI ответа: "+err.Error(), "logs_php.log", false)
|
||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
// Обрабатываем ответ
|
||
processPHPResponse(w, response)
|
||
tools.Logs_file(0, "PHP", fmt.Sprintf("✅ FastCGI обработал: %s (порт %d)", phpPath, port), "logs_php.log", false)
|
||
}
|
||
|
||
// Чтение FastCGI ответа
|
||
func readFastCGIResponse(conn net.Conn, requestID uint16) ([]byte, error) {
|
||
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
|
||
|
||
var stdout bytes.Buffer
|
||
var stderr bytes.Buffer
|
||
|
||
for {
|
||
// Читаем заголовок FastCGI
|
||
headerBuf := make([]byte, 8)
|
||
_, err := io.ReadFull(conn, headerBuf)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
var header FCGIHeader
|
||
buf := bytes.NewReader(headerBuf)
|
||
binary.Read(buf, binary.BigEndian, &header)
|
||
|
||
// Читаем содержимое
|
||
content := make([]byte, header.ContentLength)
|
||
if header.ContentLength > 0 {
|
||
_, err = io.ReadFull(conn, content)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
// Читаем padding
|
||
if header.PaddingLength > 0 {
|
||
padding := make([]byte, header.PaddingLength)
|
||
io.ReadFull(conn, padding)
|
||
}
|
||
|
||
// Обрабатываем пакет
|
||
switch header.Type {
|
||
case FCGI_STDOUT:
|
||
if header.ContentLength > 0 {
|
||
stdout.Write(content)
|
||
} else {
|
||
// Пустой STDOUT означает конец
|
||
}
|
||
case FCGI_STDERR:
|
||
if header.ContentLength > 0 {
|
||
stderr.Write(content)
|
||
}
|
||
case FCGI_END_REQUEST:
|
||
// Завершение запроса
|
||
if stderr.Len() > 0 {
|
||
tools.Logs_file(1, "PHP", "FastCGI stderr: "+stderr.String(), "logs_php.log", false)
|
||
}
|
||
return stdout.Bytes(), nil
|
||
}
|
||
}
|
||
}
|
||
|
||
// Обработка PHP ответа (как раньше)
|
||
func processPHPResponse(w http.ResponseWriter, response []byte) {
|
||
responseStr := string(response)
|
||
|
||
// Разбираем заголовки и тело
|
||
parts := strings.SplitN(responseStr, "\r\n\r\n", 2)
|
||
if len(parts) < 2 {
|
||
parts = strings.SplitN(responseStr, "\n\n", 2)
|
||
}
|
||
|
||
if len(parts) >= 2 {
|
||
headers := strings.Split(parts[0], "\n")
|
||
statusCode := 200
|
||
|
||
for _, header := range headers {
|
||
header = strings.TrimSpace(header)
|
||
if header == "" {
|
||
continue
|
||
}
|
||
|
||
if strings.HasPrefix(strings.ToLower(header), "content-type:") {
|
||
contentType := strings.TrimSpace(strings.SplitN(header, ":", 2)[1])
|
||
w.Header().Set("Content-Type", contentType)
|
||
} else if strings.HasPrefix(strings.ToLower(header), "set-cookie:") {
|
||
cookie := strings.TrimSpace(strings.SplitN(header, ":", 2)[1])
|
||
w.Header().Add("Set-Cookie", cookie)
|
||
} else if strings.HasPrefix(strings.ToLower(header), "location:") {
|
||
location := strings.TrimSpace(strings.SplitN(header, ":", 2)[1])
|
||
w.Header().Set("Location", location)
|
||
w.WriteHeader(http.StatusFound)
|
||
return
|
||
} else if strings.HasPrefix(strings.ToLower(header), "status:") {
|
||
status := strings.TrimSpace(strings.SplitN(header, ":", 2)[1])
|
||
if code, err := strconv.Atoi(strings.Split(status, " ")[0]); err == nil {
|
||
statusCode = code
|
||
}
|
||
} else if strings.Contains(header, ":") {
|
||
headerParts := strings.SplitN(header, ":", 2)
|
||
if len(headerParts) == 2 {
|
||
w.Header().Set(strings.TrimSpace(headerParts[0]), strings.TrimSpace(headerParts[1]))
|
||
}
|
||
}
|
||
}
|
||
|
||
w.WriteHeader(statusCode)
|
||
w.Write([]byte(parts[1]))
|
||
} else {
|
||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||
w.Write(response)
|
||
}
|
||
}
|
||
|
||
// PHP_Stop останавливает все FastCGI процессы
|
||
func PHP_Stop() {
|
||
// Устанавливаем флаг остановки
|
||
stopping = true
|
||
|
||
for i, cmd := range phpProcesses {
|
||
if cmd != nil && cmd.Process != nil {
|
||
err := cmd.Process.Kill()
|
||
if err != nil {
|
||
tools.Logs_file(1, "PHP", fmt.Sprintf("❌ Ошибка остановки FastCGI процесса %d: %v", i, err), "logs_php.log", true)
|
||
} else {
|
||
tools.Logs_file(0, "PHP", fmt.Sprintf("✅ FastCGI процесс %d остановлен", i), "logs_php.log", false)
|
||
}
|
||
}
|
||
}
|
||
|
||
phpProcesses = nil
|
||
fcgiPorts = nil
|
||
|
||
// Дополнительно убиваем все процессы php-cgi.exe
|
||
cmd := exec.Command("taskkill", "/F", "/IM", "php-cgi.exe")
|
||
cmd.Run()
|
||
|
||
tools.Logs_file(0, "PHP", "🛑 Все FastCGI процессы остановлены", "logs_php.log", true)
|
||
}
|