diff --git a/VERSION.txt b/VERSION.txt index d8ba80f..39f2f0a 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -v2.0.7 +v2.0.10 diff --git a/deploy/VERSION.txt b/deploy/VERSION.txt index d8ba80f..39f2f0a 100644 --- a/deploy/VERSION.txt +++ b/deploy/VERSION.txt @@ -1 +1 @@ -v2.0.7 +v2.0.10 diff --git a/kustomize/configmap.yaml b/kustomize/configmap.yaml index 104a7a6..d546e8c 100644 --- a/kustomize/configmap.yaml +++ b/kustomize/configmap.yaml @@ -5,7 +5,7 @@ metadata: namespace: lcg data: # Основные настройки - LCG_VERSION: "v2.0.7" + LCG_VERSION: "v2.0.10" 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 228e48f..04a2c3d 100644 --- a/kustomize/deployment.yaml +++ b/kustomize/deployment.yaml @@ -5,7 +5,7 @@ metadata: namespace: lcg labels: app: lcg - version: v2.0.7 + version: v2.0.10 spec: replicas: 1 selector: @@ -18,7 +18,7 @@ spec: spec: containers: - name: lcg - image: kuznetcovay/lcg:v2.0.7 + image: kuznetcovay/lcg:v2.0.10 imagePullPolicy: Always ports: - containerPort: 8080 diff --git a/kustomize/kustomization.yaml b/kustomize/kustomization.yaml index 6860444..d713dfa 100644 --- a/kustomize/kustomization.yaml +++ b/kustomize/kustomization.yaml @@ -15,11 +15,11 @@ resources: # Common labels # commonLabels: # app: lcg -# version: v2.0.7 +# version: v2.0.10 # managed-by: kustomize # Images # images: # - name: lcg # newName: kuznetcovay/lcg -# newTag: v2.0.7 +# newTag: v2.0.10 diff --git a/serve/results.go b/serve/results.go index e4cb39f..8587c2d 100644 --- a/serve/results.go +++ b/serve/results.go @@ -169,22 +169,30 @@ func formatFileSize(size int64) string { // handleFileView обрабатывает просмотр конкретного файла func handleFileView(w http.ResponseWriter, r *http.Request) { - filename := strings.TrimPrefix(r.URL.Path, "/file/") + // Учитываем BasePath при извлечении имени файла + basePath := config.AppConfig.Server.BasePath + var filename string + if basePath != "" && basePath != "/" { + basePath = strings.TrimSuffix(basePath, "/") + filename = strings.TrimPrefix(r.URL.Path, basePath+"/file/") + } else { + filename = strings.TrimPrefix(r.URL.Path, "/file/") + } if filename == "" { - http.NotFound(w, r) + renderNotFound(w, "Файл не указан", getBasePath()) return } // Проверяем, что файл существует и находится в папке результатов filePath := filepath.Join(config.AppConfig.ResultFolder, filename) if !strings.HasPrefix(filePath, config.AppConfig.ResultFolder) { - http.NotFound(w, r) + renderNotFound(w, "Запрошенный файл недоступен", getBasePath()) return } content, err := os.ReadFile(filePath) if err != nil { - http.NotFound(w, r) + renderNotFound(w, "Файл не найден или был удален", getBasePath()) return } @@ -195,9 +203,11 @@ func handleFileView(w http.ResponseWriter, r *http.Request) { data := struct { Filename string Content template.HTML + BasePath string }{ Filename: filename, Content: template.HTML(htmlContent), + BasePath: getBasePath(), } // Парсим и выполняем шаблон @@ -221,22 +231,30 @@ func handleDeleteFile(w http.ResponseWriter, r *http.Request) { return } - filename := strings.TrimPrefix(r.URL.Path, "/delete/") + // Учитываем BasePath при извлечении имени файла + basePath := config.AppConfig.Server.BasePath + var filename string + if basePath != "" && basePath != "/" { + basePath = strings.TrimSuffix(basePath, "/") + filename = strings.TrimPrefix(r.URL.Path, basePath+"/delete/") + } else { + filename = strings.TrimPrefix(r.URL.Path, "/delete/") + } if filename == "" { - http.NotFound(w, r) + renderNotFound(w, "Файл не указан", getBasePath()) return } // Проверяем, что файл существует и находится в папке результатов filePath := filepath.Join(config.AppConfig.ResultFolder, filename) if !strings.HasPrefix(filePath, config.AppConfig.ResultFolder) { - http.NotFound(w, r) + renderNotFound(w, "Запрошенный файл недоступен", getBasePath()) return } // Проверяем, что файл существует if _, err := os.Stat(filePath); os.IsNotExist(err) { - http.NotFound(w, r) + renderNotFound(w, "Файл не найден или уже удален", getBasePath()) return } diff --git a/serve/serve.go b/serve/serve.go index cb84a29..f05dd78 100644 --- a/serve/serve.go +++ b/serve/serve.go @@ -3,11 +3,13 @@ package serve import ( "crypto/tls" "fmt" + "html/template" "net/http" "os" "strings" "github.com/direct-dev-ru/linux-command-gpt/config" + "github.com/direct-dev-ru/linux-command-gpt/serve/templates" "github.com/direct-dev-ru/linux-command-gpt/ssl" ) @@ -48,6 +50,18 @@ func StartResultServer(host, port string) error { return fmt.Errorf("failed to initialize CSRF manager: %v", err) } + // Гарантируем наличие папки результатов и файла истории + if _, err := os.Stat(config.AppConfig.ResultFolder); os.IsNotExist(err) { + if mkErr := os.MkdirAll(config.AppConfig.ResultFolder, 0755); mkErr != nil { + return fmt.Errorf("failed to create results folder: %v", mkErr) + } + } + if _, err := os.Stat(config.AppConfig.ResultHistory); os.IsNotExist(err) { + if writeErr := Write(config.AppConfig.ResultHistory, []HistoryEntry{}); writeErr != nil { + return fmt.Errorf("failed to create history file: %v", writeErr) + } + } + addr := fmt.Sprintf("%s:%s", host, port) // Проверяем, нужно ли использовать HTTPS @@ -138,15 +152,21 @@ func registerHTTPSRoutes() { // Регистрируем все маршруты кроме главной страницы registerRoutesExceptHome() - // Регистрируем главную страницу с проверкой HTTPS + // Регистрируем главную страницу (строго по BasePath) с проверкой HTTPS http.HandleFunc(makePath("/"), func(w http.ResponseWriter, r *http.Request) { // Проверяем, пришел ли запрос по HTTP (не HTTPS) if r.TLS == nil { handleHTTPSRedirect(w, r) return } - // Если уже HTTPS, обрабатываем как обычно - AuthMiddleware(handleResultsPage)(w, r) + // Обрабатываем только точные пути: BasePath или BasePath/ + bp := getBasePath() + p := r.URL.Path + if (bp == "" && (p == "/" || p == "")) || (bp != "" && (p == bp || p == bp+"/")) { + AuthMiddleware(handleResultsPage)(w, r) + return + } + renderNotFound(w, "Страница не найдена", bp) }) // Регистрируем главную страницу без слэша в конце для BasePath @@ -163,6 +183,13 @@ func registerHTTPSRoutes() { AuthMiddleware(handleResultsPage)(w, r) }) } + + // Catch-all 404 для любых незарегистрированных путей (только когда BasePath задан) + if getBasePath() != "" { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + renderNotFound(w, "Страница не найдена", getBasePath()) + }) + } } // registerRoutesExceptHome регистрирует все маршруты кроме главной страницы @@ -203,6 +230,13 @@ func registerRoutesExceptHome() { // API для сохранения результатов и истории http.HandleFunc(makePath("/api/save-result"), AuthMiddleware(CSRFMiddleware(handleSaveResult))) http.HandleFunc(makePath("/api/add-to-history"), AuthMiddleware(CSRFMiddleware(handleAddToHistory))) + + // Catch-all 404 для любых незарегистрированных путей (только когда BasePath задан) + if getBasePath() != "" { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + renderNotFound(w, "Страница не найдена", getBasePath()) + }) + } } // registerRoutes регистрирует все маршруты сервера @@ -215,8 +249,17 @@ func registerRoutes() { http.HandleFunc(makePath("/api/logout"), handleLogout) http.HandleFunc(makePath("/api/validate-token"), handleValidateToken) - // Главная страница и файлы - http.HandleFunc(makePath("/"), AuthMiddleware(handleResultsPage)) + // Главная страница (строго по BasePath) и файлы + http.HandleFunc(makePath("/"), func(w http.ResponseWriter, r *http.Request) { + // Обрабатываем только точные пути: BasePath или BasePath/ + bp := getBasePath() + p := r.URL.Path + if (bp == "" && (p == "/" || p == "")) || (bp != "" && (p == bp || p == bp+"/")) { + AuthMiddleware(handleResultsPage)(w, r) + return + } + renderNotFound(w, "Страница не найдена", bp) + }) http.HandleFunc(makePath("/file/"), AuthMiddleware(handleFileView)) http.HandleFunc(makePath("/delete/"), AuthMiddleware(handleDeleteFile)) @@ -251,4 +294,30 @@ func registerRoutes() { basePath = strings.TrimSuffix(basePath, "/") http.HandleFunc(basePath, AuthMiddleware(handleResultsPage)) } + + // Catch-all 404 для любых незарегистрированных путей (только когда BasePath задан) + if getBasePath() != "" { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + renderNotFound(w, "Страница не найдена", getBasePath()) + }) + } +} + +// renderNotFound рендерит кастомную страницу 404 +func renderNotFound(w http.ResponseWriter, message, basePath string) { + w.WriteHeader(http.StatusNotFound) + data := struct { + Message string + BasePath string + }{ + Message: message, + BasePath: basePath, + } + tmpl, err := template.New("not_found").Parse(templates.NotFoundTemplate) + if err != nil { + http.Error(w, "404 Not Found", http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + _ = tmpl.Execute(w, data) } diff --git a/serve/templates/file.go b/serve/templates/file.go index efd4908..eabbc2d 100644 --- a/serve/templates/file.go +++ b/serve/templates/file.go @@ -126,7 +126,7 @@ const FileViewTemplate = `

📄 {{.Filename}}

- ← Назад к списку + ← Назад к списку
{{.Content}} diff --git a/serve/templates/not_found.go b/serve/templates/not_found.go new file mode 100644 index 0000000..09296d0 --- /dev/null +++ b/serve/templates/not_found.go @@ -0,0 +1,147 @@ +package templates + +// NotFoundTemplate современная страница 404 +const NotFoundTemplate = ` + + + + + + Страница не найдена — 404 + + + + + + + +
+
+
404
+
Страница не найдена
+

{{.Message}}

+ +
Нажмите Esc, чтобы вернуться на главную
+
+ + +` + +