Files
vServer/Backend/WebServer/php_server.go
Falknat 7a87617282 Инициализация проекта
Стабильный рабочий проект.
2025-10-02 06:02:45 +07:00

506 lines
15 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"
"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)
}