Инициализация проекта

Стабильный рабочий проект.
This commit is contained in:
Falknat
2025-10-02 06:02:45 +07:00
commit 7a87617282
47 changed files with 6057 additions and 0 deletions

185
Backend/WebServer/MySQL.go Normal file
View File

@@ -0,0 +1,185 @@
package webserver
import (
"fmt"
"os/exec"
"path/filepath"
"strconv"
"time"
config "vServer/Backend/config"
tools "vServer/Backend/tools"
)
var mysqlProcess *exec.Cmd
var mysql_status bool = false
var mysql_secure bool = false
var mysqldPath string
var configPath string
var dataDirAbs string
var binDirAbs string
var binPathAbs string
var mysql_port int
var mysql_ip string
var console_mysql bool = false
func AbsPathMySQL() {
var err error
mysqldPath, err = tools.AbsPath(filepath.Join("WebServer/soft/MySQL/bin", "mysqld.exe"))
tools.CheckError(err)
configPath, err = tools.AbsPath("WebServer/soft/MySQL/my.ini")
tools.CheckError(err)
dataDirAbs, err = tools.AbsPath("WebServer/soft/MySQL/bin/data")
tools.CheckError(err)
binDirAbs, err = tools.AbsPath("WebServer/soft/MySQL/bin")
tools.CheckError(err)
binPathAbs, err = tools.AbsPath("WebServer/soft/MySQL/bin")
tools.CheckError(err)
}
// config_patch возвращает путь к mysqld, аргументы и бинарную директорию
func config_patch(secures bool) (string, []string, string) {
// Получаем абсолютные пути
AbsPathMySQL()
// Объявляем args на уровне функции
var args []string
if secures {
args = []string{
"--defaults-file=" + configPath,
"--datadir=" + dataDirAbs,
"--shared-memory",
"--skip-grant-tables",
"--console",
}
} else {
args = []string{
"--defaults-file=" + configPath,
"--port=" + fmt.Sprintf("%d", mysql_port),
"--bind-address=" + mysql_ip,
"--datadir=" + dataDirAbs,
"--console",
}
}
return mysqldPath, args, binDirAbs
}
// StartMySQLServer запускает MySQL сервер
func StartMySQLServer(secure bool) {
mysql_port = config.ConfigData.Soft_Settings.Mysql_port
mysql_ip = config.ConfigData.Soft_Settings.Mysql_host
if tools.Port_check("MySQL", mysql_ip, strconv.Itoa(mysql_port)) {
return
}
if mysql_status {
tools.Logs_file(1, "MySQL", "Сервер MySQL уже запущен", "logs_mysql.log", true)
return
}
if false {
return
}
// Настройка режима
mysql_secure = secure
mysqldPath, args, binDirAbs := config_patch(secure)
// Выбор сообщения
if secure {
tools.Logs_file(0, "MySQL", "Запуск сервера MySQL в режиме безопасности", "logs_mysql.log", true)
} else {
tools.Logs_file(0, "MySQL", "Запуск сервера MySQL в обычном режиме", "logs_mysql.log", true)
}
// Общая логика запуска
mysqlProcess = exec.Command(mysqldPath, args...)
mysqlProcess.Dir = binDirAbs
tools.Logs_console(mysqlProcess, console_mysql)
tools.Logs_file(0, "MySQL", fmt.Sprintf("Сервер MySQL запущен на %s:%d", mysql_ip, mysql_port), "logs_mysql.log", true)
mysql_status = true
}
// StopMySQLServer останавливает MySQL сервер
func StopMySQLServer() {
if mysql_status {
cmd := exec.Command("taskkill", "/F", "/IM", "mysqld.exe")
err := cmd.Run()
tools.CheckError(err)
tools.Logs_file(0, "MySQL", "Сервер MySQL остановлен", "logs_mysql.log", true)
mysql_status = false
} else {
tools.Logs_file(1, "MySQL", "Сервер MySQL уже остановлен", "logs_mysql.log", true)
}
}
func ResetPasswordMySQL() {
NewPasswordMySQL := "root"
StopMySQLServer()
time.Sleep(2 * time.Second)
mysql_secure = true
StartMySQLServer(true)
time.Sleep(2 * time.Second)
query := "FLUSH PRIVILEGES; ALTER USER 'root'@'%' IDENTIFIED BY '" + NewPasswordMySQL + "';"
СheckMySQLPassword(query)
tools.Logs_file(0, "MySQL", "Новый пароль: "+NewPasswordMySQL, "logs_mysql.log", true)
println()
StopMySQLServer()
StartMySQLServer(false)
}
// СheckMySQLPassword проверяет пароль для MySQL
func СheckMySQLPassword(query string) {
AbsPathMySQL()
if mysql_secure {
// В безопасном режиме подключаемся без пароля
cmd := exec.Command(filepath.Join(binPathAbs, "mysql.exe"), "-u", "root", "-pRoot", "-e", query)
cmd.Dir = binPathAbs
// Захватываем вывод для логирования
err := tools.Logs_console(cmd, false)
if err != nil {
tools.Logs_file(1, "MySQL", "Вывод MySQL (stdout/stderr):", "logs_mysql.log", true)
} else {
tools.Logs_file(0, "MySQL", "Команда выполнена успешно", "logs_mysql.log", true)
}
}
}

View File

@@ -0,0 +1,195 @@
package webserver
import (
"fmt"
"os"
"time"
admin "vServer/Backend/admin"
config "vServer/Backend/config"
tools "vServer/Backend/tools"
)
var Secure_post bool = false
func CommandListener() {
fmt.Println("Введите help для получения списка команд")
fmt.Println("")
for {
var cmd string
fmt.Print(tools.Color(" > ", tools.Оранжевый))
fmt.Scanln(&cmd)
switch cmd {
case "help":
fmt.Println(" ------------------------------------------")
fmt.Println(" 1: mysql_stop - Остановить MySQL")
fmt.Println(" 2: mysql_start - Запустить MySQL")
fmt.Println(" 3: mysql_pass - Сбросить пароль MySQL")
fmt.Println(" 4: clear - Очистить консоль")
fmt.Println(" 5: cert_reload - Перезагрузить SSL сертификаты")
fmt.Println(" 6: admin_toggle - Переключить режим админки (embed/файловая система)")
fmt.Println(" 7: config_reload - Перезагрузить конфигурацию")
fmt.Println(" 8: restart - Перезапустить сервер")
fmt.Println(" 9: php_console - Открыть PHP консоль")
fmt.Println(" 10: exit - выйти из программы")
fmt.Println(" ------------------------------------------")
fmt.Println("")
case "mysql_stop":
StopMySQLServer()
case "mysql_start":
StartMySQLServer(false)
case "mysql_pass":
ResetPasswordMySQL()
case "clear":
ClearConsole()
case "cert_reload":
ReloadCertificates()
case "admin_toggle":
AdminToggle()
case "config_reload":
ConfigReload()
case "restart":
RestartServer()
case "time_run":
fmt.Println(tools.ServerUptime("get"))
case "secure_post":
if Secure_post {
Secure_post = false
fmt.Println("Secure post is disabled")
} else {
Secure_post = true
fmt.Println("Secure post is enabled")
}
case "exit":
fmt.Println("Завершение...")
os.Exit(0)
default:
fmt.Println(" Неизвестная команда. Введите 'help' для получения списка команд")
fmt.Println("")
}
}
}
func RestartServer() {
fmt.Println("")
fmt.Println("⏹️ Перезагрузка сервера...")
// Останавливаем все сервисы
fmt.Println("⏹️ Останавливаем сервисы...")
fmt.Println("")
// Останавливаем HTTP/HTTPS серверы
StopHTTPServer()
StopHTTPSServer()
// Останавливаем MySQL
if mysql_status {
StopMySQLServer()
time.Sleep(1 * time.Second)
}
// Останавливаем PHP
PHP_Stop()
time.Sleep(1 * time.Second)
fmt.Println("")
fmt.Println("✅ Все сервисы остановлены")
// Перезагружаем конфигурацию
fmt.Println("📋 Перезагружаем конфигурацию...")
fmt.Println("")
config.LoadConfig()
// Запускаем сервисы заново
fmt.Println("🚀 Запускаем сервисы...")
fmt.Println("")
// Запускаем HTTP/HTTPS серверы
go StartHTTP()
time.Sleep(100 * time.Millisecond)
go StartHTTPS()
time.Sleep(100 * time.Millisecond)
// Запускаем PHP
PHP_Start()
time.Sleep(100 * time.Millisecond)
// Запускаем MySQL
StartMySQLServer(false)
time.Sleep(100 * time.Millisecond)
fmt.Println("✅ Сервер успешно перезагружен!")
fmt.Println("")
}
func ClearConsole() {
// Очищаем консоль, но сохраняем первые три строки
fmt.Print("\033[H\033[2J") // ANSI escape code для очистки экрана
println("")
println(tools.Color("vServer", tools.Жёлтый) + tools.Color(" 1.0.0", tools.Голубой))
println(tools.Color("Автор: ", tools.Зелёный) + tools.Color("Суманеев Роман (c) 2025", tools.Голубой))
println(tools.Color("Официальный сайт: ", tools.Зелёный) + tools.Color("https://voxsel.ru", tools.Голубой))
println("")
// Восстанавливаем первые три строки
fmt.Println("Введите help для получения списка команд")
fmt.Println("")
}
// Переключает режим админки между embed и файловой системой
func AdminToggle() {
fmt.Println("")
if admin.UseEmbedded {
// Переключаем на файловую систему
admin.UseEmbedded = false
fmt.Println("🔄 Режим изменен: Embedded → Файловая система")
fmt.Println("✅ Админка переключена на файловую систему")
fmt.Println("📁 Файлы будут загружаться с диска из Backend/admin/html/")
fmt.Println("💡 Теперь можно редактировать файлы и изменения будут видны сразу")
} else {
// Переключаем обратно на embedded
admin.UseEmbedded = true
fmt.Println("🔄 Режим изменен: Файловая система → Embedded")
fmt.Println("✅ Админка переключена на embedded режим")
fmt.Println("📦 Файлы загружаются из встроенных ресурсов")
fmt.Println("🚀 Быстрая загрузка, но изменения требуют перекомпиляции")
}
fmt.Println("")
}
// Перезагружает конфигурацию без перезапуска сервисов
func ConfigReload() {
fmt.Println("")
fmt.Println("📋 Перезагружаем конфигурацию...")
// Загружаем новую конфигурацию
config.LoadConfig()
fmt.Println("✅ Конфигурация успешно перезагружена!")
fmt.Println("💡 Изменения применятся к новым запросам")
fmt.Println("")
}

View File

@@ -0,0 +1,221 @@
package webserver
import (
"net/http"
"os"
"strings"
"vServer/Backend/config"
tools "vServer/Backend/tools"
)
func StartHandler() {
http.HandleFunc("/", handler)
}
func Alias_check(r *http.Request) (alias_found bool, host string) {
alias_found = false
for _, site := range config.ConfigData.Site_www {
for _, alias := range site.Alias {
if alias == r.Host {
alias_found = true
return alias_found, site.Host
} else {
alias_found = false
}
}
}
return alias_found, ""
}
func Alias_Run(r *http.Request) (rhost string) {
var host string
host = r.Host
alias_check, alias := Alias_check(r)
if alias_check {
host = alias
}
return host
}
// Получает список root_file для сайта из конфигурации
func getRootFiles(host string) []string {
for _, site := range config.ConfigData.Site_www {
if site.Host == host {
if site.Root_file != "" {
// Разделяем по запятой и убираем пробелы
files := strings.Split(site.Root_file, ",")
var cleanFiles []string
for _, file := range files {
cleanFile := strings.TrimSpace(file)
if cleanFile != "" {
cleanFiles = append(cleanFiles, cleanFile)
}
}
if len(cleanFiles) > 0 {
return cleanFiles
}
}
// Если не указан, используем index.html как fallback
return []string{"index.html"}
}
}
// Если сайт не найден в конфиге, используем index.html
return []string{"index.html"}
}
// Находит первый существующий root файл из списка
func findExistingRootFile(host string, dirPath string) (string, bool) {
rootFiles := getRootFiles(host)
basePath := "WebServer/www/" + host + "/public_www" + dirPath
for _, rootFile := range rootFiles {
fullPath := basePath + rootFile
if _, err := os.Stat(fullPath); err == nil {
return rootFile, true
}
}
return "", false
}
// Проверяет включен ли роутинг через root файл для сайта
func isRootFileRoutingEnabled(host string) bool {
for _, site := range config.ConfigData.Site_www {
if site.Host == host {
return site.Root_file_routing
}
}
// По умолчанию роутинг выключен
return false
}
// Проверка vAccess с обработкой ошибки
// Возвращает true если доступ разрешён, false если заблокирован
func checkVAccessAndHandle(w http.ResponseWriter, r *http.Request, filePath string, host string) bool {
accessAllowed, errorPage := CheckVAccess(filePath, host, r)
if !accessAllowed {
HandleVAccessError(w, r, errorPage, host)
tools.Logs_file(2, "vAccess", "🚫 Доступ запрещён vAccess: "+r.RemoteAddr+" → "+r.Host+filePath+" (error: "+errorPage+")", "logs_vaccess.log", false)
return false
}
return true
}
// Обработчик запросов
func handler(w http.ResponseWriter, r *http.Request) {
host := Alias_Run(r) // Получаем хост из запроса
https_check := !(r.TLS == nil) // Проверяем, по HTTPS ли запрос
root_url := r.URL.Path == "/" // Проверяем, является ли запрос корневым URL
// Проверяем, обработал ли прокси запрос
if StartHandlerProxy(w, r) {
return // Если прокси обработал запрос, прерываем выполнение
}
// ЕДИНСТВЕННАЯ ПРОВЕРКА vAccess - простая проверка запрошенного пути
if !checkVAccessAndHandle(w, r, r.URL.Path, host) {
return
}
if https_check {
tools.Logs_file(0, "HTTPS", "🔍 IP клиента: "+r.RemoteAddr+" Обработка запроса: https://"+r.Host+r.URL.Path, "logs_https.log", false)
} else {
tools.Logs_file(0, "HTTP", "🔍 IP клиента: "+r.RemoteAddr+" Обработка запроса: http://"+r.Host+r.URL.Path, "logs_http.log", false)
// Если сертификат для домена существует в папке cert, перенаправляем на HTTPS
if checkHostCert(r) {
// Если запрос не по HTTPS, перенаправляем на HTTPS
httpsURL := "https://" + r.Host + r.URL.RequestURI()
http.Redirect(w, r, httpsURL, http.StatusMovedPermanently)
return // Прерываем выполнение после редиректа
}
}
// Проверяем существование директории сайта
if _, err := os.Stat("WebServer/www/" + host + "/public_www"); err != nil {
http.ServeFile(w, r, "WebServer/tools/error_page/index.html")
tools.Logs_file(2, "H404", "🔍 IP клиента: "+r.RemoteAddr+" Директория сайта не найдена: "+host, "logs_http.log", false)
return
}
if root_url {
// Если корневой URL, то ищем первый существующий root файл
if rootFile, found := findExistingRootFile(host, "/"); found {
// Обрабатываем найденный root файл (статический или PHP)
HandlePHPRequest(w, r, host, "/"+rootFile, r.URL.RequestURI(), r.URL.Path)
} else {
// Ни один root файл не найден - показываем ошибку
rootFiles := getRootFiles(host)
tools.Logs_file(2, "H404", "🔍 IP клиента: "+r.RemoteAddr+" Root файлы не найдены: "+strings.Join(rootFiles, ", "), "logs_http.log", false)
http.ServeFile(w, r, "WebServer/tools/error_page/index.html")
}
}
if !root_url {
// Проверяем существование запрашиваемого файла
filePath := "WebServer/www/" + host + "/public_www" + r.URL.Path
if fileInfo, err := os.Stat(filePath); err == nil {
// Путь существует - проверяем что это
if fileInfo.IsDir() {
// Это директория - ищем индексные файлы
// Убираем слэш в конце если есть, и добавляем обратно для единообразия
dirPath := r.URL.Path
if !strings.HasSuffix(dirPath, "/") {
dirPath += "/"
}
// Ищем первый существующий root файл в директории
if rootFile, found := findExistingRootFile(host, dirPath); found {
// Обрабатываем найденный индексный файл в директории
HandlePHPRequest(w, r, host, dirPath+rootFile, r.URL.RequestURI(), r.URL.Path)
return
}
// Если никаких индексных файлов нет - показываем ошибку (запрещаем листинг)
rootFiles := getRootFiles(host)
tools.Logs_file(2, "H404", "🔍 IP клиента: "+r.RemoteAddr+" Индексные файлы не найдены в директории "+r.Host+r.URL.Path+": "+strings.Join(rootFiles, ", "), "logs_http.log", false)
http.ServeFile(w, r, "WebServer/tools/error_page/index.html")
} else {
// Это файл - обрабатываем через HandlePHPRequest
HandlePHPRequest(w, r, host, r.URL.Path, "", "")
}
} else {
// Файл не найден - проверяем нужен ли роутинг через root файл
if isRootFileRoutingEnabled(host) {
// Ищем первый существующий root файл для роутинга
if rootFile, found := findExistingRootFile(host, "/"); found {
// Root файл существует - используем для роутинга
HandlePHPRequest(w, r, host, "/"+rootFile, r.URL.RequestURI(), r.URL.Path)
} else {
// Root файлы не найдены
rootFiles := getRootFiles(host)
tools.Logs_file(2, "H404", "🔍 IP клиента: "+r.RemoteAddr+" Root файлы не найдены для роутинга: "+strings.Join(rootFiles, ", "), "logs_http.log", false)
http.ServeFile(w, r, "WebServer/tools/error_page/index.html")
}
} else {
// Роутинг отключен - показываем обычную 404
http.ServeFile(w, r, "WebServer/tools/error_page/index.html")
tools.Logs_file(2, "H404", "🔍 IP клиента: "+r.RemoteAddr+" Файл не найден: "+r.Host+r.URL.Path, "logs_http.log", false)
}
}
}
}

View File

@@ -0,0 +1,41 @@
package webserver
import (
"net/http"
tools "vServer/Backend/tools"
)
var httpServer *http.Server
var port_http string = "80"
// Запуск HTTP сервера
func StartHTTP() {
if tools.Port_check("HTTP", "localhost", port_http) {
return
}
// Создаем HTTP сервер
httpServer = &http.Server{
Addr: ":" + port_http,
Handler: nil,
}
tools.Logs_file(0, "HTTP ", "💻 HTTP сервер запущен на порту 80", "logs_http.log", true)
if err := httpServer.ListenAndServe(); err != nil {
// Игнорируем нормальную ошибку при остановке сервера
if err.Error() != "http: Server closed" {
tools.Logs_file(1, "HTTP", "❌ Ошибка запуска сервера: "+err.Error(), "logs_http.log", true)
}
}
}
// StopHTTPServer останавливает HTTP сервер
func StopHTTPServer() {
if httpServer != nil {
httpServer.Close()
httpServer = nil
tools.Logs_file(0, "HTTP", "HTTP сервер остановлен", "logs_http.log", true)
}
}

View File

@@ -0,0 +1,179 @@
package webserver
import (
"crypto/tls"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
tools "vServer/Backend/tools"
)
var certDir = "WebServer/cert/"
var certMap map[string]*tls.Certificate
var fallbackCert *tls.Certificate
var httpsServer *http.Server
var port_https string = "443"
// Запуск https сервера
func StartHTTPS() {
if tools.Port_check("HTTPS", "localhost", port_https) {
return
}
// Отключаем вывод ошибок TLS в консоль
log.SetOutput(io.Discard)
// Конфигурация TLS
tlsConfig := &tls.Config{
GetCertificate: func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) {
serverName := chi.ServerName
if serverName == "" {
tools.Logs_file(1, "HTTPS", "⚠️ Подключение без SNI (возможно по IP)", "logs_https.log", false)
} else if cert, ok := certMap[serverName]; ok {
// Найден точный сертификат для домена
return cert, nil
} else {
// Пробуем найти сертификат для родительского домена
parentDomain := getParentDomain(serverName)
if parentDomain != "" {
if cert, ok := certMap[parentDomain]; ok {
tools.Logs_file(1, "HTTPS", "✅ Используем сертификат родительского домена "+parentDomain+" для "+serverName, "logs_https.log", false)
return cert, nil
}
}
tools.Logs_file(1, "HTTPS", "⚠️ Нет сертификата для: "+serverName, "logs_https.log", false)
}
if fallbackCert != nil {
tools.Logs_file(1, "HTTPS", "⚠️ Используем fallback-сертификат", "logs_https.log", false)
return fallbackCert, nil
}
tools.Logs_file(1, "HTTPS", "❌ Нет fallback-сертификата — соединение будет отклонено", "logs_https.log", true)
return nil, nil
},
}
// Запуск сервера
httpsServer = &http.Server{
Addr: ":" + port_https,
TLSConfig: tlsConfig,
Handler: nil,
}
tools.Logs_file(0, "HTTPS", "✅ HTTPS сервер запущен на порту "+port_https, "logs_https.log", true)
if err := httpsServer.ListenAndServeTLS("", ""); err != nil {
// Игнорируем нормальную ошибку при остановке сервера
if err.Error() != "http: Server closed" {
tools.Logs_file(1, "HTTPS", "❌ Ошибка запуска сервера: "+err.Error(), "logs_https.log", true)
}
}
}
// Извлекает родительский домен из поддомена
func getParentDomain(domain string) string {
parts := strings.Split(domain, ".")
if len(parts) <= 2 {
return "" // Уже основной домен или некорректный формат
}
// Возвращаем домен без первого поддомена
return strings.Join(parts[1:], ".")
}
// Проверяет, существует ли сертификат для домена
func Cert_start() {
fallbackCert = loadFallbackCertificate(filepath.Join(certDir, "no_cert"))
certMap = loadCertificates(certDir)
}
func checkHostCert(r *http.Request) bool {
if _, err := os.Stat(certDir + r.Host); err != nil {
return false
}
return true
}
func loadCertificates(certDir string) map[string]*tls.Certificate {
certMap := make(map[string]*tls.Certificate)
entries, err := os.ReadDir(certDir)
if err != nil {
tools.Logs_file(1, "HTTPS", "📁 Ошибка чтения каталога сертификатов: "+err.Error(), "logs_https.log", true)
}
for _, entry := range entries {
if !entry.IsDir() || entry.Name() == "no_cert" {
continue
}
domain := entry.Name()
certPath := filepath.Join(certDir, domain, "certificate.crt")
keyPath := filepath.Join(certDir, domain, "private.key")
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
tools.Logs_file(1, "HTTPS", "⚠️ Ошибка загрузки сертификата для "+domain+": "+err.Error(), "logs_https.log", true)
continue
}
certMap[domain] = &cert
tools.Logs_file(0, "HTTPS", "✅ Загрузили сертификат для: "+tools.Color(domain, tools.Голубой), "logs_https.log", true)
}
return certMap
}
func loadFallbackCertificate(fallbackDir string) *tls.Certificate {
certPath := filepath.Join(fallbackDir, "certificate.crt")
keyPath := filepath.Join(fallbackDir, "private.key")
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
tools.Logs_file(1, "HTTPS", "⚠️ Не удалось загрузить fallback-сертификат: "+err.Error(), "logs_https.log", true)
return nil
}
tools.Logs_file(0, "HTTPS", "✅ Fallback-сертификат загружен", "logs_https.log", true)
return &cert
}
func ReloadCertificates() {
fmt.Println("")
fmt.Println("🔒 Перезагружаем SSL сертификаты...")
fmt.Println("")
// Выгружаем старые сертификаты
certMap = make(map[string]*tls.Certificate)
fallbackCert = nil
fmt.Println("⏹️ Старые сертификаты выгружены")
// Загружаем сертификаты заново
Cert_start()
fmt.Println("✅ SSL сертификаты успешно перезагружены!")
fmt.Println("")
}
// StopHTTPSServer останавливает HTTPS сервер
func StopHTTPSServer() {
// Останавливаем HTTPS сервер
if httpsServer != nil {
httpsServer.Close()
httpsServer = nil
tools.Logs_file(0, "HTTPS", "HTTPS сервер остановлен", "logs_https.log", true)
}
}

View File

@@ -0,0 +1,505 @@
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)
}

View File

@@ -0,0 +1,154 @@
package webserver
import (
"crypto/tls"
"io"
"log"
"net/http"
"strings"
"sync"
tools "vServer/Backend/tools"
)
// ProxyConfig хранит конфигурацию для прокси
type ProxyConfig struct {
ExternalDomain string
LocalAddress string
LocalPort string
UseHTTPS bool
}
var (
proxyConfigs = make(map[int]*ProxyConfig)
configMutex sync.RWMutex
configsLoaded = false
)
// InitProxyConfigs инициализирует конфигурации прокси один раз при старте
func InitProxyConfigs() {
configMutex.Lock()
defer configMutex.Unlock()
if configsLoaded {
return
}
// Конфигурация 1
config1 := &ProxyConfig{
ExternalDomain: "git.voxsel.ru",
LocalAddress: "127.0.0.1",
LocalPort: "3333",
UseHTTPS: false, // Локальный сервис работает по HTTP
}
proxyConfigs[1] = config1
// Конфигурация 2
config2 := &ProxyConfig{
ExternalDomain: "localhost",
LocalAddress: "127.0.0.1",
LocalPort: "8000",
UseHTTPS: false, // Локальный сервис работает по HTTP
}
proxyConfigs[2] = config2
configsLoaded = true
}
func StartHandlerProxy(w http.ResponseWriter, r *http.Request) (valid bool) {
valid = false
// Инициализируем конфигурации если еще не сделано
if !configsLoaded {
InitProxyConfigs()
}
configMutex.RLock()
defer configMutex.RUnlock()
// Выбираем конфигурацию (пока используем 1)
config := proxyConfigs[1]
if config == nil {
return false
}
if r.Host == config.ExternalDomain {
valid = true
// Определяем протокол для локального соединения
protocol := "http"
if config.UseHTTPS {
protocol = "https"
}
// Проксирование на локальный адрес
proxyURL := protocol + "://" + config.LocalAddress + ":" + config.LocalPort + r.URL.RequestURI()
proxyReq, err := http.NewRequest(r.Method, proxyURL, r.Body)
if err != nil {
http.Error(w, "Ошибка создания прокси-запроса", http.StatusInternalServerError)
return
}
// Копируем ВСЕ заголовки без изменений (кроме технических)
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)
}
}
// Прозрачная передача - никаких дополнительных заголовков
// Все заголовки уже скопированы выше "как есть"
// Выполняем прокси-запрос
client := &http.Client{
// Отключаем автоматическое следование редиректам для корректной работы с авторизацией
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
// Для HTTPS соединений настраиваем TLS (если понадобится)
if config.UseHTTPS {
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", true)
return
}
defer resp.Body.Close()
// Прозрачно копируем ВСЕ заголовки ответа без изменений
for name, values := range resp.Header {
for _, value := range values {
w.Header().Add(name, value)
}
}
// Устанавливаем статус код
w.WriteHeader(resp.StatusCode)
// Копируем тело ответа
if _, err := io.Copy(w, resp.Body); err != nil {
log.Printf("Ошибка копирования тела ответа: %v", err)
}
return valid
} else {
return valid
}
}

View File

@@ -0,0 +1,422 @@
package webserver
import (
"bufio"
"net/http"
"os"
"path/filepath"
"strings"
tools "vServer/Backend/tools"
)
// Структура для правила vAccess
type VAccessRule struct {
Type string // "Allow" или "Disable"
TypeFile []string // Список расширений файлов
PathAccess []string // Список путей для применения правила
IPList []string // Список IP адресов для фильтрации
ExceptionsDir []string // Список путей-исключений (не применять правило к этим путям)
UrlError string // Страница ошибки: "404", внешний URL или локальный путь
}
// Структура для конфигурации vAccess
type VAccessConfig struct {
Rules []VAccessRule
}
// Проверка валидности правила
func isValidRule(rule *VAccessRule) bool {
// Минимум нужен Type
if rule.Type == "" {
return false
}
// Должно быть хотя бы одно условие: type_file, path_access или ip_list
hasCondition := len(rule.TypeFile) > 0 || len(rule.PathAccess) > 0 || len(rule.IPList) > 0
return hasCondition
}
// Парсинг vAccess.conf файла
func parseVAccessFile(filePath string) (*VAccessConfig, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()
config := &VAccessConfig{}
scanner := bufio.NewScanner(file)
var currentRule *VAccessRule
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
// Пропускаем пустые строки
if line == "" {
continue
}
// Комментарии разделяют правила
if strings.HasPrefix(line, "#") {
// Если есть текущее правило, сохраняем его перед началом нового
if currentRule != nil && isValidRule(currentRule) {
config.Rules = append(config.Rules, *currentRule)
currentRule = nil
}
continue
}
// Парсим строки конфигурации
if strings.HasPrefix(line, "type:") {
// Создаём новое правило только если его нет
if currentRule == nil {
currentRule = &VAccessRule{}
}
currentRule.Type = strings.TrimSpace(strings.TrimPrefix(line, "type:"))
} else if strings.HasPrefix(line, "type_file:") && currentRule != nil {
fileTypes := strings.TrimSpace(strings.TrimPrefix(line, "type_file:"))
// Разбиваем по запятым и очищаем пробелы
for _, fileType := range strings.Split(fileTypes, ",") {
fileType = strings.TrimSpace(fileType)
if fileType != "" {
currentRule.TypeFile = append(currentRule.TypeFile, fileType)
}
}
} else if strings.HasPrefix(line, "path_access:") && currentRule != nil {
pathAccess := strings.TrimSpace(strings.TrimPrefix(line, "path_access:"))
// Разбиваем по запятым и очищаем пробелы
for _, path := range strings.Split(pathAccess, ",") {
path = strings.TrimSpace(path)
if path != "" {
currentRule.PathAccess = append(currentRule.PathAccess, path)
}
}
} else if strings.HasPrefix(line, "ip_list:") && currentRule != nil {
ipList := strings.TrimSpace(strings.TrimPrefix(line, "ip_list:"))
// Разбиваем по запятым и очищаем пробелы
for _, ip := range strings.Split(ipList, ",") {
ip = strings.TrimSpace(ip)
if ip != "" {
currentRule.IPList = append(currentRule.IPList, ip)
}
}
} else if strings.HasPrefix(line, "exceptions_dir:") && currentRule != nil {
exceptionsDir := strings.TrimSpace(strings.TrimPrefix(line, "exceptions_dir:"))
// Разбиваем по запятым и очищаем пробелы
for _, exception := range strings.Split(exceptionsDir, ",") {
exception = strings.TrimSpace(exception)
if exception != "" {
currentRule.ExceptionsDir = append(currentRule.ExceptionsDir, exception)
}
}
} else if strings.HasPrefix(line, "url_error:") && currentRule != nil {
currentRule.UrlError = strings.TrimSpace(strings.TrimPrefix(line, "url_error:"))
}
}
// Добавляем последнее правило если оно валидно
if currentRule != nil && isValidRule(currentRule) {
config.Rules = append(config.Rules, *currentRule)
}
return config, scanner.Err()
}
// Поиск всех vAccess.conf файлов от корня сайта до запрашиваемого пути
func findVAccessFiles(requestPath string, host string) []string {
var configFiles []string
// Базовый путь к сайту (НЕ public_www, а уровень выше)
basePath := "WebServer/www/" + host
// Проверяем корневой vAccess.conf
rootConfigPath := filepath.Join(basePath, "vAccess.conf")
if _, err := os.Stat(rootConfigPath); err == nil {
configFiles = append(configFiles, rootConfigPath)
}
// Разбиваем путь на части для поиска вложенных конфигов
pathParts := strings.Split(strings.Trim(requestPath, "/"), "/")
currentPath := basePath
for _, part := range pathParts {
if part == "" {
continue
}
currentPath = filepath.Join(currentPath, part)
configPath := filepath.Join(currentPath, "vAccess.conf")
if _, err := os.Stat(configPath); err == nil {
configFiles = append(configFiles, configPath)
}
}
return configFiles
}
// Проверка соответствия пути правилу
func matchPath(rulePath, requestPath string) bool {
// Если правило заканчивается на /*, проверяем префикс
if strings.HasSuffix(rulePath, "/*") {
prefix := strings.TrimSuffix(rulePath, "/*")
// Специальный случай: /* должен совпадать со всеми путями
if prefix == "" {
return true
}
return strings.HasPrefix(requestPath, prefix)
}
// Точное совпадение
return rulePath == requestPath
}
// Извлечение всех расширений из пути
func getAllExtensionsFromPath(filePath string) []string {
var extensions []string
// Разбиваем путь на части по слэшам
parts := strings.Split(filePath, "/")
for _, part := range parts {
// Ищем все точки в каждой части пути
if strings.Contains(part, ".") {
// Находим все расширения в части (может быть несколько: file.tar.gz)
dotIndex := strings.Index(part, ".")
for dotIndex != -1 && dotIndex < len(part)-1 {
// Извлекаем расширение от точки до следующей точки или конца
nextDotIndex := strings.Index(part[dotIndex+1:], ".")
if nextDotIndex == -1 {
// Последнее расширение
ext := strings.ToLower(part[dotIndex:])
if ext != "." && len(ext) > 1 {
extensions = append(extensions, ext)
}
break
} else {
// Промежуточное расширение
ext := strings.ToLower(part[dotIndex : dotIndex+1+nextDotIndex+1])
if ext != "." && len(ext) > 1 {
extensions = append(extensions, ext)
}
dotIndex = dotIndex + 1 + nextDotIndex
}
}
}
}
return extensions
}
// Проверка соответствия расширений файла
// Возвращает true если ВСЕ найденные расширения разрешены
func matchFileExtension(ruleExtensions []string, filePath string) bool {
// Получаем все расширения из пути
pathExtensions := getAllExtensionsFromPath(filePath)
// Если расширений нет, проверяем есть ли no_extension в правилах
if len(pathExtensions) == 0 {
for _, ruleExt := range ruleExtensions {
ruleExt = strings.ToLower(strings.TrimSpace(ruleExt))
if ruleExt == "no_extension" {
return true
}
}
return false
}
// Проверяем каждое найденное расширение
for _, pathExt := range pathExtensions {
found := false
for _, ruleExt := range ruleExtensions {
ruleExt = strings.ToLower(strings.TrimSpace(ruleExt))
// Поддержка паттернов типа *.php
if strings.HasPrefix(ruleExt, "*.") {
if pathExt == strings.TrimPrefix(ruleExt, "*") {
found = true
break
}
} else if ruleExt == pathExt {
found = true
break
}
}
// Если хотя бы одно расширение не найдено в правилах - блокируем
if !found {
return false
}
}
// Все расширения найдены в правилах
return true
}
// Получение реального IP адреса клиента из соединения (без заголовков прокси)
func getClientIP(r *http.Request) string {
// Извлекаем IP из RemoteAddr (формат: "IP:port")
ip := r.RemoteAddr
if idx := strings.LastIndex(ip, ":"); idx != -1 {
ip = ip[:idx]
}
// Убираем квадратные скобки для IPv6
ip = strings.Trim(ip, "[]")
return ip
}
// Проверка соответствия IP адреса правилу
func matchIPAddress(ruleIPs []string, clientIP string) bool {
if len(ruleIPs) == 0 {
return true // Если IP не указаны, то проверка пройдена
}
for _, ruleIP := range ruleIPs {
ruleIP = strings.TrimSpace(ruleIP)
if ruleIP == clientIP {
return true
}
}
return false
}
// Проверка исключений - возвращает true если путь находится в исключениях
func matchExceptions(exceptions []string, requestPath string) bool {
if len(exceptions) == 0 {
return false // Нет исключений
}
for _, exception := range exceptions {
exception = strings.TrimSpace(exception)
if matchPath(exception, requestPath) {
return true // Путь найден в исключениях
}
}
return false
}
// Основная функция проверки доступа
// Возвращает (разрешён_доступ, страница_ошибки)
func CheckVAccess(requestPath string, host string, r *http.Request) (bool, string) {
// Находим все vAccess.conf файлы
configFiles := findVAccessFiles(requestPath, host)
if len(configFiles) == 0 {
// Нет конфигурационных файлов - разрешаем доступ
return true, ""
}
// Применяем правила по порядку (от корня к файлу)
for _, configFile := range configFiles {
config, err := parseVAccessFile(configFile)
if err != nil {
tools.Logs_file(1, "vAccess", "❌ Ошибка парсинга "+configFile+": "+err.Error(), "logs_vaccess.log", false)
continue
}
// Проверяем каждое правило в конфиге
for _, rule := range config.Rules {
// Проверяем соответствие путей (если указаны)
pathMatched := true // По умолчанию true, если путей нет
if len(rule.PathAccess) > 0 {
pathMatched = false
for _, rulePath := range rule.PathAccess {
if matchPath(rulePath, requestPath) {
pathMatched = true
break
}
}
}
// Если путь не совпадает - переходим к следующему правилу
if !pathMatched {
continue
}
// Проверяем исключения - если путь в исключениях, пропускаем правило
if matchExceptions(rule.ExceptionsDir, requestPath) {
continue
}
// Проверяем соответствие расширения файла (если указаны)
fileMatches := true // По умолчанию true, если типов файлов нет
if len(rule.TypeFile) > 0 {
fileMatches = matchFileExtension(rule.TypeFile, requestPath)
}
// Проверяем соответствие IP адреса (если указаны)
ipMatches := true // По умолчанию true, если IP не указаны
if len(rule.IPList) > 0 {
clientIP := getClientIP(r)
ipMatches = matchIPAddress(rule.IPList, clientIP)
}
// Применяем правило в зависимости от типа
switch rule.Type {
case "Allow":
// Allow правило: разрешаем только если ВСЕ условия выполнены
if (len(rule.TypeFile) > 0 && !fileMatches) || (len(rule.IPList) > 0 && !ipMatches) {
// Условия НЕ выполнены - блокируем
errorPage := rule.UrlError
if errorPage == "" {
errorPage = "404" // По умолчанию 404
}
return false, errorPage
}
// Все условия Allow выполнены - разрешаем доступ
return true, ""
case "Disable":
// Disable правило: запрещаем если ЛЮБОЕ условие выполнено
if (len(rule.TypeFile) == 0 || fileMatches) && (len(rule.IPList) == 0 || ipMatches) {
errorPage := rule.UrlError
if errorPage == "" {
errorPage = "404" // По умолчанию 404
}
return false, errorPage
}
default:
// Неизвестный тип правила - игнорируем
continue
}
}
}
// Все проверки пройдены - разрешаем доступ
return true, ""
}
// Обработка страницы ошибки vAccess
func HandleVAccessError(w http.ResponseWriter, r *http.Request, errorPage string, host string) {
switch {
case errorPage == "404":
// Стандартная 404 страница
http.ServeFile(w, r, "WebServer/tools/error_page/index.html")
case strings.HasPrefix(errorPage, "http://") || strings.HasPrefix(errorPage, "https://"):
// Внешний сайт - редирект
http.Redirect(w, r, errorPage, http.StatusFound)
default:
// Локальный путь от public_www
localPath := "WebServer/www/" + host + "/public_www" + errorPage
if _, err := os.Stat(localPath); err == nil {
http.ServeFile(w, r, localPath)
} else {
// Файл не найден - показываем стандартную 404
http.ServeFile(w, r, "WebServer/tools/error_page/index.html")
tools.Logs_file(1, "vAccess", "❌ Страница ошибки не найдена: "+localPath, "logs_vaccess.log", false)
}
}
}