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

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

View File

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