Инициализация проекта
Стабильный рабочий проект.
This commit is contained in:
422
Backend/WebServer/vAccess.go
Normal file
422
Backend/WebServer/vAccess.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user