diff --git a/Dockerfiles/OllamaServer/Makefile b/Dockerfiles/OllamaServer/Makefile index 4b8e4b5..451cb51 100644 --- a/Dockerfiles/OllamaServer/Makefile +++ b/Dockerfiles/OllamaServer/Makefile @@ -72,6 +72,23 @@ run-podman: ## Запустить контейнер (Podman) ${IMAGE_NAME}:${IMAGE_TAG} @echo "Контейнер ${CONTAINER_NAME} запущен" +run-podman-nodemon: ## Запустить контейнер (Podman) без -d + echo "Запустить контейнер ${CONTAINER_NAME}" + echo "IMAGE_NAME: ${IMAGE_NAME}" + echo "IMAGE_TAG: ${IMAGE_TAG}" + echo "CONTAINER_NAME: ${CONTAINER_NAME}" + + podman run \ + --name ${CONTAINER_NAME} \ + --restart always \ + -p 8989:8080 \ + -v ollama-data:/home/ollama/.ollama \ + -v lcg-results:/app/data/results \ + -v lcg-prompts:/app/data/prompts \ + -v lcg-config:/app/data/config \ + ${IMAGE_NAME}:${IMAGE_TAG} + @echo "Контейнер ${CONTAINER_NAME} запущен" + stop: ## Остановить контейнер (Docker) docker stop $(CONTAINER_NAME) || true docker rm $(CONTAINER_NAME) || true diff --git a/Dockerfiles/OllamaServer/entrypoint.sh b/Dockerfiles/OllamaServer/entrypoint.sh index ad4c05b..cb5a3df 100755 --- a/Dockerfiles/OllamaServer/entrypoint.sh +++ b/Dockerfiles/OllamaServer/entrypoint.sh @@ -64,7 +64,7 @@ export LCG_PROMPT_FOLDER="${LCG_PROMPT_FOLDER:-/app/data/prompts}" export LCG_CONFIG_FOLDER="${LCG_CONFIG_FOLDER:-/app/data/config}" export LCG_SERVER_HOST="${LCG_SERVER_HOST:-0.0.0.0}" export LCG_SERVER_PORT="${LCG_SERVER_PORT:-8080}" -export LCG_SERVER_ALLOW_HTTP="${LCG_SERVER_ALLOW_HTTP:-true}" +export LCG_SERVER_ALLOW_HTTP="${LCG_SERVER_ALLOW_HTTP:-false}" log "==========================================" log "Запуск LCG с Ollama сервером" @@ -88,8 +88,7 @@ sleep 3 # Проверяем, что LCG запущен if ! kill -0 $LCG_PID 2>/dev/null; then - error "LCG сервер не запустился" - kill $OLLAMA_PID 2>/dev/null || true + error "LCG сервер не запустился" exit 1 fi diff --git a/VERSION.txt b/VERSION.txt index 7229f03..be2d10b 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -v.2.0.19 +v.2.0.20 diff --git a/deploy/VERSION.txt b/deploy/VERSION.txt index 7229f03..be2d10b 100644 --- a/deploy/VERSION.txt +++ b/deploy/VERSION.txt @@ -1 +1 @@ -v.2.0.19 +v.2.0.20 diff --git a/kustomize/configmap.yaml b/kustomize/configmap.yaml index 26c32a6..d90f0e7 100644 --- a/kustomize/configmap.yaml +++ b/kustomize/configmap.yaml @@ -5,7 +5,7 @@ metadata: namespace: lcg data: # Основные настройки - LCG_VERSION: "v.2.0.19" + LCG_VERSION: "v.2.0.20" LCG_BASE_PATH: "/lcg" LCG_SERVER_HOST: "0.0.0.0" LCG_SERVER_PORT: "8080" diff --git a/kustomize/deployment.yaml b/kustomize/deployment.yaml index f71e85c..22dbdd7 100644 --- a/kustomize/deployment.yaml +++ b/kustomize/deployment.yaml @@ -5,7 +5,7 @@ metadata: namespace: lcg labels: app: lcg - version: v.2.0.19 + version: v.2.0.20 spec: replicas: 1 selector: @@ -18,7 +18,7 @@ spec: spec: containers: - name: lcg - image: kuznetcovay/lcg:v.2.0.19 + image: kuznetcovay/lcg:v.2.0.20 imagePullPolicy: Always ports: - containerPort: 8080 diff --git a/kustomize/kustomization.yaml b/kustomize/kustomization.yaml index 0e7c0e3..9b93852 100644 --- a/kustomize/kustomization.yaml +++ b/kustomize/kustomization.yaml @@ -15,11 +15,11 @@ resources: # Common labels # commonLabels: # app: lcg -# version: v.2.0.19 +# version: v.2.0.20 # managed-by: kustomize # Images # images: # - name: lcg # newName: kuznetcovay/lcg -# newTag: v.2.0.19 +# newTag: v.2.0.20 diff --git a/serve/csrf.go b/serve/csrf.go index a42e6b4..865763c 100644 --- a/serve/csrf.go +++ b/serve/csrf.go @@ -75,6 +75,8 @@ func getCSRFSecretKey() ([]byte, error) { // GenerateToken генерирует CSRF токен для пользователя func (c *CSRFManager) GenerateToken(userID string) (string, error) { + fmt.Printf("[CSRF DEBUG] Генерация нового токена для UserID: %s\n", userID) + // Создаем данные токена data := CSRFData{ Token: generateRandomString(32), @@ -82,54 +84,85 @@ func (c *CSRFManager) GenerateToken(userID string) (string, error) { UserID: userID, } + fmt.Printf("[CSRF DEBUG] Созданные данные токена: Token (первые 20 символов): %s..., Timestamp: %d, UserID: %s\n", + safeSubstring(data.Token, 0, 20), data.Timestamp, data.UserID) + // Создаем подпись signature := c.createSignature(data) + fmt.Printf("[CSRF DEBUG] Созданная подпись (первые 20 символов): %s...\n", safeSubstring(signature, 0, 20)) // Кодируем данные в base64 encodedData := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%d:%s", data.Token, data.Timestamp, data.UserID))) + fmt.Printf("[CSRF DEBUG] Закодированные данные (первые 30 символов): %s...\n", safeSubstring(encodedData, 0, 30)) - return fmt.Sprintf("%s.%s", encodedData, signature), nil + token := fmt.Sprintf("%s.%s", encodedData, signature) + fmt.Printf("[CSRF DEBUG] Итоговый токен сгенерирован (первые 50 символов): %s...\n", safeSubstring(token, 0, 50)) + + return token, nil } // ValidateToken проверяет CSRF токен func (c *CSRFManager) ValidateToken(token, userID string) bool { + fmt.Printf("[CSRF DEBUG] Начало валидации токена. UserID из запроса: %s\n", userID) + fmt.Printf("[CSRF DEBUG] Токен (первые 50 символов): %s...\n", safeSubstring(token, 0, 50)) + // Разделяем токен на данные и подпись parts := splitToken(token) if len(parts) != 2 { + fmt.Printf("[CSRF DEBUG] ❌ ОШИБКА: Токен не может быть разделен на 2 части. Получено частей: %d\n", len(parts)) return false } encodedData, signature := parts[0], parts[1] + fmt.Printf("[CSRF DEBUG] Токен разделен на encodedData (первые 30 символов): %s... и signature (первые 20 символов): %s...\n", + safeSubstring(encodedData, 0, 30), safeSubstring(signature, 0, 20)) // Декодируем данные dataBytes, err := base64.StdEncoding.DecodeString(encodedData) if err != nil { + fmt.Printf("[CSRF DEBUG] ❌ ОШИБКА: Не удалось декодировать base64 данные: %v\n", err) return false } + fmt.Printf("[CSRF DEBUG] Данные декодированы. Длина: %d байт\n", len(dataBytes)) + // Парсим данные dataParts := splitString(string(dataBytes), ":") if len(dataParts) != 3 { + fmt.Printf("[CSRF DEBUG] ❌ ОШИБКА: Данные не могут быть разделены на 3 части. Получено частей: %d. Данные: %s\n", len(dataParts), string(dataBytes)) return false } tokenValue, timestampStr, tokenUserID := dataParts[0], dataParts[1], dataParts[2] + fmt.Printf("[CSRF DEBUG] Распарсены данные: tokenValue (первые 20 символов): %s..., timestamp: %s, tokenUserID: %s\n", + safeSubstring(tokenValue, 0, 20), timestampStr, tokenUserID) // Проверяем пользователя if tokenUserID != userID { + fmt.Printf("[CSRF DEBUG] ❌ ОШИБКА: UserID не совпадает! Ожидался: '%s', получен из токена: '%s'\n", userID, tokenUserID) return false } + fmt.Printf("[CSRF DEBUG] ✅ UserID совпадает: %s\n", userID) // Проверяем время жизни токена (минимум 12 часов) timestamp, err := parseInt64(timestampStr) if err != nil { + fmt.Printf("[CSRF DEBUG] ❌ ОШИБКА: Не удалось распарсить timestamp '%s': %v\n", timestampStr, err) return false } + now := time.Now().Unix() + age := now - timestamp + ageHours := float64(age) / 3600.0 + fmt.Printf("[CSRF DEBUG] Текущее время: %d, timestamp токена: %d, возраст токена: %d сек (%.2f часов)\n", now, timestamp, age, ageHours) + // Минимальное время жизни токена: 12 часов (не менее 12 часов согласно требованиям) - if time.Now().Unix()-timestamp > CSRFTokenLifetimeSeconds { + if age > CSRFTokenLifetimeSeconds { + fmt.Printf("[CSRF DEBUG] ❌ ОШИБКА: Токен устарел! Возраст: %d сек (%.2f часов), максимум: %d сек (%.2f часов)\n", + age, ageHours, CSRFTokenLifetimeSeconds, float64(CSRFTokenLifetimeSeconds)/3600.0) return false } + fmt.Printf("[CSRF DEBUG] ✅ Токен не устарел (возраст в пределах лимита)\n") // Создаем данные для проверки подписи data := CSRFData{ @@ -140,7 +173,30 @@ func (c *CSRFManager) ValidateToken(token, userID string) bool { // Проверяем подпись expectedSignature := c.createSignature(data) - return signature == expectedSignature + signatureMatch := signature == expectedSignature + if !signatureMatch { + fmt.Printf("[CSRF DEBUG] ❌ ОШИБКА: Подпись не совпадает!\n") + fmt.Printf("[CSRF DEBUG] Ожидаемая подпись (первые 20 символов): %s...\n", safeSubstring(expectedSignature, 0, 20)) + fmt.Printf("[CSRF DEBUG] Полученная подпись (первые 20 символов): %s...\n", safeSubstring(signature, 0, 20)) + fmt.Printf("[CSRF DEBUG] Данные для подписи: Token=%s (первые 20), Timestamp=%d, UserID=%s\n", + safeSubstring(tokenValue, 0, 20), timestamp, tokenUserID) + } else { + fmt.Printf("[CSRF DEBUG] ✅ Подпись совпадает\n") + } + fmt.Printf("[CSRF DEBUG] Результат валидации: %t\n", signatureMatch) + return signatureMatch +} + +// safeSubstring безопасно обрезает строку +func safeSubstring(s string, start, length int) string { + if start >= len(s) { + return "" + } + end := start + length + if end > len(s) { + end = len(s) + } + return s[start:end] } // createSignature создает подпись для данных @@ -161,6 +217,13 @@ func GetCSRFTokenFromCookie(r *http.Request) string { // setCSRFCookie устанавливает CSRF токен в cookie func setCSRFCookie(w http.ResponseWriter, token string) { + fmt.Printf("[CSRF DEBUG] Установка CSRF cookie. Токен (первые 50 символов): %s...\n", safeSubstring(token, 0, 50)) + fmt.Printf("[CSRF DEBUG] Cookie настройки: Path=%s, Secure=%t, Domain=%s, MaxAge=%d сек\n", + config.AppConfig.Server.CookiePath, + config.AppConfig.Server.CookieSecure, + config.AppConfig.Server.Domain, + CSRFTokenLifetimeSeconds) + // Минимальное время жизни токена: 12 часов (не менее 12 часов согласно требованиям) cookie := &http.Cookie{ Name: "csrf_token", @@ -175,9 +238,14 @@ func setCSRFCookie(w http.ResponseWriter, token string) { // Добавляем домен если указан if config.AppConfig.Server.Domain != "" { cookie.Domain = config.AppConfig.Server.Domain + fmt.Printf("[CSRF DEBUG] Cookie Domain установлен: %s\n", cookie.Domain) + } else { + fmt.Printf("[CSRF DEBUG] Cookie Domain не установлен (пустой)\n") } http.SetCookie(w, cookie) + fmt.Printf("[CSRF DEBUG] ✅ CSRF cookie установлен: Name=%s, Path=%s, Domain=%s, Secure=%t, HttpOnly=%t, SameSite=%v, MaxAge=%d\n", + cookie.Name, cookie.Path, cookie.Domain, cookie.Secure, cookie.HttpOnly, cookie.SameSite, cookie.MaxAge) } // clearCSRFCookie удаляет CSRF cookie diff --git a/serve/execute_page.go b/serve/execute_page.go index 934ae65..6ffdfb6 100644 --- a/serve/execute_page.go +++ b/serve/execute_page.go @@ -233,6 +233,9 @@ func handleExecuteRequest(w http.ResponseWriter, r *http.Request) { return } + // Устанавливаем CSRF токен в cookie после обработки запроса + setCSRFCookie(w, csrfToken) + data := ExecutePageData{ Title: "Результат выполнения", Header: "Результат выполнения", diff --git a/serve/login.go b/serve/login.go index 410ac83..06117df 100644 --- a/serve/login.go +++ b/serve/login.go @@ -3,6 +3,7 @@ package serve import ( "crypto/sha256" "encoding/hex" + "fmt" "html/template" "net/http" @@ -92,6 +93,7 @@ func RenderLoginPage(w http.ResponseWriter, data LoginPageData) error { func getSessionID(r *http.Request) string { // Пытаемся получить из cookie if cookie, err := r.Cookie("session_id"); err == nil { + fmt.Printf("[CSRF DEBUG] SessionID получен из cookie: %s\n", cookie.Value) return cookie.Value } @@ -99,7 +101,12 @@ func getSessionID(r *http.Request) string { ip := r.RemoteAddr userAgent := r.Header.Get("User-Agent") + fmt.Printf("[CSRF DEBUG] SessionID не найден в cookie. Генерация нового на основе IP=%s, User-Agent (первые 50 символов): %s...\n", + ip, safeSubstring(userAgent, 0, 50)) + // Создаем простой хеш для сессии hash := sha256.Sum256([]byte(ip + userAgent)) - return hex.EncodeToString(hash[:])[:16] + sessionID := hex.EncodeToString(hash[:])[:16] + fmt.Printf("[CSRF DEBUG] Сгенерирован SessionID: %s\n", sessionID) + return sessionID } diff --git a/serve/middleware.go b/serve/middleware.go index 65af941..8ade76f 100644 --- a/serve/middleware.go +++ b/serve/middleware.go @@ -1,6 +1,7 @@ package serve import ( + "fmt" "net/http" "strings" @@ -45,25 +46,70 @@ func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc { // CSRFMiddleware проверяет CSRF токены для POST/PUT/DELETE запросов func CSRFMiddleware(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + fmt.Printf("\n[CSRF MIDDLEWARE] ==========================================\n") + fmt.Printf("[CSRF MIDDLEWARE] Обработка запроса: %s %s\n", r.Method, r.URL.Path) + fmt.Printf("[CSRF MIDDLEWARE] RemoteAddr: %s\n", r.RemoteAddr) + fmt.Printf("[CSRF MIDDLEWARE] Host: %s\n", r.Host) + + // Выводим все заголовки + fmt.Printf("[CSRF MIDDLEWARE] Заголовки:\n") + for name, values := range r.Header { + if name == "Cookie" { + // Cookie выводим отдельно, разбирая их + fmt.Printf("[CSRF MIDDLEWARE] %s: %s\n", name, strings.Join(values, "; ")) + } else { + fmt.Printf("[CSRF MIDDLEWARE] %s: %s\n", name, strings.Join(values, ", ")) + } + } + + // Выводим все cookies + fmt.Printf("[CSRF MIDDLEWARE] Все cookies:\n") + if len(r.Cookies()) == 0 { + fmt.Printf("[CSRF MIDDLEWARE] (нет cookies)\n") + } else { + for _, cookie := range r.Cookies() { + fmt.Printf("[CSRF MIDDLEWARE] %s = %s (Path: %s, Domain: %s, Secure: %t, HttpOnly: %t, SameSite: %v, MaxAge: %d)\n", + cookie.Name, + safeSubstring(cookie.Value, 0, 50), + cookie.Path, + cookie.Domain, + cookie.Secure, + cookie.HttpOnly, + cookie.SameSite, + cookie.MaxAge) + } + } + // Проверяем только изменяющие запросы if r.Method == "GET" || r.Method == "HEAD" || r.Method == "OPTIONS" { + fmt.Printf("[CSRF MIDDLEWARE] Пропускаем проверку CSRF для метода %s\n", r.Method) next(w, r) return } // Исключаем некоторые API endpoints (с учетом BasePath) if r.URL.Path == makePath("/api/login") || r.URL.Path == makePath("/api/logout") { + fmt.Printf("[CSRF MIDDLEWARE] Пропускаем проверку CSRF для пути %s\n", r.URL.Path) next(w, r) return } // Получаем CSRF токен из заголовка или формы - csrfToken := r.Header.Get("X-CSRF-Token") + csrfTokenFromHeader := r.Header.Get("X-CSRF-Token") + csrfTokenFromForm := r.FormValue("csrf_token") + + fmt.Printf("[CSRF MIDDLEWARE] CSRF токен из заголовка X-CSRF-Token: %s\n", + safeSubstring(csrfTokenFromHeader, 0, 50)) + fmt.Printf("[CSRF MIDDLEWARE] CSRF токен из формы csrf_token: %s\n", + safeSubstring(csrfTokenFromForm, 0, 50)) + + csrfToken := csrfTokenFromHeader if csrfToken == "" { - csrfToken = r.FormValue("csrf_token") + csrfToken = csrfTokenFromForm } if csrfToken == "" { + fmt.Printf("[CSRF MIDDLEWARE] ❌ ОШИБКА: CSRF токен не найден ни в заголовке, ни в форме!\n") // Для API запросов возвращаем JSON ошибку if isAPIRequest(r) { w.Header().Set("Content-Type", "application/json") @@ -77,12 +123,47 @@ func CSRFMiddleware(next http.HandlerFunc) http.HandlerFunc { return } + fmt.Printf("[CSRF MIDDLEWARE] Используемый CSRF токен (первые 50 символов): %s...\n", + safeSubstring(csrfToken, 0, 50)) + // Получаем сессионный ID sessionID := getSessionID(r) + fmt.Printf("[CSRF MIDDLEWARE] SessionID: %s\n", sessionID) + + // Получаем CSRF токен из cookie для сравнения + csrfTokenFromCookie := GetCSRFTokenFromCookie(r) + if csrfTokenFromCookie != "" { + fmt.Printf("[CSRF MIDDLEWARE] CSRF токен из cookie (первые 50 символов): %s...\n", + safeSubstring(csrfTokenFromCookie, 0, 50)) + if csrfTokenFromCookie != csrfToken { + fmt.Printf("[CSRF MIDDLEWARE] ⚠️ ВНИМАНИЕ: Токен из cookie отличается от токена в запросе!\n") + } else { + fmt.Printf("[CSRF MIDDLEWARE] ✅ Токен из cookie совпадает с токеном в запросе\n") + } + } else { + fmt.Printf("[CSRF MIDDLEWARE] ⚠️ ВНИМАНИЕ: CSRF токен не найден в cookie!\n") + } // Проверяем CSRF токен csrfManager := GetCSRFManager() - if csrfManager == nil || !csrfManager.ValidateToken(csrfToken, sessionID) { + if csrfManager == nil { + fmt.Printf("[CSRF MIDDLEWARE] ❌ ОШИБКА: CSRF менеджер не инициализирован!\n") + if isAPIRequest(r) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + w.Write([]byte(`{"success": false, "error": "Invalid CSRF token"}`)) + return + } + http.Error(w, "Invalid CSRF token", http.StatusForbidden) + return + } + + fmt.Printf("[CSRF MIDDLEWARE] Вызов ValidateToken с токеном и sessionID: %s\n", sessionID) + valid := csrfManager.ValidateToken(csrfToken, sessionID) + fmt.Printf("[CSRF MIDDLEWARE] Результат ValidateToken: %t\n", valid) + + if !valid { + fmt.Printf("[CSRF MIDDLEWARE] ❌ ОШИБКА: Валидация CSRF токена не прошла!\n") // Для API запросов возвращаем JSON ошибку if isAPIRequest(r) { w.Header().Set("Content-Type", "application/json") @@ -96,6 +177,8 @@ func CSRFMiddleware(next http.HandlerFunc) http.HandlerFunc { return } + fmt.Printf("[CSRF MIDDLEWARE] ✅ CSRF токен валиден, продолжаем обработку запроса\n") + fmt.Printf("[CSRF MIDDLEWARE] ==========================================\n\n") // CSRF токен валиден, продолжаем next(w, r) }