commit 7a876172828b2eb9901d5cee251237dda1b326da Author: Falknat <> Date: Thu Oct 2 06:02:45 2025 +0700 Инициализация проекта Стабильный рабочий проект. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a2de2f2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Исключения +myapp.exe +.vscode/ +WebServer/www/home.voxsel.ru +WebServer/cert/* +!WebServer/cert/no_cert/ +WebServer/tools/logs/* +WebServer/soft/* +!WebServer/soft/soft.rar +.cursorrules \ No newline at end of file diff --git a/Backend/WebServer/MySQL.go b/Backend/WebServer/MySQL.go new file mode 100644 index 0000000..4075656 --- /dev/null +++ b/Backend/WebServer/MySQL.go @@ -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) + } + + } + +} diff --git a/Backend/WebServer/cmd_console.go b/Backend/WebServer/cmd_console.go new file mode 100644 index 0000000..41e1ac2 --- /dev/null +++ b/Backend/WebServer/cmd_console.go @@ -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("") +} diff --git a/Backend/WebServer/handler.go b/Backend/WebServer/handler.go new file mode 100644 index 0000000..2650cde --- /dev/null +++ b/Backend/WebServer/handler.go @@ -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) + } + } + } +} diff --git a/Backend/WebServer/http_server.go b/Backend/WebServer/http_server.go new file mode 100644 index 0000000..6d01393 --- /dev/null +++ b/Backend/WebServer/http_server.go @@ -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) + } +} diff --git a/Backend/WebServer/https_server.go b/Backend/WebServer/https_server.go new file mode 100644 index 0000000..9156033 --- /dev/null +++ b/Backend/WebServer/https_server.go @@ -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) + } +} diff --git a/Backend/WebServer/php_server.go b/Backend/WebServer/php_server.go new file mode 100644 index 0000000..319b3c2 --- /dev/null +++ b/Backend/WebServer/php_server.go @@ -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) +} diff --git a/Backend/WebServer/proxy_server.go b/Backend/WebServer/proxy_server.go new file mode 100644 index 0000000..af80ee9 --- /dev/null +++ b/Backend/WebServer/proxy_server.go @@ -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 + } +} diff --git a/Backend/WebServer/vAccess.go b/Backend/WebServer/vAccess.go new file mode 100644 index 0000000..c8e8bb0 --- /dev/null +++ b/Backend/WebServer/vAccess.go @@ -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) + } + } +} diff --git a/Backend/admin/embed.go b/Backend/admin/embed.go new file mode 100644 index 0000000..c5399b6 --- /dev/null +++ b/Backend/admin/embed.go @@ -0,0 +1,21 @@ +package admin + +import ( + "embed" + "io/fs" + "os" +) + +//go:embed html +var AdminHTML embed.FS + +// Флаг для переключения между embed и файловой системой +var UseEmbedded bool = true + +// Получает файловую систему в зависимости от флага +func GetFileSystem() fs.FS { + if UseEmbedded { + return AdminHTML + } + return os.DirFS("Backend/admin/") +} diff --git a/Backend/admin/go/admin_server.go b/Backend/admin/go/admin_server.go new file mode 100644 index 0000000..9ff9a90 --- /dev/null +++ b/Backend/admin/go/admin_server.go @@ -0,0 +1,46 @@ +package admin + +import ( + "net/http" + command "vServer/Backend/admin/go/command" + config "vServer/Backend/config" + tools "vServer/Backend/tools" +) + +var adminServer *http.Server + +// Запуск Admin сервера +func StartAdmin() { + + // Получаем значения из конфига во время выполнения + port_admin := config.ConfigData.Soft_Settings.Admin_port + host_admin := config.ConfigData.Soft_Settings.Admin_host + + if tools.Port_check("ADMIN", host_admin, port_admin) { + return + } + + // Создаем оптимизированный мультиплексор для админ сервера + mux := http.NewServeMux() + + // Регистрируем специализированные обработчики (быстрая маршрутизация) + mux.HandleFunc("/api/", command.ApiHandler) // API эндпоинты + mux.HandleFunc("/json/", command.JsonHandler) // JSON данные + mux.HandleFunc("/service/", command.ServiceHandler) // Сервисные команды POST + mux.HandleFunc("/", command.StaticHandler) // Статические файлы + + // Создаем Admin сервер (только localhost для безопасности) + adminServer = &http.Server{ + Addr: host_admin + ":" + port_admin, + Handler: mux, + } + + tools.Logs_file(0, "ADMIN", "🛠️ Admin панель запущена на порту "+port_admin, "logs_http.log", true) + + if err := adminServer.ListenAndServe(); err != nil { + // Игнорируем нормальную ошибку при остановке сервера + if err.Error() != "http: Server closed" { + tools.Logs_file(1, "ADMIN", "❌ Ошибка запуска админ сервера: "+err.Error(), "logs_http.log", true) + } + } +} diff --git a/Backend/admin/go/command/commands.go b/Backend/admin/go/command/commands.go new file mode 100644 index 0000000..6721f64 --- /dev/null +++ b/Backend/admin/go/command/commands.go @@ -0,0 +1,122 @@ +package command + +import ( + "io/fs" + "net/http" + "path/filepath" + "strings" + webserver "vServer/Backend/WebServer" + admin "vServer/Backend/admin" + json "vServer/Backend/admin/go/json" +) + +func SecurePost(w http.ResponseWriter, r *http.Request) bool { + // Проверяем, что запрос POST (не GET из браузера) + + if webserver.Secure_post { + + if r.Method != "POST" { + http.Error(w, "Метод не разрешен. Используйте POST", http.StatusMethodNotAllowed) + return false + } + + } + + return true +} + +// API обработчик для /api/* +func ApiHandler(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + + switch path { + case "/api/metrics": + json.GetAllMetrics(w) + default: + http.NotFound(w, r) + } +} + +// JSON обработчик для /json/* +func JsonHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + path := r.URL.Path + + switch path { + case "/json/server_status.json": + w.Write(json.GetServerStatusJSON()) + case "/json/menu.json": + w.Write(json.GetMenuJSON()) + default: + http.NotFound(w, r) + } +} + +// Обработчик сервисных команд для /service/* +func ServiceHandler(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + + if !SecurePost(w, r) { + return + } + + if SiteList(w, r, path) { + return + } + + if site_add(w, path) { + return + } + + if Service_Run(w, r, path) { + return + } + http.NotFound(w, r) +} + +// Обработчик статических файлов из embed +func StaticHandler(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + + // Убираем ведущий слэш + path = strings.TrimPrefix(path, "/") + + // Если пустой путь, показываем index.html + if path == "" { + path = "html/index.html" + } else { + path = "html/" + path + } + + // Читаем файл из файловой системы (embed или диск) + fileSystem := admin.GetFileSystem() + content, err := fs.ReadFile(fileSystem, path) + if err != nil { + http.NotFound(w, r) + return + } + + // Устанавливаем правильный Content-Type + ext := filepath.Ext(path) + switch ext { + case ".css": + w.Header().Set("Content-Type", "text/css") + case ".js": + w.Header().Set("Content-Type", "application/javascript") + case ".json": + w.Header().Set("Content-Type", "application/json") + case ".html": + w.Header().Set("Content-Type", "text/html") + } + + // Предотвращаем кеширование в режиме разработки (когда UseEmbedded = false) + if !admin.UseEmbedded { + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Set("Pragma", "no-cache") + w.Header().Set("Expires", "0") + } + + // Отдаем файл + w.Write(content) + +} diff --git a/Backend/admin/go/command/service_run.go b/Backend/admin/go/command/service_run.go new file mode 100644 index 0000000..df9b426 --- /dev/null +++ b/Backend/admin/go/command/service_run.go @@ -0,0 +1,57 @@ +package command + +import ( + "net/http" + webserver "vServer/Backend/WebServer" + json "vServer/Backend/admin/go/json" +) + +// Обработчик команд управления серверами +func Service_Run(w http.ResponseWriter, r *http.Request, path string) bool { + + switch path { + case "/service/MySql_Stop": + webserver.StopMySQLServer() + json.UpdateServerStatus("MySQL Server", "stopped") + return true + + case "/service/MySql_Start": + webserver.StartMySQLServer(false) + json.UpdateServerStatus("MySQL Server", "running") + return true + + case "/service/Http_Stop": + webserver.StopHTTPServer() + json.UpdateServerStatus("HTTP Server", "stopped") + return true + + case "/service/Http_Start": + go webserver.StartHTTP() + json.UpdateServerStatus("HTTP Server", "running") + return true + + case "/service/Https_Stop": + webserver.StopHTTPSServer() + json.UpdateServerStatus("HTTPS Server", "stopped") + return true + + case "/service/Https_Start": + go webserver.StartHTTPS() + json.UpdateServerStatus("HTTPS Server", "running") + return true + + case "/service/Php_Start": + webserver.PHP_Start() + json.UpdateServerStatus("PHP Server", "running") + return true + + case "/service/Php_Stop": + webserver.PHP_Stop() + json.UpdateServerStatus("PHP Server", "stopped") + return true + + default: + http.NotFound(w, r) + return false // Команда не найдена + } +} diff --git a/Backend/admin/go/command/site_list.go b/Backend/admin/go/command/site_list.go new file mode 100644 index 0000000..15200c8 --- /dev/null +++ b/Backend/admin/go/command/site_list.go @@ -0,0 +1,269 @@ +package command + +import ( + "encoding/json" + "net/http" + "os" + "regexp" + "strings" + "vServer/Backend/config" + "vServer/Backend/tools" +) + +var entries []os.DirEntry + +func path_www() { + + wwwPath, err := tools.AbsPath("WebServer/www") + tools.Error_check(err, "Ошибка получения пути") + + entries, err = os.ReadDir(wwwPath) + tools.Error_check(err, "Ошибка чтения директории") + +} + +func site_type(entry os.DirEntry) string { + + certPath, err := tools.AbsPath("WebServer/cert/" + entry.Name()) + if err == nil { + if _, err := os.Stat(certPath); err == nil { + return "https" + } + } + return "http" +} + +func site_alliace(siteName string) []string { + // Получаем абсолютный путь к config.json + configPath, err := tools.AbsPath("WebServer/config.json") + tools.Error_check(err, "Ошибка получения пути к config.json") + + // Читаем содержимое config.json + configData, err := os.ReadFile(configPath) + tools.Error_check(err, "Ошибка чтения config.json") + + // Структура для парсинга Site_www + type SiteConfig struct { + Name string `json:"name"` + Host string `json:"host"` + Alias []string `json:"alias"` + Status string `json:"status"` + } + type Config struct { + SiteWWW []SiteConfig `json:"Site_www"` + } + + var config Config + err = json.Unmarshal(configData, &config) + tools.Error_check(err, "Ошибка парсинга config.json") + + // Ищем алиасы для конкретного сайта + for _, site := range config.SiteWWW { + if site.Host == siteName { + return site.Alias + } + } + + // Возвращаем пустой массив если сайт не найден + return []string{} +} + +func site_status(siteName string) string { + configPath := "WebServer/config.json" + + // Читаем конфигурационный файл + data, err := os.ReadFile(configPath) + if err != nil { + return "error" + } + + // Парсим JSON + var config map[string]interface{} + if err := json.Unmarshal(data, &config); err != nil { + return "error" + } + + // Получаем список сайтов + siteWww, ok := config["Site_www"].([]interface{}) + if !ok { + return "error" + } + + // Ищем сайт по host + for _, siteInterface := range siteWww { + site, ok := siteInterface.(map[string]interface{}) + if !ok { + continue + } + + // Проверяем только по host (имя папки) + if host, ok := site["host"].(string); ok && host == siteName { + if status, ok := site["status"].(string); ok { + return status + } + } + } + + return "inactive" +} + +func SiteList(w http.ResponseWriter, r *http.Request, path string) bool { + + switch path { + + case "/service/Site_List": + sites := []map[string]interface{}{} + + path_www() + + for _, entry := range entries { + if entry.IsDir() { + site := map[string]interface{}{ + "host": entry.Name(), + "type": site_type(entry), + "aliases": site_alliace(entry.Name()), + "status": site_status(entry.Name()), + } + sites = append(sites, site) + } + } + + metrics := map[string]interface{}{ + "sites": sites, + } + + data, _ := json.MarshalIndent(metrics, "", " ") + w.Write(data) + + return true + + } + + return false + +} + +func addSiteToConfig(siteName string) error { + // Получаем абсолютный путь к config.json + configPath, err := tools.AbsPath("WebServer/config.json") + if err != nil { + return err + } + + // Читаем содержимое config.json + configData, err := os.ReadFile(configPath) + if err != nil { + return err + } + + // Парсим как общий JSON объект + var config map[string]interface{} + err = json.Unmarshal(configData, &config) + if err != nil { + return err + } + + // Получаем массив сайтов + siteWWW, ok := config["Site_www"].([]interface{}) + if !ok { + siteWWW = []interface{}{} + } + + // Создаем новый сайт в том же формате что уже есть + newSite := map[string]interface{}{ + "name": siteName, + "host": siteName, + "alias": []string{""}, + "status": "active", + } + + // Добавляем новый сайт в массив + config["Site_www"] = append(siteWWW, newSite) + + // Сохраняем обновленный конфиг + updatedData, err := json.MarshalIndent(config, "", " ") + if err != nil { + return err + } + + // Делаем массивы алиасов в одну строку + re := regexp.MustCompile(`"alias": \[\s+"([^"]*?)"\s+\]`) + compactData := re.ReplaceAll(updatedData, []byte(`"alias": ["$1"]`)) + + // Исправляем отступы после Site_www + dataStr := string(compactData) + dataStr = strings.ReplaceAll(dataStr, ` ], + "Soft_Settings"`, ` ], + + "Soft_Settings"`) + compactData = []byte(dataStr) + + err = os.WriteFile(configPath, compactData, 0644) + if err != nil { + return err + } + + return nil +} + +func site_add(w http.ResponseWriter, path string) bool { + + // URL параметры: /service/Site_Add/sitename + if strings.HasPrefix(path, "/service/Site_Add/") { + siteName := strings.TrimPrefix(path, "/service/Site_Add/") + + if siteName == "" { + w.WriteHeader(400) + w.Write([]byte(`{"status":"error","message":"Не указано имя сайта в URL"}`)) + return true + } + + wwwPath := "WebServer/www/" + siteName + + // Проверяем существует ли уже такой сайт + if _, err := os.Stat(wwwPath); err == nil { + w.WriteHeader(409) // Conflict + w.Write([]byte(`{"status":"error","message":"Сайт ` + siteName + ` уже существует"}`)) + return true + } + + err := os.MkdirAll(wwwPath, 0755) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(`{"status":"error","message":"Ошибка создания папки сайта"}`)) + return true + } + + // Добавляем сайт в config.json + err = addSiteToConfig(siteName) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(`{"status":"error","message":"Ошибка добавления в конфигурацию: ` + err.Error() + `"}`)) + return true + } + + // Создаем папку public_www + publicWwwPath := wwwPath + "/public_www" + err = os.MkdirAll(publicWwwPath, 0755) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(`{"status":"error","message":"Ошибка создания папки public_www"}`)) + return true + } + + indexFilePath := wwwPath + "/public_www/index.html" + indexContent := "Привет друг! Твой сайт создан!" + err = os.WriteFile(indexFilePath, []byte(indexContent), 0644) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(`{"status":"error","message":"Ошибка создания index.html: ` + err.Error() + `"}`)) + return true + } + + w.Write([]byte(`{"status":"ok","message":"Сайт ` + siteName + ` успешно создан и добавлен в конфигурацию"}`)) + config.LoadConfig() + return true + } + + return false +} diff --git a/Backend/admin/go/json/json.go b/Backend/admin/go/json/json.go new file mode 100644 index 0000000..f41ea81 --- /dev/null +++ b/Backend/admin/go/json/json.go @@ -0,0 +1,45 @@ +package json + +import "encoding/json" + +// Данные серверов +var ServerStatus = []map[string]interface{}{ + {"NameService": "HTTP Server", "Port": 80, "Status": "stopped"}, + {"NameService": "HTTPS Server", "Port": 443, "Status": "stopped"}, + {"NameService": "PHP Server", "Port": 9000, "Status": "stopped"}, + {"NameService": "MySQL Server", "Port": 3306, "Status": "stopped"}, +} + +// Данные меню +var MenuData = []map[string]interface{}{ + {"name": "Dashboard", "icon": "🏠", "url": "#dashboard", "active": true}, + {"name": "Серверы", "icon": "🖥️", "url": "#servers", "active": false}, + {"name": "Сайты", "icon": "🌐", "url": "#sites", "active": false}, + {"name": "SSL Сертификаты", "icon": "🔒", "url": "#certificates", "active": false}, + {"name": "Файловый менеджер", "icon": "📁", "url": "#files", "active": false}, + {"name": "Базы данных", "icon": "🗄️", "url": "#databases", "active": false}, + {"name": "Логи", "icon": "📋", "url": "#logs", "active": false}, + {"name": "Настройки", "icon": "⚙️", "url": "#settings", "active": false}, +} + +// Функция обновления статуса сервера +func UpdateServerStatus(serviceName, status string) { + for i := range ServerStatus { + if ServerStatus[i]["NameService"] == serviceName { + ServerStatus[i]["Status"] = status + break + } + } +} + +// Получить JSON серверов +func GetServerStatusJSON() []byte { + data, _ := json.Marshal(ServerStatus) + return data +} + +// Получить JSON меню +func GetMenuJSON() []byte { + data, _ := json.Marshal(MenuData) + return data +} diff --git a/Backend/admin/go/json/metrics.go b/Backend/admin/go/json/metrics.go new file mode 100644 index 0000000..c09170d --- /dev/null +++ b/Backend/admin/go/json/metrics.go @@ -0,0 +1,130 @@ +package json + +import ( + "encoding/json" + "net/http" + "strings" + "time" + "vServer/Backend/tools" +) + +var CPU_NAME string +var CPU_GHz string +var CPU_Cores string +var CPU_Using string +var Disk_Size string +var Disk_Free string +var Disk_Used string +var Disk_Name string +var RAM_Using string +var RAM_Total string + +// Инициализация при запуске пакета +func init() { + // Загружаем статичные данные один раз при старте + UpdateMetrics() + // Загружаем динамические данные в фоне + go updateMetricsBackground() +} + +func UpdateMetrics() { + commands := []string{ + `$name = (Get-WmiObject Win32_DiskDrive | Where-Object { $_.Index -eq (Get-WmiObject Win32_DiskPartition | Where-Object { $_.DeviceID -eq ((Get-WmiObject Win32_LogicalDiskToPartition | Where-Object { $_.Dependent -match "C:" }).Antecedent -split '"')[1] }).DiskIndex }).Model`, + `$size = "{0} GB" -f [math]::Round(((Get-PSDrive -Name C).Used + (Get-PSDrive -Name C).Free) / 1GB)`, + "$cpuInfo = Get-CimInstance Win32_Processor", + "$cpuCores = $cpuInfo.NumberOfCores", + "$cpuName = $cpuInfo.Name", + `$ram_total = [math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB, 2)`, + "Write-Output \"$cpuName|$cpuCores|$name|$size|$ram_total\"", + } + + // Выполняем команды и получаем результат + result := tools.RunPersistentScript(commands) + + // Парсим результат для статичных данных + parts := strings.Split(result, "|") + if len(parts) >= 4 { + cpuName := strings.TrimSpace(parts[0]) + cpuCores := strings.TrimSpace(parts[1]) + diskName := strings.TrimSpace(parts[2]) + diskSize := strings.TrimSpace(parts[3]) + ramTotal := strings.TrimSpace(parts[4]) + + // Обновляем глобальные переменные + CPU_NAME = cpuName + CPU_Cores = cpuCores + Disk_Name = diskName + Disk_Size = diskSize + RAM_Total = ramTotal + } +} + +// Фоновое обновление динамических метрик (внутреннее) +func updateMetricsBackground() { + + updateDynamicMetrics := func() { + commands := []string{ + "$cpuInfo = Get-CimInstance Win32_Processor", + "$cpuGHz = $cpuInfo.MaxClockSpeed", + "$cpuUsage = $cpuInfo.LoadPercentage", + `$used = "{0} GB" -f [math]::Round(((Get-PSDrive -Name C).Used / 1GB))`, + `$free = "{0} GB" -f [math]::Round(((Get-PSDrive -Name C).Free / 1GB))`, + `$ram_using = [math]::Round((Get-CimInstance Win32_OperatingSystem | % {($_.TotalVisibleMemorySize - $_.FreePhysicalMemory) / 1MB}), 2)`, + "Write-Output \"$cpuGHz|$cpuUsage|$used|$free|$ram_using\"", + } + + // Один запуск PowerShell для динамических команд! + result := tools.RunPersistentScript(commands) + + // Парсим результат для динамических данных + parts := strings.Split(result, "|") + if len(parts) >= 4 { + cpuGHz := strings.TrimSpace(parts[0]) + cpuUsage := strings.TrimSpace(parts[1]) + diskUsed := strings.TrimSpace(parts[2]) + diskFree := strings.TrimSpace(parts[3]) + ramUsing := strings.TrimSpace(parts[4]) + // Обновляем глобальные переменные + CPU_GHz = cpuGHz + CPU_Using = cpuUsage + Disk_Used = diskUsed + Disk_Free = diskFree + RAM_Using = ramUsing + } + } + + // Выполняем сразу при запуске + updateDynamicMetrics() + + // Затем каждые 5 секунд + for range time.NewTicker(5 * time.Second).C { + updateDynamicMetrics() + } + +} + +// Получить JSON системных метрик +func GetAllMetrics(w http.ResponseWriter) { + w.Header().Set("Content-Type", "application/json") + w.Write(GetMetricsJSON()) +} + +// Получить JSON метрик +func GetMetricsJSON() []byte { + metrics := map[string]interface{}{ + "cpu_name": CPU_NAME, + "cpu_ghz": CPU_GHz, + "cpu_cores": CPU_Cores, + "cpu_usage": CPU_Using, + "disk_name": Disk_Name, + "disk_size": Disk_Size, + "disk_used": Disk_Used, + "disk_free": Disk_Free, + "ram_using": RAM_Using, + "ram_total": RAM_Total, + "server_uptime": tools.ServerUptime("get"), + } + + data, _ := json.Marshal(metrics) + return data +} diff --git a/Backend/admin/html/assets/css/dashboard.css b/Backend/admin/html/assets/css/dashboard.css new file mode 100644 index 0000000..615f4e2 --- /dev/null +++ b/Backend/admin/html/assets/css/dashboard.css @@ -0,0 +1,652 @@ +/* Стили дашборда админ-панели */ + +.dashboard-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 2rem; + margin-bottom: 6rem; +} + +@media (max-width: 1200px) { + .dashboard-grid { + grid-template-columns: 1fr; + gap: 1.5rem; + } +} + +/* Карточки дашборда */ +.dashboard-card { + background: linear-gradient(135deg, rgba(26, 37, 47, 0.95), rgba(20, 30, 40, 0.9)); + backdrop-filter: blur(20px); + border-radius: 16px; + border: 1px solid rgba(52, 152, 219, 0.25); + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.4), + 0 0 0 1px rgba(52, 152, 219, 0.1), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); + overflow: hidden; + position: relative; +} + +.dashboard-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, #3498db, #2ecc71, #9b59b6); + background-size: 400% 400%; + animation: gradientFlow 6s ease-in-out infinite; + box-shadow: 0 2px 10px rgba(52, 152, 219, 0.3); +} + +@keyframes gradientFlow { + 0%, 100% { + background-position: 0% 50%; + } + 25% { + background-position: 100% 50%; + } + 50% { + background-position: 200% 50%; + } + 75% { + background-position: 300% 50%; + } +} + + + + +.dashboard-card.full-width { + grid-column: 1 / -1; +} + +/* Заголовки карточек */ +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.2rem 1.5rem; + border-bottom: 1px solid rgba(52, 152, 219, 0.15); + background: linear-gradient(135deg, rgba(52, 152, 219, 0.08), rgba(46, 204, 113, 0.05)); + position: relative; +} + +.card-header::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, transparent, rgba(52, 152, 219, 0.3), transparent); +} + +.card-title { + display: flex; + align-items: center; + font-size: 1.4rem; + font-weight: 700; + color: #ecf0f1; + margin: 0; +} + +.card-icon { + font-size: 1.5rem; + margin-right: 1rem; +} + +/* Содержимое карточек */ +.card-content { + padding: 1.5rem 2rem 2rem 2rem; +} + +/* Статистика */ +.stats-row { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + margin-bottom: 1.5rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid rgba(52, 152, 219, 0.1); +} + +.stat-item { + text-align: center; +} + +.stat-number { + font-size: 2rem; + font-weight: 700; + background: linear-gradient(135deg, #3498db, #2ecc71); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 0.3rem; + display: block; +} + +.stat-number.active-stat { + background: linear-gradient(135deg, #2ecc71, #27ae60); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.stat-number.inactive-stat { + background: linear-gradient(135deg, #e74c3c, #c0392b); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.stat-number.valid-stat { + background: linear-gradient(135deg, #2ecc71, #27ae60); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.stat-number.warning-stat { + background: linear-gradient(135deg, #f39c12, #e67e22); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.stat-label { + color: #95a5a6; + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 1px; + font-weight: 500; +} + +/* Превью секции */ +.sites-preview, .certs-preview { + margin-top: 1rem; +} + +.preview-header { + margin-bottom: 1rem; +} + +.preview-title { + color: #bdc3c7; + font-size: 0.9rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 1px; +} + +/* Списки сайтов */ +.sites-list { + display: flex; + flex-direction: column; + gap: 0.8rem; +} + +.site-item { + display: flex; + align-items: center; + padding: 1rem; + background: rgba(52, 152, 219, 0.05); + border-radius: 8px; + border: 1px solid rgba(52, 152, 219, 0.1); + transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); + gap: 1rem; +} + +.site-item:hover { + background: rgba(52, 152, 219, 0.1); + transform: translateX(4px); + border-color: rgba(52, 152, 219, 0.2); +} + +.site-status { + width: 10px; + height: 10px; + border-radius: 50%; + flex-shrink: 0; +} + +.site-status.active { + background: #2ecc71; + box-shadow: 0 0 8px rgba(46, 204, 113, 0.5); +} + +.site-status.inactive { + background: #e74c3c; + box-shadow: 0 0 8px rgba(231, 76, 60, 0.5); +} + +.site-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.site-name { + color: #ecf0f1; + font-weight: 600; + font-size: 0.95rem; +} + +.site-details { + color: #95a5a6; + font-size: 0.8rem; +} + +.site-actions { + display: flex; + gap: 0.5rem; +} + +/* Списки сертификатов */ +.certs-list { + display: flex; + flex-direction: column; + gap: 0.8rem; +} + +.cert-item { + display: flex; + align-items: center; + padding: 1rem; + background: rgba(52, 152, 219, 0.05); + border-radius: 8px; + border: 1px solid rgba(52, 152, 219, 0.1); + transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); + gap: 1rem; +} + +.cert-item:hover { + background: rgba(52, 152, 219, 0.1); + transform: translateX(4px); + border-color: rgba(52, 152, 219, 0.2); +} + +.cert-status { + width: 10px; + height: 10px; + border-radius: 50%; + flex-shrink: 0; +} + +.cert-status.valid { + background: #2ecc71; + box-shadow: 0 0 8px rgba(46, 204, 113, 0.5); +} + +.cert-status.warning { + background: #f39c12; + box-shadow: 0 0 8px rgba(243, 156, 18, 0.5); +} + +.cert-status.expired { + background: #e74c3c; + box-shadow: 0 0 8px rgba(231, 76, 60, 0.5); +} + +.cert-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.cert-name { + color: #ecf0f1; + font-weight: 600; + font-size: 0.95rem; +} + +.cert-details { + color: #95a5a6; + font-size: 0.8rem; +} + +.cert-expires { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0.1rem; + min-width: 80px; +} + +.expires-date { + color: #bdc3c7; + font-size: 0.8rem; + font-weight: 500; +} + +.expires-days { + color: #95a5a6; + font-size: 0.7rem; +} + +.cert-expires.warning .expires-date { + color: #f39c12; +} + +.cert-expires.warning .expires-days { + color: #e67e22; +} + +.cert-actions { + display: flex; + gap: 0.5rem; +} + +/* Серверы */ +.servers-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1rem; +} + +.server-item { + display: flex; + align-items: center; + padding: 1rem; + background: rgba(52, 152, 219, 0.05); + border-radius: 8px; + border: 1px solid rgba(52, 152, 219, 0.1); + transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); +} + +.server-item:hover { + background: rgba(52, 152, 219, 0.1); + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); +} + +.server-status { + width: 12px; + height: 12px; + border-radius: 50%; + margin-right: 1rem; + flex-shrink: 0; + position: relative; +} + +.server-status.running { + background: #2ecc71; + box-shadow: 0 0 12px rgba(46, 204, 113, 0.5); + animation: pulse 2s infinite; +} + +.server-status.stopped { + background: #e74c3c; + box-shadow: 0 0 12px rgba(231, 76, 60, 0.5); +} + +.server-status.starting { + background: #f39c12; + box-shadow: 0 0 12px rgba(243, 156, 18, 0.5); + animation: pulse 1.5s infinite; +} + +.server-status.stopping { + background: #f39c12; + box-shadow: 0 0 12px rgba(243, 156, 18, 0.5); + animation: pulse 1.5s infinite; +} + +.server-status.warning { + background: #f39c12; + box-shadow: 0 0 12px rgba(243, 156, 18, 0.5); +} + +.server-info { + flex: 1; +} + +.server-name { + color: #ecf0f1; + font-weight: 600; + font-size: 1rem; + margin-bottom: 0.3rem; +} + +.server-details { + color: #95a5a6; + font-size: 0.85rem; +} + +/* Кнопки действий */ +.btn-icon { + background: none; + border: none; + font-size: 1rem; + cursor: pointer; + padding: 0.3rem; + border-radius: 4px; + transition: all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94); + opacity: 0.7; +} + +.btn-icon:hover { + opacity: 1; + background: rgba(52, 152, 219, 0.1); + transform: scale(1.1); +} + +.btn-icon.disabled { + opacity: 0.4; + cursor: not-allowed; + pointer-events: none; +} + +.btn-icon.disabled:hover { + opacity: 0.4; + background: none; + transform: none; +} + +.btn-icon.warning { + color: #f39c12; +} + +.btn-icon.warning:hover { + background: rgba(243, 156, 18, 0.1); +} + +/* Кнопки - новый дизайн */ +.btn-primary { + background: linear-gradient(135deg, #3498db, #2980b9); + color: white; + border: none; + padding: 0.4rem 1rem; + border-radius: 8px; + font-weight: 600; + font-size: 0.75rem; + cursor: pointer; + transition: all 0.2s ease; + display: inline-flex; + align-items: center; + position: relative; + overflow: hidden; + box-shadow: 0 2px 8px rgba(52, 152, 219, 0.25); + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + white-space: nowrap; +} + +.btn-primary::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); + transition: left 0.6s ease; +} + +.btn-primary:hover { + background: linear-gradient(135deg, #2980b9, #3498db); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3); +} + +.btn-primary:hover::before { + left: 100%; +} + +.btn-primary:active { + transform: translateY(0); + box-shadow: 0 2px 8px rgba(52, 152, 219, 0.25); +} + +/* Иконка плюса */ +.btn-primary .btn-plus { + background: rgba(255, 255, 255, 0.25); + border-radius: 50%; + width: 14px; + height: 14px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 10px; + font-weight: 700; + margin-right: 0.5rem; + flex-shrink: 0; +} + +/* Специальные стили для кнопки добавления сайта */ +#add-site-btn { + background: linear-gradient(135deg, #2ecc71, #27ae60); + box-shadow: 0 2px 8px rgba(46, 204, 113, 0.25); +} + +#add-site-btn:hover { + background: linear-gradient(135deg, #27ae60, #2ecc71); + box-shadow: 0 4px 12px rgba(46, 204, 113, 0.3); +} + +#add-site-btn:active { + box-shadow: 0 2px 8px rgba(46, 204, 113, 0.25); +} + +/* Специальные стили для кнопки добавления SSL */ +#add-cert-btn { + background: linear-gradient(135deg, #f39c12, #e67e22); + box-shadow: 0 2px 8px rgba(243, 156, 18, 0.25); +} + +#add-cert-btn:hover { + background: linear-gradient(135deg, #e67e22, #f39c12); + box-shadow: 0 4px 12px rgba(243, 156, 18, 0.3); +} + +#add-cert-btn:active { + box-shadow: 0 2px 8px rgba(243, 156, 18, 0.25); +} + +/* Футер карточек */ +.card-footer { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid rgba(52, 152, 219, 0.1); + text-align: center; +} + +.btn-link { + background: none; + border: none; + color: #3498db; + font-size: 0.9rem; + cursor: pointer; + padding: 0.5rem; + border-radius: 6px; + transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); + font-weight: 500; +} + +.btn-link:hover { + background: rgba(52, 152, 219, 0.1); + color: #2ecc71; + transform: translateY(-1px); +} + +.show-all-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + width: 100%; +} + +/* Адаптивность дашборда */ +@media (max-width: 768px) { + .dashboard-grid { + grid-template-columns: 1fr; + gap: 1.5rem; + margin-bottom: 4rem; + } + + .card-header { + flex-direction: column; + gap: 1.5rem; + align-items: stretch; + padding: 1.5rem; + } + + .card-content { + padding: 1rem 1.5rem 1.5rem 1.5rem; + } + + .card-title { + font-size: 1.3rem; + } + + .servers-grid { + grid-template-columns: 1fr; + } + + .stats-row { + grid-template-columns: 1fr; + gap: 0.5rem; + } + + .site-item, .cert-item { + flex-direction: column; + align-items: flex-start; + gap: 0.8rem; + } + + .site-info, .cert-info { + order: 1; + width: 100%; + } + + .site-actions, .cert-actions { + order: 3; + align-self: flex-end; + } + + .cert-expires { + order: 2; + align-self: flex-start; + align-items: flex-start; + min-width: auto; + } +} + +@media (max-width: 480px) { + .stat-number { + font-size: 2rem; + } + + .card-title { + font-size: 1.1rem; + } + + .server-item { + padding: 0.8rem; + } +} \ No newline at end of file diff --git a/Backend/admin/html/assets/css/main.css b/Backend/admin/html/assets/css/main.css new file mode 100644 index 0000000..97cd3ce --- /dev/null +++ b/Backend/admin/html/assets/css/main.css @@ -0,0 +1,228 @@ +/* Основные стили админ-панели vServer */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #1a252f 0%, #2c3e50 50%, #34495e 100%); + min-height: 100vh; + color: #ffffff; + overflow-x: hidden; +} + +/* Фоновые частицы */ +.floating-particles { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: -1; +} + +.particle { + position: absolute; + background: rgba(52, 152, 219, 0.15); + border-radius: 50%; + animation: float 8s ease-in-out infinite; +} + +@keyframes float { + 0%, 100% { + transform: translateY(0px) rotate(0deg); + } + 50% { + transform: translateY(-20px) rotate(180deg); + } +} + +/* Основной контейнер */ +.admin-container { + display: flex; + min-height: 100vh; + position: relative; +} + +/* Основной контент */ +.admin-main { + flex: 1; + padding: 2rem; + margin-left: 280px; + transition: margin-left 0.3s ease; +} + + + +/* Заголовки секций */ +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; + padding-bottom: 1rem; + border-bottom: 1px solid rgba(52, 152, 219, 0.2); +} + +.section-title { + font-size: 2.5rem; + font-weight: 700; + background: linear-gradient(135deg, #3498db, #2ecc71); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Индикатор статуса */ +.status-indicator { + display: flex; + align-items: center; + gap: 10px; + background: linear-gradient(135deg, rgba(46, 204, 113, 0.15), rgba(39, 174, 96, 0.25)); + padding: 12px 20px; + border-radius: 8px; + border: 1px solid rgba(46, 204, 113, 0.4); +} + +.status-dot { + width: 12px; + height: 12px; + background: #2ecc71; + border-radius: 50%; + animation: pulse 2s infinite; + box-shadow: 0 0 8px rgba(46, 204, 113, 0.5); +} + +@keyframes pulse { + 0% { + box-shadow: 0 0 0 0 rgba(46, 204, 113, 0.7), 0 0 8px rgba(46, 204, 113, 0.5); + } + 70% { + box-shadow: 0 0 0 8px rgba(46, 204, 113, 0), 0 0 8px rgba(46, 204, 113, 0.5); + } + 100% { + box-shadow: 0 0 0 0 rgba(46, 204, 113, 0), 0 0 8px rgba(46, 204, 113, 0.5); + } +} + +.status-text { + color: #ecf0f1; + font-weight: 600; + font-size: 0.9rem; +} + +/* Кнопки */ +.btn-primary { + background: linear-gradient(135deg, #3498db, #2980b9); + color: white; + border: none; + padding: 14px 24px; + border-radius: 10px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 16px rgba(52, 152, 219, 0.25); + font-size: 0.95rem; + position: relative; + overflow: hidden; +} + +.btn-primary::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); + transition: left 0.5s ease; +} + +.btn-primary:hover::before { + left: 100%; +} + +.btn-primary:hover { + background: linear-gradient(135deg, #2980b9, #3498db); + transform: translateY(-3px); + box-shadow: 0 8px 25px rgba(52, 152, 219, 0.4); +} + +.btn-primary:active { + transform: translateY(-1px); + box-shadow: 0 4px 16px rgba(52, 152, 219, 0.3); +} + +.btn-primary span { + margin-right: 10px; + font-size: 1.2rem; + font-weight: 400; +} + + + +/* Копирайт */ +.admin-footer { + position: fixed; + bottom: 0; + left: 280px; + right: 0; + background: rgba(26, 37, 47, 0.8); + backdrop-filter: blur(10px); + border-top: 1px solid rgba(52, 152, 219, 0.2); + padding: 1rem 2rem; + z-index: 100; +} + +.footer-content { + font-size: 0.85rem; + color: #95a5a6; + text-align: center; +} + +.footer-content a { + color: #3498db; + text-decoration: none; + transition: color 0.3s ease; +} + +.footer-content a:hover { + color: #2ecc71; + text-decoration: underline; +} + +/* Адаптивность */ +@media (max-width: 1024px) { + .admin-main { + margin-left: 0; + padding: 1rem; + } + + .admin-footer { + left: 0; + } + + .section-header { + flex-direction: column; + gap: 1rem; + align-items: flex-start; + } + + .section-title { + font-size: 2rem; + } +} + +@media (max-width: 768px) { + .admin-main { + padding: 1rem 0.5rem; + } + + .section-title { + font-size: 1.8rem; + } +} \ No newline at end of file diff --git a/Backend/admin/html/assets/css/navigation.css b/Backend/admin/html/assets/css/navigation.css new file mode 100644 index 0000000..fd83d86 --- /dev/null +++ b/Backend/admin/html/assets/css/navigation.css @@ -0,0 +1,203 @@ +/* Стили навигации админ-панели */ + +.admin-nav { + position: fixed; + left: 0; + top: 0; + width: 280px; + height: 100vh; + background: linear-gradient(135deg, rgba(26, 37, 47, 0.95), rgba(20, 30, 40, 0.9)); + backdrop-filter: blur(20px); + border-right: 1px solid rgba(52, 152, 219, 0.3); + box-shadow: + 8px 0 32px rgba(0, 0, 0, 0.4), + 0 0 0 1px rgba(52, 152, 219, 0.1), + inset -1px 0 0 rgba(255, 255, 255, 0.1); + z-index: 1000; + display: flex; + flex-direction: column; + transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); +} + +/* Заголовок навигации */ +.nav-header { + padding: 2rem 1.5rem; + border-bottom: 1px solid rgba(52, 152, 219, 0.25); + background: linear-gradient(135deg, rgba(52, 152, 219, 0.08), rgba(46, 204, 113, 0.05)); + position: relative; + box-shadow: + 0 4px 15px rgba(0, 0, 0, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.1); +} + +.nav-header::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, #3498db, #2ecc71, #e74c3c, #f39c12); + background-size: 400% 400%; + animation: gradientShift 4s ease infinite; +} + +@keyframes gradientShift { + 0%, 100% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } +} + +.logo { + font-size: 2.2rem; + font-weight: 700; + background: linear-gradient(135deg, #3498db, #2ecc71); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + letter-spacing: 2px; + margin-bottom: 0.5rem; + filter: drop-shadow(0 2px 8px rgba(52, 152, 219, 0.4)); +} + +.logo-subtitle { + font-size: 0.9rem; + color: #95a5a6; + font-weight: 400; + text-transform: uppercase; + letter-spacing: 1px; +} + +/* Меню навигации */ +.nav-menu { + list-style: none; + padding: 1rem 0; + flex: 1; +} + +.nav-item { + margin-bottom: 0.5rem; +} + +.nav-link { + display: flex; + align-items: center; + padding: 1rem 1.5rem; + color: #bdc3c7; + text-decoration: none; + transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); + position: relative; + border-radius: 0 25px 25px 0; + margin-right: 1rem; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); +} + +.nav-link::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 4px; + background: transparent; + transition: all 0.3s ease; +} + +.nav-link:hover { + background: linear-gradient(135deg, rgba(52, 152, 219, 0.12), rgba(46, 204, 113, 0.06)); + color: #3498db; + transform: translateX(0); + margin-right: 0.5rem; + border-radius: 0 12px 12px 0; + box-shadow: inset 2px 0 8px rgba(52, 152, 219, 0.15); +} + +.nav-link:hover::before { + background: linear-gradient(135deg, #3498db, #2ecc71); +} + +.nav-item.active .nav-link { + background: linear-gradient(135deg, rgba(52, 152, 219, 0.2), rgba(46, 204, 113, 0.1)); + color: #3498db; + font-weight: 600; + transform: translateX(0); + margin-right: 0; + border-radius: 0; + box-shadow: inset 3px 0 12px rgba(52, 152, 219, 0.25); +} + +.nav-item.active .nav-link::before { + background: linear-gradient(135deg, #3498db, #2ecc71); +} + +.nav-icon { + font-size: 1.3rem; + margin-right: 1rem; + width: 24px; + text-align: center; + transition: transform 0.3s ease; +} + +.nav-link:hover .nav-icon { + transform: scale(1.1); +} + +.nav-text { + font-size: 1rem; + transition: all 0.3s ease; +} + +/* Мобильная адаптивность */ +@media (max-width: 1024px) { + .admin-nav { + transform: translateX(-100%); + box-shadow: 2px 0 10px rgba(0, 0, 0, 0.3); + } + + .admin-nav.open { + transform: translateX(0); + } + + /* Кнопка меню (будет добавлена в JS) */ + .nav-toggle { + position: fixed; + top: 1rem; + left: 1rem; + z-index: 1001; + background: rgba(26, 37, 47, 0.9); + border: 1px solid rgba(52, 152, 219, 0.3); + color: #3498db; + padding: 12px; + border-radius: 8px; + cursor: pointer; + backdrop-filter: blur(10px); + font-size: 1.2rem; + transition: all 0.3s ease; + } + + .nav-toggle:hover { + background: rgba(52, 152, 219, 0.2); + transform: scale(1.05); + } +} + +@media (max-width: 768px) { + .admin-nav { + width: 100%; + } + + .nav-link { + padding: 1.2rem 1.5rem; + margin-right: 0; + border-radius: 0; + } + + .nav-icon { + font-size: 1.4rem; + } + + .nav-text { + font-size: 1.1rem; + } +} + +/* Анимация появления меню - убрано для избежания проблем с ресайзом */ \ No newline at end of file diff --git a/Backend/admin/html/assets/css/system-metrics.css b/Backend/admin/html/assets/css/system-metrics.css new file mode 100644 index 0000000..565c52b --- /dev/null +++ b/Backend/admin/html/assets/css/system-metrics.css @@ -0,0 +1,440 @@ +/* Стили системной панели метрик */ + +.system-panel { + background: linear-gradient(135deg, rgba(26, 37, 47, 0.95), rgba(20, 30, 40, 0.9)); + backdrop-filter: blur(20px); + border-radius: 16px; + border: 1px solid rgba(52, 152, 219, 0.3); + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.4), + 0 0 0 1px rgba(52, 152, 219, 0.1), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + margin-bottom: 2rem; + overflow: hidden; + position: relative; +} + +.system-panel::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, #3498db, #2ecc71, #f39c12, #e74c3c, #9b59b6); + background-size: 400% 400%; + animation: gradientFlow 8s ease-in-out infinite; + box-shadow: 0 3px 15px rgba(52, 152, 219, 0.3); +} + +@keyframes gradientFlow { + 0%, 100% { + background-position: 0% 50%; + } + 25% { + background-position: 100% 50%; + } + 50% { + background-position: 200% 50%; + } + 75% { + background-position: 300% 50%; + } +} + +/* Заголовок панели */ +.system-panel-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 1.5rem; + border-bottom: 1px solid rgba(52, 152, 219, 0.15); + background: linear-gradient(135deg, rgba(52, 152, 219, 0.05), rgba(46, 204, 113, 0.03)); +} + +.system-title { + display: flex; + align-items: center; + font-size: 1.4rem; + font-weight: 700; + color: #ecf0f1; + margin: 0; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); +} + +.system-icon { + font-size: 1.5rem; + margin-right: 0.8rem; + filter: drop-shadow(0 0 8px rgba(52, 152, 219, 0.4)); +} + +/* Uptime информация */ +.system-uptime { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.uptime-label { + color: #95a5a6; + font-size: 0.7rem; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); +} + +.uptime-value { + color: #2ecc71; + font-size: 0.9rem; + font-weight: 700; + background: linear-gradient(135deg, #2ecc71, #27ae60); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); + font-family: 'Courier New', monospace; + letter-spacing: 0.5px; +} + +/* Сетка метрик */ +.metrics-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 1rem; + padding: 1rem 1.5rem; +} + +/* Карточки метрик */ +.metric-card { + display: flex; + align-items: center; + background: linear-gradient(135deg, + rgba(52, 152, 219, 0.08), + rgba(52, 152, 219, 0.03)); + border: 1px solid rgba(52, 152, 219, 0.15); + border-radius: 12px; + padding: 1rem; + transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); + position: relative; + overflow: hidden; + box-shadow: + 0 4px 15px rgba(0, 0, 0, 0.1), + inset 0 1px 0 rgba(255, 255, 255, 0.1); +} + +.metric-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 4px; + height: 100%; + background: linear-gradient(135deg, #3498db, #2ecc71); + transition: all 0.4s ease; + box-shadow: 0 0 10px rgba(52, 152, 219, 0.4); +} + +.metric-card::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(135deg, + rgba(255, 255, 255, 0.02), + transparent, + rgba(52, 152, 219, 0.02)); + pointer-events: none; + transition: all 0.4s ease; +} + +.metric-card:hover { + background: linear-gradient(135deg, + rgba(52, 152, 219, 0.12), + rgba(52, 152, 219, 0.06)); + border-color: rgba(52, 152, 219, 0.25); + transform: translateY(-3px) scale(1.02); + box-shadow: + 0 8px 25px rgba(0, 0, 0, 0.2), + 0 0 20px rgba(52, 152, 219, 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.15); +} + +.metric-card:hover::before { + width: 6px; + box-shadow: 0 0 15px rgba(52, 152, 219, 0.6); +} + +.metric-card:hover::after { + background: linear-gradient(135deg, + rgba(255, 255, 255, 0.05), + transparent, + rgba(52, 152, 219, 0.05)); +} + +/* Специальные цвета для разных метрик */ +.metric-card.cpu::before { + background: linear-gradient(135deg, #3498db, #2980b9); + box-shadow: 0 0 10px rgba(52, 152, 219, 0.4); +} + +.metric-card.cpu:hover::before { + box-shadow: 0 0 15px rgba(52, 152, 219, 0.6); +} + +.metric-card.ram::before { + background: linear-gradient(135deg, #e74c3c, #c0392b); + box-shadow: 0 0 10px rgba(231, 76, 60, 0.4); +} + +.metric-card.ram:hover::before { + box-shadow: 0 0 15px rgba(231, 76, 60, 0.6); +} + +.metric-card.disk::before { + background: linear-gradient(135deg, #9b59b6, #8e44ad); + box-shadow: 0 0 10px rgba(155, 89, 182, 0.4); +} + +.metric-card.disk:hover::before { + box-shadow: 0 0 15px rgba(155, 89, 182, 0.6); +} + +/* Иконки метрик */ +.metric-icon-wrapper { + display: flex; + align-items: center; + justify-content: center; + width: 45px; + height: 45px; + background: linear-gradient(135deg, rgba(52, 152, 219, 0.15), rgba(52, 152, 219, 0.08)); + border-radius: 10px; + margin-right: 0.8rem; + flex-shrink: 0; + transition: all 0.4s ease; + box-shadow: + 0 3px 10px rgba(0, 0, 0, 0.1), + inset 0 1px 0 rgba(255, 255, 255, 0.1); +} + +.metric-card:hover .metric-icon-wrapper { + background: linear-gradient(135deg, rgba(52, 152, 219, 0.2), rgba(52, 152, 219, 0.12)); + transform: scale(1.1) rotate(5deg); + box-shadow: + 0 5px 15px rgba(0, 0, 0, 0.15), + 0 0 20px rgba(52, 152, 219, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.15); +} + +.metric-icon { + font-size: 1.3rem; + filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.3)); +} + +/* Содержимое метрик */ +.metric-content { + flex: 1; + min-width: 0; +} + +.metric-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.metric-name { + color: #ecf0f1; + font-weight: 600; + font-size: 0.85rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); +} + +.metric-value { + font-weight: 700; + font-size: 1rem; + background: linear-gradient(135deg, #3498db, #2ecc71); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + flex-shrink: 0; + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3)); +} + +/* Специальные цвета значений */ +.metric-card.cpu .metric-value { + background: linear-gradient(135deg, #3498db, #2980b9); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.metric-card.ram .metric-value { + background: linear-gradient(135deg, #e74c3c, #c0392b); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.metric-card.disk .metric-value { + background: linear-gradient(135deg, #9b59b6, #8e44ad); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Прогресс-бары */ +.metric-progress-bar { + width: 100%; + height: 5px; + background: linear-gradient(135deg, rgba(52, 152, 219, 0.1), rgba(52, 152, 219, 0.05)); + border-radius: 3px; + overflow: hidden; + margin-bottom: 0.5rem; + position: relative; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2); +} + +.metric-progress { + height: 100%; + border-radius: 3px; + position: relative; + overflow: hidden; + box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.2), + 0 0 10px rgba(52, 152, 219, 0.3); + transition: all 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94); +} + +.metric-progress::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); + animation: shimmer 4s ease-in-out infinite; +} + +@keyframes shimmer { + 0% { + left: -100%; + opacity: 0; + } + 50% { + opacity: 1; + } + 100% { + left: 100%; + opacity: 0; + } +} + +/* Цвета прогресс-баров */ +.cpu-progress { + background: linear-gradient(135deg, #3498db, #2980b9); + box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.2), + 0 0 10px rgba(52, 152, 219, 0.4); +} + +.ram-progress { + background: linear-gradient(135deg, #e74c3c, #c0392b); + box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.2), + 0 0 10px rgba(231, 76, 60, 0.4); +} + +.disk-progress { + background: linear-gradient(135deg, #9b59b6, #8e44ad); + box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.2), + 0 0 10px rgba(155, 89, 182, 0.4); +} + +/* Детали метрик */ +.metric-details { + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.5rem; +} + +.metric-info { + color: #bdc3c7; + font-size: 0.7rem; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); +} + +.metric-frequency, +.metric-type, +.metric-range, +.metric-speed { + color: #95a5a6; + font-size: 0.65rem; + flex-shrink: 0; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); +} + +/* Адаптивность */ +@media (max-width: 1200px) { + .metrics-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 768px) { + .metrics-grid { + grid-template-columns: 1fr; + gap: 1rem; + padding: 1rem 1.5rem 1.5rem 1.5rem; + } + + .system-panel-header { + flex-direction: column; + gap: 1rem; + align-items: stretch; + padding: 1.5rem; + } + + .metric-card { + flex-direction: column; + text-align: center; + padding: 1rem; + } + + .metric-icon-wrapper { + margin-right: 0; + margin-bottom: 0.8rem; + } + + .metric-details { + flex-direction: column; + gap: 0.3rem; + } +} + +@media (max-width: 480px) { + .system-title { + font-size: 1.1rem; + } + + .metric-name { + font-size: 0.8rem; + } + + .metric-value { + font-size: 1rem; + } +} \ No newline at end of file diff --git a/Backend/admin/html/assets/js/dashboard.js b/Backend/admin/html/assets/js/dashboard.js new file mode 100644 index 0000000..721296c --- /dev/null +++ b/Backend/admin/html/assets/js/dashboard.js @@ -0,0 +1,11 @@ +/** + * Дашборд админ-панели vServer + */ + +document.addEventListener('DOMContentLoaded', function() { + // Каждая кнопка отдельно с возможностью настройки + messageUp.send('#add-site-btn', '🌐 Страница добавления нового сайта в разработке', 'warning'); + messageUp.send('#add-cert-btn', '🔒 Страница добавления SSL-сертификата в разработке', 'warning'); + messageUp.send('.btn-icon', '⚙️ Функция настроек в разработке', 'warning'); + messageUp.send('.show-all-btn', '📋 Полный список в разработке', 'warning'); +}); diff --git a/Backend/admin/html/assets/js/json_loader.js b/Backend/admin/html/assets/js/json_loader.js new file mode 100644 index 0000000..546b13c --- /dev/null +++ b/Backend/admin/html/assets/js/json_loader.js @@ -0,0 +1,90 @@ +// Универсальный класс для загрузки JSON данных +class JSONLoader { + constructor(config) { + this.url = config.url; + this.container = config.container; + this.requiredFields = config.requiredFields || []; + this.displayTemplate = config.displayTemplate; + this.errorMessage = config.errorMessage || 'Ошибка загрузки данных'; + } + + // Загрузка данных + async load() { + try { + const response = await fetch(this.url); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + const data = await response.json(); + + // Проверяем структуру данных + if (!this.validateData(data)) { + throw new Error('Неверная структура данных'); + } + + this.display(data); + } catch (error) { + console.error('Ошибка загрузки:', error); + this.displayError(); + } + } + + // Простая проверка обязательных полей + validateData(data) { + if (!Array.isArray(data)) { + return false; + } + + for (let item of data) { + for (let field of this.requiredFields) { + if (!item.hasOwnProperty(field)) { + return false; + } + } + } + + return true; + } + + // Отображение данных по шаблону + display(data) { + const container = document.querySelector(this.container); + container.innerHTML = ''; + + data.forEach(item => { + let html; + + // Если шаблон - функция, вызываем её + if (typeof this.displayTemplate === 'function') { + html = this.displayTemplate(item); + } else { + // Иначе используем строковый шаблон с заменой + html = this.displayTemplate; + for (let key in item) { + const value = item[key]; + html = html.replace(new RegExp(`{{${key}}}`, 'g'), value); + } + } + + container.innerHTML += html; + }); + } + + // Отображение ошибки + displayError() { + const container = document.querySelector(this.container); + if (container) { + container.innerHTML = ` +
+ ⚠️ ${this.errorMessage} +
+ `; + } + } + + // Перезагрузка данных + reload() { + this.load(); + } +} \ No newline at end of file diff --git a/Backend/admin/html/assets/js/menu_loader.js b/Backend/admin/html/assets/js/menu_loader.js new file mode 100644 index 0000000..1ae81b1 --- /dev/null +++ b/Backend/admin/html/assets/js/menu_loader.js @@ -0,0 +1,27 @@ +// Функция для создания HTML пункта меню +function getMenuTemplate(item) { + const isActive = item.active ? 'active' : ''; + + return ` + + `; +} + +// Создаем загрузчик меню +const menuLoader = new JSONLoader({ + url: '/json/menu.json', + container: '.nav-menu', + requiredFields: ['name', 'icon', 'url', 'active'], + displayTemplate: getMenuTemplate, + errorMessage: 'Ошибка загрузки меню' +}); + +// Запуск при загрузке страницы +document.addEventListener('DOMContentLoaded', function() { + menuLoader.load(); +}); \ No newline at end of file diff --git a/Backend/admin/html/assets/js/metrics_loader.js b/Backend/admin/html/assets/js/metrics_loader.js new file mode 100644 index 0000000..991e986 --- /dev/null +++ b/Backend/admin/html/assets/js/metrics_loader.js @@ -0,0 +1,153 @@ +// Функция для создания HTML карточки метрики +function createMetricCard(type, icon, name, data) { + // Используем уже вычисленный процент для всех типов + let value = Math.round(data.usage || data.usage_percent || 0); + const progressClass = type === 'cpu' ? 'cpu-progress' : + type === 'ram' ? 'ram-progress' : 'disk-progress'; + + // Определяем детали для каждого типа + let details = ''; + if (type === 'cpu') { + details = ` + ${data.model_name || 'CPU'} + ${data.frequency || ''} MHz + `; + } else if (type === 'ram') { + const usedGb = parseFloat(data.used_gb) || 0; + details = ` + ${usedGb.toFixed(1)} GB из ${data.total_gb || 0} GB + ${data.type || 'RAM'} + `; + } else if (type === 'disk') { + const usedGb = parseFloat(data.used_gb) || 0; + const freeGb = parseFloat(data.free_gb) || 0; + details = ` + Занято: ${usedGb.toFixed(0)} GB : Свободно: ${freeGb.toFixed(0)} GB + Размер: ${data.total_gb || 0} + `; + } + + return ` +
+
+ ${icon} +
+
+
+ ${name} + ${value}% +
+
+
+
+
+ ${details} +
+
+
+ `; +} + +// Функция для создания всех метрик +function renderMetrics(data) { + const container = document.querySelector('.metrics-grid'); + if (!container) return; + + const html = [ + createMetricCard('cpu', '🖥️', 'Процессор', data.cpu || {}), + createMetricCard('ram', '💾', 'Оперативная память', data.memory || {}), + createMetricCard('disk', '💿', data.disk.type, data.disk || {}) + ].join(''); + + container.innerHTML = html; +} + +// Данные по умолчанию (будут заменены данными из API) +const staticData = { + cpu: { + usage: 0, + model_name: 'Загрузка...', + frequency: '0', + cores: '0' + }, + memory: { + usage_percent: 0, + used_gb: 0, + total_gb: 0, + type: 'Загрузка...' + }, + disk: { + usage_percent: 0, + used_gb: 0, + free_gb: 0, + total_gb: '0', + type: 'Загрузка...', + read_speed: '520 MB/s' + } +}; + + + + +// Функция обновления метрик +async function updateMetrics() { + try { + const response = await fetch('/api/metrics'); + const data = await response.json(); + + // Обновляем все данные из API + if (data.cpu_name) staticData.cpu.model_name = data.cpu_name; + if (data.cpu_ghz) staticData.cpu.frequency = data.cpu_ghz; + if (data.cpu_cores) staticData.cpu.cores = data.cpu_cores; + if (data.cpu_usage) staticData.cpu.usage = parseInt(data.cpu_usage); + + if (data.disk_name) staticData.disk.type = data.disk_name; + if (data.disk_size) staticData.disk.total_gb = data.disk_size; + if (data.disk_used) staticData.disk.used_gb = parseFloat(data.disk_used); + if (data.disk_free) staticData.disk.free_gb = parseFloat(data.disk_free); + + if (data.ram_using) staticData.memory.used_gb = parseFloat(data.ram_using); + if (data.ram_total) staticData.memory.total_gb = parseFloat(data.ram_total); + + // Вычисляем процент использования памяти + if (staticData.memory.used_gb && staticData.memory.total_gb) { + staticData.memory.usage_percent = Math.round((staticData.memory.used_gb / staticData.memory.total_gb) * 100); + } + + // Вычисляем процент использования диска + if (staticData.disk.used_gb && staticData.disk.total_gb) { + const used = parseFloat(staticData.disk.used_gb.toString().replace(' GB', '')) || 0; + const total = parseFloat(staticData.disk.total_gb.toString().replace(' GB', '')) || 1; + staticData.disk.usage_percent = Math.round((used / total) * 100); + } + + // Обновляем uptime + if (data.server_uptime) { + const uptimeElement = document.querySelector('.uptime-value'); + if (uptimeElement) { + uptimeElement.textContent = data.server_uptime; + } + } + + // Перерисовываем только метрики + renderMetrics(staticData); + } catch (error) { + console.error('Ошибка обновления метрик:', error); + } + + +} + +// Показываем статические данные когда DOM загружен +document.addEventListener('DOMContentLoaded', function() { + renderMetrics(staticData); + + // Сразу загружаем актуальные данные + updateMetrics(); + + // Обновляем метрики каждые 5 секунд + setInterval(updateMetrics, 5000); +}); + + + diff --git a/Backend/admin/html/assets/js/server_status.js b/Backend/admin/html/assets/js/server_status.js new file mode 100644 index 0000000..36e1473 --- /dev/null +++ b/Backend/admin/html/assets/js/server_status.js @@ -0,0 +1,180 @@ +// Функция для получения HTML шаблона сервера +var patch_json = '/json/server_status.json'; + +function getServerTemplate(server) { + // Определяем класс и текст статуса + let statusClass, statusText; + switch(server.Status.toLowerCase()) { + case 'running': + statusClass = 'running'; + statusText = 'Работает'; + break; + case 'stopped': + statusClass = 'stopped'; + statusText = 'Остановлен'; + break; + case 'starting': + statusClass = 'starting'; + statusText = 'Запускается'; + break; + case 'stopping': + statusClass = 'stopping'; + statusText = 'Завершается'; + break; + default: + statusClass = 'stopped'; + statusText = `Неизвестно (${server.Status})`; + } + + // Определяем иконку и состояние кнопки + let buttonIcon, buttonDisabled, buttonClass; + + if (statusClass === 'starting' || statusClass === 'stopping') { + buttonIcon = '⏳'; + buttonDisabled = 'disabled'; + buttonClass = 'btn-icon disabled'; + } else if (statusClass === 'running') { + buttonIcon = '⏹️'; + buttonDisabled = ''; + buttonClass = 'btn-icon'; + } else { + buttonIcon = '▶️'; + buttonDisabled = ''; + buttonClass = 'btn-icon'; + } + + return ` +
+
+
+
${server.NameService}
+
Port ${server.Port} - ${statusText}
+
+
+ +
+
+ `; +} + +// Создаем загрузчик серверов +const serversLoader = new JSONLoader({ + url: patch_json, + container: '.servers-grid', + requiredFields: ['NameService', 'Port', 'Status'], + displayTemplate: getServerTemplate, + errorMessage: 'Ошибка загрузки статуса серверов' +}); + +// Функция для показа временного статуса +function showTempStatus(serviceName, tempStatus) { + // Ищем элемент этого сервера на странице + const serverElements = document.querySelectorAll('.server-item'); + serverElements.forEach(element => { + const nameElement = element.querySelector('.server-name'); + if (nameElement && nameElement.textContent === serviceName) { + // Создаем временный объект сервера с новым статусом + fetch(patch_json) + .then(response => response.json()) + .then(servers => { + const server = servers.find(s => s.NameService === serviceName); + if (server) { + const tempServer = {...server, Status: tempStatus}; + // Заменяем только этот элемент + element.outerHTML = getServerTemplate(tempServer); + } + }); + } + }); +} + +// Функция обновления одного сервера +function updateSingleServer(serviceName) { + fetch(patch_json) + .then(response => response.json()) + .then(servers => { + const server = servers.find(s => s.NameService === serviceName); + if (server) { + // Ищем элемент этого сервера на странице + const serverElements = document.querySelectorAll('.server-item'); + serverElements.forEach(element => { + const nameElement = element.querySelector('.server-name'); + if (nameElement && nameElement.textContent === serviceName) { + // Заменяем только этот элемент + element.outerHTML = getServerTemplate(server); + } + }); + } + }); +} + +// Универсальная функция управления сервером +function serverAction(serviceName, startEndpoint, stopEndpoint, updateDelayMs) { + // Получаем текущий статус сервера из JSON + fetch(patch_json) + .then(response => response.json()) + .then(servers => { + const server = servers.find(s => s.NameService === serviceName); + + // Блокируем действие если сервер в процессе изменения + if (server.Status === 'starting' || server.Status === 'stopping') { + return; + } + + if (server.Status === 'running') { + // Сервер запущен - останавливаем + showTempStatus(serviceName, 'stopping'); + fetch(stopEndpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }).then(() => { + setTimeout(() => { + updateSingleServer(serviceName); // Обновляем только этот сервер + }, updateDelayMs); + }); + } else { + // Сервер остановлен - запускаем + showTempStatus(serviceName, 'starting'); + fetch(startEndpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }).then(() => { + setTimeout(() => { + updateSingleServer(serviceName); // Обновляем только этот сервер + }, updateDelayMs); + }); + } + }); +} + +// Функция для переключения сервера +function toggleServer(serviceName) { + + if (serviceName === 'MySQL Server') { + serverAction('MySQL Server', '/service/MySql_Start', '/service/MySql_Stop', 2000); + } + + if (serviceName === 'HTTP Server') { + serverAction('HTTP Server', '/service/Http_Start', '/service/Http_Stop', 2000); + } + + if (serviceName === 'HTTPS Server') { + serverAction('HTTPS Server', '/service/Https_Start', '/service/Https_Stop', 2000); + } + + if (serviceName === 'PHP Server') { + serverAction('PHP Server', '/service/Php_Start', '/service/Php_Stop', 2000); + } + +} + +// Запуск при загрузке страницы +document.addEventListener('DOMContentLoaded', function() { + serversLoader.load(); +}); diff --git a/Backend/admin/html/assets/js/site_list.js b/Backend/admin/html/assets/js/site_list.js new file mode 100644 index 0000000..3d11b3b --- /dev/null +++ b/Backend/admin/html/assets/js/site_list.js @@ -0,0 +1,55 @@ +document.addEventListener('DOMContentLoaded', function() { + const sitesList = document.querySelector('.sites-list'); + if (sitesList) { + fetch('/service/Site_List') + .then(r => r.json()) + .then(data => { + const sites = data.sites || []; + + // Генерируем статистику + updateSiteStats(sites); + + // Отображаем список сайтов + sitesList.innerHTML = sites.map(site => ` +
+
+
+ ${site.host} + ${site.type.toUpperCase()} • Протокол +
+
+ +
+
+ `).join(''); + }); + } +}); + +function updateSiteStats(sites) { + const totalSites = sites.length; + const activeSites = sites.filter(site => site.status === 'active').length; + const inactiveSites = totalSites - activeSites; + + // Находим контейнер статистики + const statsRow = document.querySelector('.stats-row'); + + // Создаём всю статистику через JavaScript + statsRow.innerHTML = ` +
+
${totalSites}
+
Всего сайтов
+
+
+
${activeSites}
+
Активных
+
+
+
${inactiveSites}
+
Неактивных
+
+ `; +} + + + diff --git a/Backend/admin/html/assets/js/tools.js b/Backend/admin/html/assets/js/tools.js new file mode 100644 index 0000000..4f67518 --- /dev/null +++ b/Backend/admin/html/assets/js/tools.js @@ -0,0 +1,201 @@ +/* Класс для показа уведомлений */ +class MessageUp { + // Время автозакрытия уведомлений (в миллисекундах) + static autoCloseTime = 3000; + + // Типы уведомлений (легко редактировать тут) + static TYPES = { + info: { + borderColor: 'rgb(52, 152, 219)', + background: 'linear-gradient(135deg, rgb(30, 50, 70), rgb(35, 55, 75))' + }, + success: { + borderColor: 'rgb(46, 204, 113)', + background: 'linear-gradient(135deg, rgb(25, 55, 35), rgb(30, 60, 40))' + }, + warning: { + borderColor: 'rgb(243, 156, 18)', + background: 'linear-gradient(135deg, rgb(60, 45, 20), rgb(65, 50, 25))' + }, + error: { + borderColor: 'rgb(231, 76, 60)', + background: 'linear-gradient(135deg, rgb(60, 25, 25), rgb(65, 30, 30))' + } + }; + + constructor() { + this.addStyles(); + } + + /* Показать уведомление */ + show(message, type = 'info') { + const notification = document.createElement('div'); + notification.className = `message-up message-up-${type}`; + + notification.innerHTML = ` +
+ ${message} +
+ `; + + // Вычисляем позицию для нового уведомления + const existingNotifications = document.querySelectorAll('.message-up'); + let topPosition = 2; // начальная позиция в rem + + existingNotifications.forEach(existing => { + const rect = existing.getBoundingClientRect(); + const currentTop = parseFloat(existing.style.top) || 2; + const height = rect.height / 16; // переводим px в rem (примерно) + topPosition = Math.max(topPosition, currentTop + height + 1); // добавляем отступ + }); + + notification.style.top = `${topPosition}rem`; + document.body.appendChild(notification); + + // Показываем с анимацией + setTimeout(() => { + notification.classList.add('message-up-show'); + }, 10); + + // Автоматическое закрытие и удаление + if (MessageUp.autoCloseTime > 0) { + setTimeout(() => { + if (notification && notification.parentNode) { + notification.classList.remove('message-up-show'); + notification.classList.add('message-up-hide'); + + setTimeout(() => { + if (notification.parentNode) { + notification.remove(); + // Пересчитываем позиции оставшихся уведомлений + this.repositionNotifications(); + } + }, 300); + } + }, MessageUp.autoCloseTime); + } + } + + /* Пересчитать позиции всех уведомлений */ + repositionNotifications() { + const notifications = document.querySelectorAll('.message-up'); + let currentTop = 2; // начальная позиция + + notifications.forEach(notification => { + notification.style.transition = 'all 0.3s ease'; + notification.style.top = `${currentTop}rem`; + + const rect = notification.getBoundingClientRect(); + const height = rect.height / 16; // переводим px в rem + currentTop += height + 1; // добавляем отступ + }); + } + + /* Показать сообщение напрямую или привязать к элементам */ + send(messageOrSelector, typeOrMessage = 'info', type = 'info') { + // Если первый параметр строка и нет других элементов на странице с таким селектором + // то показываем сообщение напрямую + if (typeof messageOrSelector === 'string' && + document.querySelectorAll(messageOrSelector).length === 0) { + // Показываем сообщение напрямую + this.show(messageOrSelector, typeOrMessage); + return; + } + + // Иначе привязываем к элементам (старый способ) + this.bindToElements(messageOrSelector, typeOrMessage, type); + } + + /* Привязать уведомления к элементам */ + bindToElements(selector, message = 'Страница в разработке', type = 'info') { + document.querySelectorAll(selector).forEach(element => { + element.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + + // Вызываем нужный тип уведомления + window.messageUp.show(message, type); + }); + }); + } + + /* Добавить стили для уведомлений */ + addStyles() { + if (document.querySelector('#message-up-styles')) return; + + const cfg = MessageUp.TYPES; + + // Генерируем стили для типов уведомлений + const typeStyles = Object.entries(cfg).map(([type, style]) => ` + .message-up-${type} { + border-color: ${style.borderColor}; + background: ${style.background}; + } + `).join(''); + + const style = document.createElement('style'); + style.id = 'message-up-styles'; + style.textContent = ` + .message-up { + position: fixed; + top: 2rem; + right: 2rem; + min-width: 300px; + max-width: 400px; + background: rgb(26, 37, 47); + backdrop-filter: blur(15px); + border-radius: 12px; + padding: 1rem; + color: #ecf0f1; + font-size: 0.9rem; + border: 2px solid; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + z-index: 10000; + opacity: 0; + transform: translateX(100%); + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + font-family: 'Segoe UI', system-ui, sans-serif; + } + + .message-up-show { + opacity: 1; + transform: translateX(0); + } + + .message-up-hide { + opacity: 0; + transform: translateX(100%); + } + + ${typeStyles} + + .message-content { + display: flex; + align-items: center; + gap: 1rem; + } + + .message-text { + flex: 1; + line-height: 1.4; + font-weight: 500; + } + + @media (max-width: 768px) { + .message-up { + top: 1rem; + right: 1rem; + left: 1rem; + min-width: auto; + max-width: none; + } + } + `; + + document.head.appendChild(style); + } + +} + +// Создаем глобальный экземпляр +window.messageUp = new MessageUp(); \ No newline at end of file diff --git a/Backend/admin/html/index.html b/Backend/admin/html/index.html new file mode 100644 index 0000000..adf0093 --- /dev/null +++ b/Backend/admin/html/index.html @@ -0,0 +1,225 @@ + + + + + + vServer - Админ панель + + + + + + + + + +
+
+
+
+
+
+
+
+ + +
+ + + + +
+ +
+

Панель Управления

+
+
+ Сервер работает +
+
+ + +
+
+

+ + Системные ресурсы +

+
+
Время работы
+
+
+
+ +
+ +
+
+ +
+ + +
+ +
+

+ 🖥️ + Статус серверов +

+
+ +
+
+ +
+
+ +
+ + +
+
+

+ 🌐 + Сайты +

+ +
+
+
+ +
+ +
+
+ Последние сайты +
+
+ +
+ +
+
+
+ + +
+
+

+ 🔒 + Сертификаты +

+ +
+
+
+
+
5
+
Всего сертификатов
+
+
+
3
+
Действующих
+
+
+
2
+
Истекающих
+
+
+ +
+
+ Состояние сертификатов +
+
+
+
+
+ *.voxsel.ru + Wildcard • Let's Encrypt +
+
+ До 30.12.2024 + 89 дней +
+
+ +
+
+
+
+
+ example.com + Standard • Cloudflare +
+
+ До 15.01.2024 + 21 день +
+
+ +
+
+
+
+
+ api.voxsel.ru + Standard • Let's Encrypt +
+
+ До 05.03.2024 + 72 дня +
+
+ +
+
+
+ +
+
+
+ +
+ +
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Backend/config/config.go b/Backend/config/config.go new file mode 100644 index 0000000..63d3ec8 --- /dev/null +++ b/Backend/config/config.go @@ -0,0 +1,53 @@ +package config + +import ( + "encoding/json" + "os" + tools "vServer/Backend/tools" +) + +var ConfigPath = "WebServer/config.json" + +var ConfigData struct { + Site_www []Site_www `json:"Site_www"` + Soft_Settings Soft_Settings `json:"Soft_Settings"` +} + +type Site_www struct { + Name string `json:"name"` + Host string `json:"host"` + Alias []string `json:"alias"` + Status string `json:"status"` + Root_file string `json:"root_file"` + Root_file_routing bool `json:"root_file_routing"` +} + +type Soft_Settings struct { + Php_port int `json:"php_port"` + Php_host string `json:"php_host"` + Mysql_port int `json:"mysql_port"` + Mysql_host string `json:"mysql_host"` + Admin_port string `json:"admin_port"` + Admin_host string `json:"admin_host"` +} + +func LoadConfig() { + + data, err := os.ReadFile(ConfigPath) + + if err != nil { + tools.Logs_file(0, "JSON", "Ошибка загрузки конфигурационного файла", "logs_config.log", true) + } else { + tools.Logs_file(0, "JSON", "config.json успешно загружен", "logs_config.log", true) + } + + err = json.Unmarshal(data, &ConfigData) + if err != nil { + tools.Logs_file(0, "JSON", "Ошибка парсинга конфигурационного файла", "logs_config.log", true) + } else { + tools.Logs_file(0, "JSON", "config.json успешно прочитан", "logs_config.log", true) + } + + println() + +} diff --git a/Backend/tools/AbsPatch.go b/Backend/tools/AbsPatch.go new file mode 100644 index 0000000..24ef47f --- /dev/null +++ b/Backend/tools/AbsPatch.go @@ -0,0 +1,25 @@ +package tools + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +// getAbsPath получает абсолютный путь с автоматической проверкой существования для файлов +func AbsPath(path string) (string, error) { + absPath, err := filepath.Abs(path) + if err != nil { + return "", fmt.Errorf("ошибка получения абсолютного пути: %v", err) + } + + // Проверяем существование только для файлов (с расширением) + if strings.Contains(filepath.Base(absPath), ".") { + if _, err := os.Stat(absPath); os.IsNotExist(err) { + return "", fmt.Errorf("файл не найден: %s", absPath) + } + } + + return absPath, nil +} diff --git a/Backend/tools/FunctionAll.go b/Backend/tools/FunctionAll.go new file mode 100644 index 0000000..f0e1200 --- /dev/null +++ b/Backend/tools/FunctionAll.go @@ -0,0 +1,79 @@ +package tools + +import ( + "fmt" + "net" + "time" +) + +// Время запуска сервера +var ServerStartTime time.Time + +// isPortInUse проверяет, занят ли указанный порт +func Port_check(service string, host string, port string) bool { + conn, err := net.DialTimeout("tcp", host+":"+port, time.Millisecond*300) + if err != nil { + return false // порт свободен + } + conn.Close() + Logs_file(1, service, "⚠️ Порт "+port+" уже занят, сервис не запущен", "logs_error.log", true) + return true // порт занят +} + +// Управление временем работы сервера +func ServerUptime(action string, asSeconds ...bool) interface{} { + switch action { + case "start": + // Инициализация времени запуска + ServerStartTime = time.Now() + return nil + + case "get": + // Получить время работы + if ServerStartTime.IsZero() { + if len(asSeconds) > 0 && asSeconds[0] { + return int64(0) + } + return "Сервер не запущен" + } + + uptime := time.Since(ServerStartTime) + + // Возвращаем секунды + if len(asSeconds) > 0 && asSeconds[0] { + return int64(uptime.Seconds()) + } + + // Возвращаем читаемый формат + days := int(uptime.Hours()) / 24 + hours := int(uptime.Hours()) % 24 + minutes := int(uptime.Minutes()) % 60 + seconds := int(uptime.Seconds()) % 60 + + if days > 0 { + return fmt.Sprintf("%dд %dч %dм", days, hours, minutes) + } else if hours > 0 { + return fmt.Sprintf("%dч %dм", hours, minutes) + } else if minutes > 0 { + return fmt.Sprintf("%dм", minutes) + } else { + return fmt.Sprintf("%dс", seconds) + } + + default: + return "Неизвестное действие" + } +} + +func Error_check(err error, message string) bool { + if err != nil { + fmt.Printf("Ошибка: %v\n", message) + return false + } + + return true +} + + + + diff --git a/Backend/tools/check_error.go b/Backend/tools/check_error.go new file mode 100644 index 0000000..0c6e0e3 --- /dev/null +++ b/Backend/tools/check_error.go @@ -0,0 +1,10 @@ +package tools + + +func CheckError(err error) error { + if err != nil { + return err + } else { + return nil + } +} \ No newline at end of file diff --git a/Backend/tools/cmd_go.go b/Backend/tools/cmd_go.go new file mode 100644 index 0000000..fd68cce --- /dev/null +++ b/Backend/tools/cmd_go.go @@ -0,0 +1,131 @@ +//go:build windows + +package tools + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "syscall" + "unsafe" +) + +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + procSetConsoleMode = kernel32.NewProc("SetConsoleMode") + procGetConsoleMode = kernel32.NewProc("GetConsoleMode") + procCreateMutex = kernel32.NewProc("CreateMutexW") + procCloseHandle = kernel32.NewProc("CloseHandle") +) + +const ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 + +var mutexHandle syscall.Handle + +func init() { + enableVirtualTerminal() + +} + +func enableVirtualTerminal() { + handle := os.Stdout.Fd() + var mode uint32 + + // Получаем текущий режим консоли + _, _, _ = procGetConsoleMode.Call(uintptr(handle), uintptr(unsafe.Pointer(&mode))) + + // Добавляем флаг поддержки ANSI + mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING + + // Устанавливаем новый режим + _, _, _ = procSetConsoleMode.Call(uintptr(handle), uintptr(mode)) +} + +func RunBatScript(script string) (string, error) { + // Создание временного файла + tmpFile, err := os.CreateTemp("", "script-*.bat") + if err != nil { + return "", fmt.Errorf("ошибка создания temp-файла: %w", err) + } + defer os.Remove(tmpFile.Name()) + + // Запись скрипта в файл + if _, err := tmpFile.WriteString(script); err != nil { + return "", fmt.Errorf("ошибка записи в temp-файл: %w", err) + } + tmpFile.Close() + + // Выполняем файл через cmd + cmd := exec.Command("cmd", "/C", tmpFile.Name()) + output, err := cmd.CombinedOutput() + + return string(output), err +} + +// Функция для логирования вывода процесса в консоль +func Logs_console(process *exec.Cmd, check bool) error { + + if check { + // Настраиваем pipes для захвата вывода + stdout, err := process.StdoutPipe() + CheckError(err) + stderr, err := process.StderrPipe() + CheckError(err) + + // Запускаем процесс + process.Start() + + // Захватываем stdout и stderr для вывода логов + go func() { + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + fmt.Println(scanner.Text()) + } + }() + + go func() { + scanner := bufio.NewScanner(stderr) + for scanner.Scan() { + fmt.Println(scanner.Text()) + } + }() + } else { + // Просто запускаем процесс без логирования + return process.Start() + } + + return nil +} + +// CheckSingleInstance проверяет, не запущена ли программа уже через мьютекс +func CheckSingleInstance() bool { + mutexName, _ := syscall.UTF16PtrFromString("Global\\vServer_SingleInstance") + + handle, _, err := procCreateMutex.Call( + 0, + 0, + uintptr(unsafe.Pointer(mutexName)), + ) + + if handle == 0 { + return false // не удалось создать мьютекс + } + + mutexHandle = syscall.Handle(handle) + + // Если GetLastError возвращает ERROR_ALREADY_EXISTS (183), значит мьютекс уже существует + if err.(syscall.Errno) == 183 { + return false // программа уже запущена + } + + return true // успешно создали мьютекс, программа не запущена +} + +// ReleaseMutex освобождает мьютекс при завершении программы +func ReleaseMutex() { + if mutexHandle != 0 { + procCloseHandle.Call(uintptr(mutexHandle)) + mutexHandle = 0 + } +} \ No newline at end of file diff --git a/Backend/tools/message.go b/Backend/tools/message.go new file mode 100644 index 0000000..1db6fc6 --- /dev/null +++ b/Backend/tools/message.go @@ -0,0 +1,88 @@ +package tools + +import ( + "fmt" + "log" + "os" + "regexp" + "time" +) + +const ( + Красный = "\033[31m" + Зелёный = "\033[32m" + Жёлтый = "\033[33m" + Синий = "\033[34m" + Голубой = "\033[36m" + Фиолетовый = "\033[35m" + Белый = "\033[37m" + Серый = "\033[90m" + Оранжевый = "\033[38;5;208m" + Сброс_Цвета = "\033[0m" +) + +// Функция окрашивания текста +func Color(text, ansi string) string { + return ansi + text + Сброс_Цвета +} + +// Функция для удаления ANSI-кодов из строки +func RemoveAnsiCodes(text string) string { + // Регулярное выражение для удаления ANSI escape sequences + ansiRegex := regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`) + return ansiRegex.ReplaceAllString(text, "") +} + +// Логирование в файл +/* + type_log: + 0 - INFO + 1 - ERROR + 2 - WARNING +*/ +func Logs_file(type_log int, service string, message string, log_file string, console bool) { + + color_data := "" + + service_str := Color(" ["+service+"] ", Жёлтый) + type_log_str := Color(" [INFO] ", Голубой) + log_files := log_file + + switch type_log { + case 0: + type_log_str = Color(" [-INFOS-]", Голубой) + case 1: + type_log_str = Color(" [-ERROR-]", Красный) + case 2: + type_log_str = Color(" [WARNING]", Жёлтый) + } + + if type_log == 1 { + color_data = Красный + } else { + color_data = Зелёный + } + + if console { + // Очищаем текущую строку (стираем промпт >) и выводим лог с новой строки + fmt.Print("\r\033[K") + fmt.Println(Color(time.Now().Format("2006-01-02 15:04:05")+type_log_str+service_str+message, color_data)) + } + + // Создаем текст с цветами, затем удаляем ANSI-коды для файла + colored_text := time.Now().Format("2006-01-02 15:04:05") + type_log_str + service_str + message + text := RemoveAnsiCodes(colored_text) + "\n" + + // Открываем файл для дозаписи, создаём если нет, права на запись. + file, err := os.OpenFile("WebServer/tools/logs/"+log_files, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + log.Fatal(err) + } + defer file.Close() + + // Пишем строку в файл + if _, err := file.WriteString(text); err != nil { + log.Fatal(err) + } + +} diff --git a/Backend/tools/powershell_runner.go b/Backend/tools/powershell_runner.go new file mode 100644 index 0000000..34cccb8 --- /dev/null +++ b/Backend/tools/powershell_runner.go @@ -0,0 +1,72 @@ +package tools + +import ( + "bufio" + "os/exec" + "strings" + "sync" +) + +// Глобальная persistent PowerShell сессия +var ( + psCmd *exec.Cmd + psStdin *bufio.Writer + psStdout *bufio.Scanner + psMutex sync.Mutex +) + +// RunPersistentScript выполняет команды через постоянную PowerShell сессию +func RunPersistentScript(commands []string) string { + psMutex.Lock() + defer psMutex.Unlock() + + // Инициализируем если еще не запущен + if psCmd == nil { + psCmd = exec.Command("powershell", "-NoExit", "-Command", "-") + stdin, _ := psCmd.StdinPipe() + stdout, _ := psCmd.StdoutPipe() + psStdin = bufio.NewWriter(stdin) + psStdout = bufio.NewScanner(stdout) + psCmd.Start() + } + + // Выполняем команды + fullCommand := strings.Join(commands, "; ") + psStdin.WriteString(fullCommand + "; Write-Output '---END---'\n") + psStdin.Flush() + + // Читаем результат - только последняя строка с данными + var lastLine string + for psStdout.Scan() { + line := psStdout.Text() + if line == "---END---" { + break + } + if strings.TrimSpace(line) != "" { + lastLine = line + } + } + + return lastLine +} + +// RunPowerShellCommand выполняет PowerShell команду и возвращает результат +// Если ошибка - возвращает текст ошибки в строке +func RunPScode(command string) string { + cmd := exec.Command("powershell", "-Command", command) + + output, err := cmd.Output() + if err != nil { + return "ERROR: " + err.Error() + } + + return strings.TrimSpace(string(output)) +} + +// RunPowerShellScript выполняет несколько команд PowerShell +func RunPowerShellScript(commands []string) string { + // Объединяем команды через точку с запятой + fullCommand := strings.Join(commands, "; ") + + return RunPScode(fullCommand) +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..8a0c1fc --- /dev/null +++ b/README.md @@ -0,0 +1,120 @@ +# 🚀 vServer - Функциональный веб-сервер на Go +> Функциональный веб-сервер с поддержкой HTTP/HTTPS, MySQL, PHP и веб-админкой + +**👨‍💻 Автор:** Суманеев Роман +**🌐 Сайт:** [voxsel.ru](https://voxsel.ru) +**📞 Контакт:** [VK](https://vk.com/felias) + +## 🎯 Возможности + +### 🌐 Веб-сервер +- ✅ **HTTP/HTTPS** сервер с поддержкой SSL сертификатов +- ✅ **Proxy сервер** для проксирования запросов +- ✅ **PHP сервер** со встроенной поддержкой PHP 8 +- ✅ **Статический контент** для размещения веб-сайтов + +### 🗄️ База данных +- ✅ **MySQL сервер** с полной поддержкой + +### 🔧 Администрирование +- ✅ **Веб-админка** на порту 5555 с мониторингом +- ✅ **Консольное управление** через командную строку +- ✅ **Логирование** всех операций +- ✅ **Конфигурация** через JSON файлы + +## 🏗️ Архитектура + +``` +vServer/ +├── 🎯 main.go # Точка входа +│ +├── 🔧 Backend/ # Основная логика +│ │ +│ ├── admin/ # | 🎛️ Веб-админка (порт 5555) | +│ ├── config/ # | 🔧 Конфигурационные файлы Go | +│ ├── tools/ # | 🛠️ Утилиты и хелперы | +│ └── WebServer/ # | 🌐 Модули веб-сервера | +│ +├── 🌐 WebServer/ # Веб-контент и конфигурация +│ │ +│ ├── cert/ # | 🔐 SSL сертификаты | +│ ├── soft/ # | 📦 MySQL и PHP | +│ ├── tools/ # | 📊 Логи и инструменты | +│ └── www/ # | 🌍 Веб-контент | +│ +└── 📄 go.mod # Go модули +``` + +## 🚀 Установка и запуск + +### 🔨 Сборка проекта +```bash +go build -o MyApp.exe +``` + +### 📦 Подготовка компонентов +1. Распакуйте архив `WebServer/soft/soft.rar` в папку `WebServer/soft/` +2. Запустите скомпилированный файл `MyApp.exe` + +> 🔑 **Важно:** Пароль MySQL по умолчанию - `root` + +### 📦 Готовый проект для пользователя +Для работы приложения необходимы только: +- 📄 `MyApp.exe` - исполняемый файл +- 📁 `WebServer/` - папка с конфигурацией и ресурсами + +> 💡 Папка `Backend/` и файлы `go.mod`, `main.go` и т.д. нужны только для разработки + + +## ⚙️ Конфигурация + +Настройка через `WebServer/config.json`: + +```json +{ + "Site_www": [ + { + "name": "Локальный сайт", + "host": "127.0.0.1", + "alias": ["localhost"], + "status": "active", + "root_file": "index.html" + } + ], + "Soft_Settings": { + "mysql_port": 3306, "mysql_host": "192.168.1.6", + "php_port": 8000, "php_host": "localhost", + "admin_port": "5555", "admin_host": "localhost" + } +} +``` + +**Основные параметры:** +- `Site_www` - настройки веб-сайтов +- `Soft_Settings` - порты и хосты сервисов (MySQL, PHP, админка) + +## 📝 Логирование + +Все логи сохраняются в `WebServer/tools/logs/`: + +- 🌐 `logs_http.log` - HTTP запросы +- 🔒 `logs_https.log` - HTTPS запросы +- 🗄️ `logs_mysql.log` - MySQL операции +- 🐘 `logs_php.log` - PHP ошибки +- ⚙️ `logs_config.log` - Конфигурация +- 🔒 `logs_vaccess.log` - Контроль доступа + +## 📝 Сертификаты + +Как установить сертификат ? + +1. Открыть каталог WebServer +2. Создать папку Cert +3. Создать вашу папку с основным доменом или IP для которого нужен сертификат +4. Туда положить сертификаты с определёнными именами + + certificate.ctr + private.key + ca_bundle.crt + +5. Сертификат будет успешно загружен. \ No newline at end of file diff --git a/WebServer/cert/no_cert/.gitkeep b/WebServer/cert/no_cert/.gitkeep new file mode 100644 index 0000000..f6ced6d --- /dev/null +++ b/WebServer/cert/no_cert/.gitkeep @@ -0,0 +1 @@ +# Этот файл нужен для того, чтобы Git отслеживал пустую папку no_cert \ No newline at end of file diff --git a/WebServer/cert/no_cert/certificate.crt b/WebServer/cert/no_cert/certificate.crt new file mode 100644 index 0000000..142c921 --- /dev/null +++ b/WebServer/cert/no_cert/certificate.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCTCCAfGgAwIBAgIUY0VrW1J4hftsUqKJz2gj4UNxkJkwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDcwODIwMTA1M1oXDTI2MDcw +ODIwMTA1M1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAszgR6UgXAeV3gKR1fd8moc7NEc9cwkZgUnlDFTx4uBPu +mknWk16B5qw3DjQOJcpnkyh0zNyc2fSAWx9+3pFB87UFILHL7/PUB9KJO/RwKVmm +kqCgQMF/Ga7U3LjTnX/8i/4GLHseO18kknBA/HbrKoS2L8oa6y96sggPbS4/Jg/e +7K+wGtm+++hoNPwfGAD/ajFYmqaa43kZeIQUbW+1RpiVfayOzd71/lg2MHXROejA +0jyr3Vu1BdeFtcTlaBDAIvYm4V5Rg6REEM9XrQ4I9WNGA5aDvCVqP8TAJTdeloZB +zWkKzGXpMu27QODPGY7YlNJED9ZYv3nnta/NGGWehQIDAQABo1MwUTAdBgNVHQ4E +FgQUORzZyg2/CDSnl7voH7tePiufL9MwHwYDVR0jBBgwFoAUORzZyg2/CDSnl7vo +H7tePiufL9MwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEARN6K +Bthdsz088cabijgEZD4vQmlmYLWxdtTwq2hz5Ux44i5fcr840FJAtIospcO4obQb +B1Rps+da9A3tr1yiYbpmYJ4AbogP+IrULkWNDsvNTsBBIQerNJ41WehL0l4YxB+P +ty9pLXjYeFcAuyhTWt22GM25GhsV7WnxKqCOF28Q800LJk5aWuXyVVa5VrYKMDFQ +n0LD3sYaY5Eo77x0/Lx0q1wulzTZmIPIHwGSCCxCoFoSk34L2iknx6V7sN8ZcEA6 +zNWGnBzZ9qdELrXtqr9X7neDH3Ip3rV1C6sDkyt8epB/Jvx+WyklqxS7zOzvZD2+ +wkoFk7e4NMpx8MHnzw== +-----END CERTIFICATE----- diff --git a/WebServer/cert/no_cert/private.key b/WebServer/cert/no_cert/private.key new file mode 100644 index 0000000..fcf3608 --- /dev/null +++ b/WebServer/cert/no_cert/private.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCzOBHpSBcB5XeA +pHV93yahzs0Rz1zCRmBSeUMVPHi4E+6aSdaTXoHmrDcONA4lymeTKHTM3JzZ9IBb +H37ekUHztQUgscvv89QH0ok79HApWaaSoKBAwX8ZrtTcuNOdf/yL/gYsex47XySS +cED8dusqhLYvyhrrL3qyCA9tLj8mD97sr7Aa2b776Gg0/B8YAP9qMViapprjeRl4 +hBRtb7VGmJV9rI7N3vX+WDYwddE56MDSPKvdW7UF14W1xOVoEMAi9ibhXlGDpEQQ +z1etDgj1Y0YDloO8JWo/xMAlN16WhkHNaQrMZeky7btA4M8ZjtiU0kQP1li/eee1 +r80YZZ6FAgMBAAECggEAAeDxlx9xVkzAfvG6ZZJkRkfzsF8bGtJACj1JLbzPiyZy +Vt21SSAxfmiJvzVefJjtkwZWixs47bP5cHsZCl49cb+RCTGz8JT+wQI8/Aro7hYJ +f/n9FBCzM2K4yoOJfFouHI2SJP85ZuftazeQBtV8S5rOihu1Qofh4mHhP4S/lreJ +XvreyhW6BWdj+5mA3+KbFr0X6AEsO3vN3Bqc+a9zdUYnCVl3Uis6/1rg5swjTqgW +oZLVB11fznIR9zGNRHIVcSgOsFy3JK837Z8xjZayXf3svS9ebfYe7Cd7tAz/3IVB +0N0ASoI+AqoSBwr7f+CUJotFjKqiBpnSQQvn52CX9QKBgQDmfRBEaKbIThHDcULe +pGzvmZxfFDB7Fg9HAUJKIy6oZwQn+ea2m6N/AJ9F6Ba1eBOcMMliBc/4JbPWujZ0 +teQw1C72ixiPq8ALRhp1h8woZpW/HDioZiABYHlHfxrVQHf/l+ExAuid6Mpti/dg +J/IeJrLiVMTl+uch6JFce2SPqwKBgQDHDkZya5Quho70I66kLDnk1Vu4pnqB7u/X +t4vqObMF5VB+GKOvV6mz4pD4mfa7UkR0ILgAXA25N2HCok3Ne6N43DFygnchFrtg +WcC+FMSwOzlRk3vPqUlFvfqpoz270hlP86R8cDsk8Wu1oSiUQnHdaX5eZmmBNqgR +H2lbo+YajwKBgQCes3ww7jHwd7jJbsIRVPvhGk7ONLOQ/MZ2KIrBS2pD7/Kvp+VQ +1OeFeiMw2jZQqyYthHYVNVVWUnd6oWr/f4JokKDphyrZOfQYjyOGy4MqSkBPf5oP +cYoWCJxZO055iVNWvPgEbDFJEVHYjeg94CNY2WKQbrfIdrMQ6Pa1zAyY0wKBgDvi +8pTYAtPgjb+rwI4J9D0BZ7/s7iyLO0NWKFUGmPKsJARb21sUb6z7/AufHpkKzid1 +9IW/LC3OGK5a8Ddi/DKPZJ0D3V1qHmOFfTRywR4YI02Eppo6Xx4JYxGIWDlao1zn +e1Qo29Jog7Q4USIRv3oSk/9IpnNGg1frcGIutDrHAoGBAMvcUxFvGGSETNxjnNzU +5gSzmOjVxpd+OShqMR9sOq1H7vkZRBKMy7Y4z1LDfrmsrNE/C8vjzU2vrDcTqA1g +hbFFC9jv1LglfIYdcmFCTiHb4Etb7BSu+EkoyPTKFnVSFP4tmIz42uiJEn3X8obr +LCffIwNNqa5AbVUmC2vZRDDw +-----END PRIVATE KEY----- diff --git a/WebServer/config.json b/WebServer/config.json new file mode 100644 index 0000000..9c1c5f3 --- /dev/null +++ b/WebServer/config.json @@ -0,0 +1,21 @@ +{ + "Site_www": [ + { + "alias": ["localhost"], + "host": "127.0.0.1", + "name": "Локальный сайт", + "status": "active", + "root_file": "index.html", + "root_file_routing": true + } + ], + + "Soft_Settings": { + "mysql_host": "192.168.1.6", + "mysql_port": 3306, + "php_host": "localhost", + "php_port": 8000, + "admin_host": "localhost", + "admin_port": "5555" + } +} \ No newline at end of file diff --git a/WebServer/soft/soft.rar b/WebServer/soft/soft.rar new file mode 100644 index 0000000..d873e12 Binary files /dev/null and b/WebServer/soft/soft.rar differ diff --git a/WebServer/tools/error_page/index.html b/WebServer/tools/error_page/index.html new file mode 100644 index 0000000..3f2ef29 --- /dev/null +++ b/WebServer/tools/error_page/index.html @@ -0,0 +1 @@ +Ой, а тут ошибка \ No newline at end of file diff --git a/WebServer/www/127.0.0.1/public_www/index.html b/WebServer/www/127.0.0.1/public_www/index.html new file mode 100644 index 0000000..d6c0ed1 --- /dev/null +++ b/WebServer/www/127.0.0.1/public_www/index.html @@ -0,0 +1,209 @@ + + + + + + vServer - Добро пожаловать! + + + +
+
+
+
+
+
+
+
+ +
+ +
Добро пожаловать!
+
+ Ваш сервер успешно запущен и готов к работе.
+ +
+
+
+ Сервер работает +
+ + +
+ + \ No newline at end of file diff --git a/WebServer/www/127.0.0.1/vAccess.conf b/WebServer/www/127.0.0.1/vAccess.conf new file mode 100644 index 0000000..205612d --- /dev/null +++ b/WebServer/www/127.0.0.1/vAccess.conf @@ -0,0 +1,55 @@ +# ======================================== +# vAccess Configuration File +# Система контроля доступа для веб-сервера +# ======================================== +# +# ПРИНЦИП РАБОТЫ: +# - Правила проверяются сверху вниз по порядку +# - Первое подходящее правило срабатывает и завершает проверку +# - Если ни одно правило не сработало - доступ разрешён +# - Каждый комментарий (#) начинает новое правило +# +# ПОЛЯ ПРАВИЛ: +# type: Allow (разрешить) | Disable (запретить) - ОБЯЗАТЕЛЬНОЕ +# type_file: Расширения файлов через запятую (*.php, *.exe) - ОПЦИОНАЛЬНО +# path_access: Пути через запятую (/admin/*, /api/*) - ОПЦИОНАЛЬНО +# ip_list: IP адреса через запятую (192.168.1.1, 10.0.0.5) - ОПЦИОНАЛЬНО +# ВАЖНО: Используется реальный IP соединения (не заголовки прокси!) +# exceptions_dir: Пути-исключения через запятую (/bot/*, /public/*) - ОПЦИОНАЛЬНО +# Правило НЕ применяется к этим путям +# url_error: Куда перенаправить при блокировке - ОПЦИОНАЛЬНО +# - 404 (стандартная ошибка) +# - https://site.com (внешний редирект) +# - /error.html (локальная страница) +# +# ПАТТЕРНЫ: +# - *.ext = любой файл с расширением .ext +# - no_extension = файлы без расширения (например: /api/users, /admin) +# - /path/* = все файлы в папке /path/ и подпапках +# - /file.php = конкретный файл +# +# ВАЖНО: Порядок правил имеет значение! Специфичные правила размещайте ВЫШЕ общих! +# ======================================== + +# Правило 1: Запрещаем исполнение опасных файлов в uploads с кастомной страницей +type: Disable +type_file: *.php +path_access: /uploads/*, /API/*, /app/*, /templates/* +url_error: 404 + +# Пример 2: Разрешаем доступ к админке только с определённых IP +# type: Allow +# path_access: /admin/* +# ip_list: 192.168.1.100, 10.0.0.5, 127.0.0.1 +# url_error: 404 + +# Пример 3: Блокируем определённые IP для всего сайта +# type: Disable +# ip_list: 192.168.1.50, 10.0.0.99 +# url_error: 404 + +# Пример 4: Разрешаем доступ только с определённых IP, но исключаем /bot/* +# type: Allow +# ip_list: 127.0.0.1, 192.168.0.1 +# exceptions_dir: /bot/*, /public/api/* +# url_error: https://voxsel.ru diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ea1e85a --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module vServer + +go 1.24.4 diff --git a/icon.syso b/icon.syso new file mode 100644 index 0000000..1c87c40 Binary files /dev/null and b/icon.syso differ diff --git a/main.go b/main.go new file mode 100644 index 0000000..7fcd980 --- /dev/null +++ b/main.go @@ -0,0 +1,75 @@ +package main + +import ( + "fmt" + "time" + webserver "vServer/Backend/WebServer" + admin "vServer/Backend/admin/go" + json_admin "vServer/Backend/admin/go/json" + config "vServer/Backend/config" + tools "vServer/Backend/tools" +) + +func main() { + + if !tools.CheckSingleInstance() { + println("") + println(tools.Color("❌ ОШИБКА:", tools.Красный) + " vServer уже запущен!") + println(tools.Color("💡 Подсказка:", tools.Жёлтый) + " Завершите уже запущенный процесс перед запуском нового.") + println("") + println("Нажмите Enter для завершения...") + fmt.Scanln() + return + } + + // Освобождаем мьютекс при выходе (опционально, так как Windows сама освободит) + defer tools.ReleaseMutex() + + 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("") + println("🚀 Запуск vServer...") + println("📁 Файлы сайта будут обслуживаться из папки 'www'") + println("") + println("⏳ Запуск сервисов...") + println("") + + // Инициализируем время запуска сервера + tools.ServerUptime("start") + + config.LoadConfig() + time.Sleep(50 * time.Millisecond) + + webserver.StartHandler() + time.Sleep(50 * time.Millisecond) + + // Запускаем серверы в горутинах + go admin.StartAdmin() + time.Sleep(50 * time.Millisecond) + + webserver.Cert_start() + time.Sleep(50 * time.Millisecond) + + go webserver.StartHTTPS() + json_admin.UpdateServerStatus("HTTPS Server", "running") + time.Sleep(50 * time.Millisecond) + + go webserver.StartHTTP() + json_admin.UpdateServerStatus("HTTP Server", "running") + time.Sleep(50 * time.Millisecond) + + webserver.PHP_Start() + json_admin.UpdateServerStatus("PHP Server", "running") + time.Sleep(50 * time.Millisecond) + + webserver.StartMySQLServer(false) + json_admin.UpdateServerStatus("MySQL Server", "running") + time.Sleep(50 * time.Millisecond) + + println("") + webserver.CommandListener() + +}