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

423 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package webserver
import (
"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)
}
}
}