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) }