Исправления в ветке auth-feature

This commit is contained in:
2025-10-27 18:48:49 +06:00
parent e1bd79db8c
commit 611bd17ac1
71 changed files with 3936 additions and 258 deletions

1
.gitignore vendored
View File

@@ -17,3 +17,4 @@ gpt_results
shell-code/jwt.admin.token
run.sh
lcg_history.json
deploy/0.create_sealed_secrets.sh

View File

@@ -1,57 +0,0 @@
# This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com
# The lines below are called `modelines`. See `:help modeline`
# Feel free to remove those if you don't want/need to use them.
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
version: 2
before:
hooks:
# You may remove this if you don't use go modules.
- go mod tidy
# you may remove this if you don't need go generate
- go generate ./...
builds:
- binary: lcg
env:
- CGO_ENABLED=0
goarch:
- amd64
- arm64
- arm
goos:
- linux
- darwin
archives:
- formats: [tar.gz]
# this name template makes the OS and Arch compatible with the results of `uname`.
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
# use zip for windows archives
format_overrides:
- goos: windows
formats: [zip]
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
release:
footer: >-
---
Released by [GoReleaser](https://github.com/goreleaser/goreleaser).

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"djlint.showInstallError": false
}

View File

@@ -1 +1 @@
v2.0.1
v2.0.2

1
build-conditions.yaml Normal file
View File

@@ -0,0 +1 @@
no-serve: false

58
build.sh Executable file
View File

@@ -0,0 +1,58 @@
#!/bin/bash
# 🚀 LCG Build Script (Root)
# Скрипт для сборки из корневой директории проекта
set -e
# Цвета для вывода
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Функция для вывода сообщений
log() {
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
}
success() {
echo -e "${GREEN}$1${NC}"
}
warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
error() {
echo -e "${RED}$1${NC}"
}
# Параметры
REPOSITORY=${1:-"your-registry.com/lcg"}
VERSION=${2:-"latest"}
PLATFORMS=${3:-"linux/amd64,linux/arm64"}
log "🚀 Сборка LCG из корневой директории..."
# Проверяем, что мы в корневой директории
if [ ! -f "go.mod" ]; then
error "Этот скрипт должен запускаться из корневой директории проекта (где находится go.mod)"
exit 1
fi
# Записываем версию в файл VERSION.txt
echo "$VERSION" > VERSION.txt
log "📝 Версия записана в VERSION.txt: $VERSION"
# Запускаем полную сборку
log "🚀 Запуск полной сборки..."
./deploy/full-build.sh "$REPOSITORY" "$VERSION" "$PLATFORMS"
if [ $? -eq 0 ]; then
success "🎉 Сборка завершена успешно!"
else
error "Ошибка при сборке"
exit 1
fi

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
)
@@ -20,9 +21,20 @@ type HistoryEntry struct {
func read(historyPath string) ([]HistoryEntry, error) {
data, err := os.ReadFile(historyPath)
if err != nil || len(data) == 0 {
if err != nil {
// Если файл не существует, создаем пустой файл истории
if os.IsNotExist(err) {
emptyHistory := []HistoryEntry{}
if writeErr := write(historyPath, emptyHistory); writeErr != nil {
return nil, fmt.Errorf("не удалось создать файл истории: %v", writeErr)
}
return emptyHistory, nil
}
return nil, err
}
if len(data) == 0 {
return []HistoryEntry{}, nil
}
var items []HistoryEntry
if err := json.Unmarshal(data, &items); err != nil {
return nil, err
@@ -50,6 +62,12 @@ func ShowHistory(historyPath string, printColored func(string, string), colorYel
printColored("📝 История пуста\n", colorYellow)
return
}
// Сортируем записи по времени в убывающем порядке (новые сначала)
sort.Slice(items, func(i, j int) bool {
return items[i].Timestamp.After(items[j].Timestamp)
})
printColored("📝 История (из файла):\n", colorYellow)
for _, h := range items {
ts := h.Timestamp.Format("2006-01-02 15:04:05")

View File

@@ -3,6 +3,7 @@ package config
import (
"os"
"path"
"slices"
"strconv"
"strings"
)
@@ -11,10 +12,12 @@ type Config struct {
Cwd string
Host string
ProxyUrl string
AppName string
Completions string
Model string
Prompt string
ApiKeyFile string
ConfigFolder string
ResultFolder string
PromptFolder string
ProviderType string
@@ -39,12 +42,21 @@ type MainFlags struct {
}
type ServerConfig struct {
Port string
Host string
ConfigFolder string
AllowHTTP bool
SSLCertFile string
SSLKeyFile string
Port string
Host string
HealthUrl string
ProxyUrl string
BasePath string
ConfigFolder string
AllowHTTP bool
SSLCertFile string
SSLKeyFile string
RequireAuth bool
Password string
Domain string
CookieSecure bool
CookiePath string
CookieTTLHours int
}
type ValidationConfig struct {
@@ -87,12 +99,7 @@ func getServerAllowHTTP() bool {
func isSecureHost(host string) bool {
secureHosts := []string{"localhost", "127.0.0.1", "::1"}
for _, secureHost := range secureHosts {
if host == secureHost {
return true
}
}
return false
return slices.Contains(secureHosts, host)
}
func Load() Config {
@@ -102,14 +109,21 @@ func Load() Config {
if err != nil {
homedir = cwd
}
os.MkdirAll(path.Join(homedir, ".config", "lcg", "gpt_results"), 0755)
resultFolder := getEnv("LCG_RESULT_FOLDER", path.Join(homedir, ".config", "lcg", "gpt_results"))
privateResultsDir := path.Join(homedir, ".config", "lcg", "gpt_results")
os.MkdirAll(privateResultsDir, 0700)
resultFolder := getEnv("LCG_RESULT_FOLDER", privateResultsDir)
os.MkdirAll(path.Join(homedir, ".config", "lcg", "gpt_sys_prompts"), 0755)
promptFolder := getEnv("LCG_PROMPT_FOLDER", path.Join(homedir, ".config", "lcg", "gpt_sys_prompts"))
privatePromptsDir := path.Join(homedir, ".config", "lcg", "gpt_sys_prompts")
os.MkdirAll(privatePromptsDir, 0700)
promptFolder := getEnv("LCG_PROMPT_FOLDER", privatePromptsDir)
privateConfigDir := path.Join(homedir, ".config", "lcg", "config")
os.MkdirAll(privateConfigDir, 0700)
configFolder := getEnv("LCG_CONFIG_FOLDER", privateConfigDir)
return Config{
Cwd: cwd,
AppName: getEnv("LCG_APP_NAME", "Linux Command GPT"),
Host: getEnv("LCG_HOST", "http://192.168.87.108:11434/"),
ProxyUrl: getEnv("LCG_PROXY_URL", "/api/v1/protected/sberchat/chat"),
Completions: getEnv("LCG_COMPLETIONS_PATH", "api/chat"),
@@ -118,6 +132,7 @@ func Load() Config {
ApiKeyFile: getEnv("LCG_API_KEY_FILE", ".openai_api_key"),
ResultFolder: resultFolder,
PromptFolder: promptFolder,
ConfigFolder: configFolder,
ProviderType: getEnv("LCG_PROVIDER", "ollama"),
JwtToken: getEnv("LCG_JWT_TOKEN", ""),
PromptID: getEnv("LCG_PROMPT_ID", "1"),
@@ -126,12 +141,21 @@ func Load() Config {
NoHistoryEnv: getEnv("LCG_NO_HISTORY", ""),
AllowExecution: isAllowExecutionEnabled(),
Server: ServerConfig{
Port: getEnv("LCG_SERVER_PORT", "8080"),
Host: getEnv("LCG_SERVER_HOST", "localhost"),
ConfigFolder: getEnv("LCG_CONFIG_FOLDER", path.Join(homedir, ".config", "lcg", "config")),
AllowHTTP: getServerAllowHTTP(),
SSLCertFile: getEnv("LCG_SERVER_SSL_CERT_FILE", ""),
SSLKeyFile: getEnv("LCG_SERVER_SSL_KEY_FILE", ""),
Port: getEnv("LCG_SERVER_PORT", "8080"),
Host: getEnv("LCG_SERVER_HOST", "localhost"),
ConfigFolder: getEnv("LCG_CONFIG_FOLDER", path.Join(homedir, ".config", "lcg", "config")),
AllowHTTP: getServerAllowHTTP(),
SSLCertFile: getEnv("LCG_SERVER_SSL_CERT_FILE", ""),
SSLKeyFile: getEnv("LCG_SERVER_SSL_KEY_FILE", ""),
RequireAuth: isServerRequireAuth(),
Password: getEnv("LCG_SERVER_PASSWORD", "admin#123456"),
Domain: getEnv("LCG_DOMAIN", getEnv("LCG_SERVER_HOST", "localhost")),
CookieSecure: isCookieSecure(),
CookiePath: getEnv("LCG_COOKIE_PATH", "/lcg"),
CookieTTLHours: getEnvInt("LCG_COOKIE_TTL_HOURS", 168), // 7 дней по умолчанию
BasePath: getEnv("LCG_BASE_URL", "/lcg"),
HealthUrl: getEnv("LCG_HEALTH_URL", "/api/v1/protected/sberchat/health"),
ProxyUrl: getEnv("LCG_PROXY_URL", "/api/v1/protected/sberchat/chat"),
},
Validation: ValidationConfig{
MaxSystemPromptLength: getEnvInt("LCG_MAX_SYSTEM_PROMPT_LENGTH", 2000),
@@ -162,6 +186,24 @@ func isAllowExecutionEnabled() bool {
return vLower == "1" || vLower == "true"
}
func isServerRequireAuth() bool {
v := strings.TrimSpace(getEnv("LCG_SERVER_REQUIRE_AUTH", ""))
if v == "" {
return false
}
vLower := strings.ToLower(v)
return vLower == "1" || vLower == "true"
}
func isCookieSecure() bool {
v := strings.TrimSpace(getEnv("LCG_COOKIE_SECURE", ""))
if v == "" {
return false
}
vLower := strings.ToLower(v)
return vLower == "1" || vLower == "true"
}
var AppConfig Config
func init() {

29
deploy/.goreleaser.yaml Normal file
View File

@@ -0,0 +1,29 @@
# Goreleaser configuration version 2
version: 2
builds:
- id: lcg
binary: "lcg_{{ .Version }}"
goos:
- linux
goarch:
- amd64
- arm64
env:
- CGO_ENABLED=0
ldflags:
- -s -w
- -X main.version={{.Version}}
- -X main.commit={{.Commit}}
- -X main.date={{.Date}}
main: .
dir: .
archives:
- id: lcg
builds:
- lcg
format: binary
name_template: "{{ .Binary }}_{{ .Os }}_{{ .Arch }}"
files:
- "lcg_{{ .Version }}"

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# https://dev.to/ashokan/sealed-secrets-the-secret-sauce-for-managing-secrets-2hg6
# head -c 64 /dev/urandom | base64 -w 0
export KUBECONFIG=/home/su/.kube/config_hlab
kubectl create secret generic lcg-secrets -n lcg \
--from-literal=LCG_SERVER_PASSWORDL= \
--from-literal=LCG_CSRF_SECRET=\
--from-literal=LCG_JWT_SECRET=\
--from-literal=LCG_JWT_TOKEN=\
--dry-run=client -o yaml | tee secret-cfg.yaml
kubeseal --controller-name=sealed-secrets-controller --controller-namespace=kube-system -o yaml <secret-cfg.yaml | tee sealed-cfg.yaml
rm -f secret-cfg.yaml
kubectl apply -f sealed-cfg.yaml
cp sealed-cfg.yaml ../kustomize/secret.yaml
kubectl get secret lcg-secrets -n lcg -o json | jq ".data | map_values(@base64d)"

7
deploy/0.namespace.yaml Normal file
View File

@@ -0,0 +1,7 @@
apiVersion: v1
kind: Namespace
metadata:
name: lcg
labels:
name: lcg
app: linux-command-gpt

46
deploy/1.configmap.yaml Normal file
View File

@@ -0,0 +1,46 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: lcg-config
namespace: lcg
data:
# Основные настройки
LCG_VERSION: "${VERSION}"
LCG_BASE_PATH: "/lcg"
LCG_SERVER_HOST: "0.0.0.0"
LCG_SERVER_PORT: "8080"
LCG_SERVER_ALLOW_HTTP: "true"
LCG_APP_NAME: "Linux Command GPT"
LCG_RESULT_FOLDER: "/app/data/results"
LCG_PROMPT_FOLDER: "/app/data/prompts"
LCG_CONFIG_FOLDER: "/app/data/config"
LCG_NO_HISTORY: "false"
LCG_ALLOW_EXECUTION: "false"
LCG_DEBUG: "false"
# Настройки аутентификации
LCG_SERVER_REQUIRE_AUTH: "true"
LCG_COOKIE_SECURE: "true"
LCG_COOKIE_TTL_HOURS: "168"
LCG_DOMAIN: "direct-dev.ru"
LCG_COOKIE_PATH: "/lcg"
# Настройки провайдера (по умолчанию)
LCG_PROVIDER_TYPE: "proxy"
LCG_HOST: "https://direct-dev.ru"
LCG_HEALTH_URL: "/api/v1/protected/sberchat/health"
LCG_PROXY_URL: "/api/v1/protected/sberchat/chat"
LCG_MODEL: "GigaChat-2-Max"
# Настройки валидации
LCG_MAX_SYSTEM_PROMPT_LENGTH: "2000"
LCG_MAX_USER_MESSAGE_LENGTH: "4000"
LCG_MAX_PROMPT_NAME_LENGTH: "2000"
LCG_MAX_PROMPT_DESC_LENGTH: "50000"
# Настройки таймаутов
LCG_TIMEOUT: "300"
# Настройки отладки
LCG_DEBUG: "false"

View File

@@ -0,0 +1,12 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: linux-command-gpt
namespace: flux-system
spec:
interval: 3m
url: https://direct-dev.ru/gitea/GiteaAdmin/go-lcg.git
ref:
branch: release
secretRef:
name: gitea-token

View File

@@ -0,0 +1,19 @@
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: lcg
namespace: flux-system
spec:
healthChecks:
- kind: Deployment
name: lcg
namespace: lcg
interval: 3m15s
path: ./kustomize
prune: true
sourceRef:
kind: GitRepository
name: linux-command-gpt
targetNamespace: lcg
timeout: 2m0s

114
deploy/4.build-binaries.sh Executable file
View File

@@ -0,0 +1,114 @@
#!/bin/bash
# 🚀 LCG Binary Build Script
# Скрипт для сборки бинарных файлов с помощью goreleaser на хосте
set -e
# Цвета для вывода
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Функция для вывода сообщений
log() {
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
}
success() {
echo -e "${GREEN}$1${NC}"
}
warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
error() {
echo -e "${RED}$1${NC}"
}
# Параметры
VERSION=${1:-"dev"}
# CLEAN=${2:-"true"}
# Записываем версию в файл VERSION.txt (в корневой директории проекта)
echo "$VERSION" > VERSION.txt
log "📝 Версия записана в VERSION.txt: $VERSION"
log "🚀 Сборка бинарных файлов LCG с goreleaser..."
# Проверяем наличие goreleaser
if ! command -v goreleaser &> /dev/null; then
error "goreleaser не найден. Установите goreleaser:"
echo " curl -sL https://github.com/goreleaser/goreleaser/releases/latest/download/goreleaser_Linux_x86_64.tar.gz | tar -xz -C /usr/local/bin goreleaser"
exit 1
fi
# Проверяем наличие Go
if ! command -v go &> /dev/null; then
error "Go не найден. Установите Go для сборки."
exit 1
fi
# Переходим в корневую директорию проекта
cd "$(dirname "$0")/.."
log "📁 Рабочая директория: $(pwd)"
log "📁 Папка dist будет создана в: $(pwd)/dist"
# Очищаем предыдущие сборки если нужно
# if [ "$CLEAN" = "true" ]; then
# log "🧹 Очистка предыдущих сборок..."
# rm -rf dist/
# goreleaser clean
# fi
# Проверяем наличие .goreleaser.yaml
if [ ! -f "deploy/.goreleaser.yaml" ]; then
error "Файл .goreleaser.yaml не найден в папке deploy/"
exit 1
fi
# Копируем конфигурацию goreleaser в корень проекта
log "📋 Копирование конфигурации goreleaser..."
cp deploy/.goreleaser.yaml .goreleaser.yaml
# Устанавливаем переменные окружения для версии
export GORELEASER_CURRENT_TAG="$VERSION"
# Собираем бинарные файлы
log "🏗️ Сборка бинарных файлов для всех платформ..."
goreleaser build --snapshot --clean
# Проверяем результат
if [ -d "dist" ]; then
log "📊 Собранные бинарные файлы:"
find dist -name "lcg_*" -type f | while read -r binary; do
echo " $binary ($(stat -c%s "$binary") bytes, $(file "$binary" | cut -d: -f2))"
done
success "🎉 Бинарные файлы успешно собраны!"
# Показываем структуру dist/
log "📁 Структура папки dist/:"
tree -h dist/ 2>/dev/null || find dist -type f | sort
else
error "Папка dist/ не создана. Проверьте конфигурацию goreleaser."
exit 1
fi
# Очищаем временный файл конфигурации
rm -f .goreleaser.yaml
success "🎉 Сборка бинарных файлов завершена!"
# Показываем команды для Docker сборки
echo ""
log "📝 Следующие шаги:"
echo " cd deploy"
echo " docker buildx build --platform linux/amd64,linux/arm64 --tag your-registry.com/lcg:$VERSION --push ."
echo " # или используйте скрипт:"
echo " ./5.build-docker.sh your-registry.com/lcg $VERSION"

12
deploy/4.pvc.yaml Normal file
View File

@@ -0,0 +1,12 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: lcg-data
namespace: lcg
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: nfs

183
deploy/5.build-docker.sh Executable file
View File

@@ -0,0 +1,183 @@
#!/bin/bash
# 🐳 LCG Docker Build Script
# Скрипт для сборки Docker образа с предварительно собранными бинарными файлами
set -e
# Цвета для вывода
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Функция для вывода сообщений
log() {
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
}
success() {
echo -e "${GREEN}$1${NC}"
}
warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
error() {
echo -e "${RED}$1${NC}"
}
# Параметры
REPOSITORY=${1:-"your-registry.com/lcg"}
VERSION=${2:-""}
PLATFORMS=${3:-"linux/amd64,linux/arm64"}
if [ -z "$VERSION" ]; then
error "Версия не указана! Использование: $0 <repository> <version>"
echo "Пример: $0 your-registry.com/lcg v1.0.0 <platforms>"
exit 1
fi
log "🐳 Сборка Docker образа LCG..."
# Проверяем наличие docker
if ! command -v docker &> /dev/null; then
error "Docker не найден. Установите Docker для сборки образов."
exit 1
fi
# Проверяем наличие docker buildx
if ! docker buildx version &> /dev/null; then
error "Docker Buildx не найден. Установите Docker Buildx для мультиплатформенной сборки."
exit 1
fi
# Проверяем наличие бинарных файлов в текущей директории (если запускаем из корня)
if [ ! -d "dist" ]; then
error "Папка dist/ не найдена. Сначала соберите бинарные файлы:"
echo " ./deploy/4.build-binaries.sh $VERSION"
exit 1
fi
# Проверяем наличие бинарных файлов для всех платформ
MISSING_BINARIES=()
# Ищем бинарные файлы с версией в имени
AMD64_BINARY=$(find dist -name "*linux_amd64*" -type d | head -1)
echo "AMD64_BINARY: $AMD64_BINARY"
ARM64_BINARY=$(find dist -name "*linux_arm64*" -type d | head -1)
echo "ARM64_BINARY: $ARM64_BINARY"
# Проверяем наличие бинарных файлов в найденных папках и соответствие версии
if [ -n "$AMD64_BINARY" ]; then
AMD64_FILE=$(find "$AMD64_BINARY" -name "lcg_*" -type f | head -1)
if [ -z "$AMD64_FILE" ]; then
AMD64_BINARY=""
else
# Извлекаем версию из имени файла
FILE_VERSION=$(basename "$AMD64_FILE" | sed 's/lcg_//' | sed 's/-SNAPSHOT.*//')
# Нормализуем версии для сравнения (убираем префикс 'v' если есть)
NORMALIZED_FILE_VERSION=$(echo "$FILE_VERSION" | sed 's/^v//')
NORMALIZED_VERSION=$(echo "$VERSION" | sed 's/^v//')
if [ "$NORMALIZED_FILE_VERSION" != "$NORMALIZED_VERSION" ]; then
error "Версия в имени бинарного файла ($FILE_VERSION) не совпадает с переданной версией ($VERSION)"
echo "Файл: $AMD64_FILE"
echo "Ожидаемая версия: $VERSION"
echo "Версия в файле: $FILE_VERSION"
exit 1
fi
fi
fi
if [ -n "$ARM64_BINARY" ]; then
ARM64_FILE=$(find "$ARM64_BINARY" -name "lcg_*" -type f | head -1)
if [ -z "$ARM64_FILE" ]; then
ARM64_BINARY=""
else
# Извлекаем версию из имени файла
FILE_VERSION=$(basename "$ARM64_FILE" | sed 's/lcg_//' | sed 's/-SNAPSHOT.*//')
# Нормализуем версии для сравнения (убираем префикс 'v' если есть)
NORMALIZED_FILE_VERSION=$(echo "$FILE_VERSION" | sed 's/^v//')
NORMALIZED_VERSION=$(echo "$VERSION" | sed 's/^v//')
if [ "$NORMALIZED_FILE_VERSION" != "$NORMALIZED_VERSION" ]; then
error "Версия в имени бинарного файла ($FILE_VERSION) не совпадает с переданной версией ($VERSION)"
echo "Файл: $ARM64_FILE"
echo "Ожидаемая версия: $VERSION"
echo "Версия в файле: $FILE_VERSION"
exit 1
fi
fi
fi
if [ -z "$AMD64_BINARY" ]; then
MISSING_BINARIES+=("linux/amd64")
fi
if [ -z "$ARM64_BINARY" ]; then
MISSING_BINARIES+=("linux/arm64")
fi
if [ ${#MISSING_BINARIES[@]} -gt 0 ]; then
error "Отсутствуют бинарные файлы для платформ: ${MISSING_BINARIES[*]}"
echo "Сначала соберите бинарные файлы:"
echo " ./4.build-binaries.sh $VERSION"
exit 1
fi
# Показываем найденные файлы и их версии
log "📊 Найденные бинарные файлы:"
if [ -n "$AMD64_FILE" ]; then
echo " AMD64: $AMD64_FILE"
fi
if [ -n "$ARM64_FILE" ]; then
echo " ARM64: $ARM64_FILE"
fi
# Создаем builder если не существует
log "🔧 Настройка Docker Buildx..."
docker buildx create --name lcg-builder --use 2>/dev/null || docker buildx use lcg-builder
# Копируем бинарные файлы и файл версии в папку deploy
log "📋 Копирование бинарных файлов и файла версии..."
cp -r dist ./deploy/dist
cp VERSION.txt ./deploy/VERSION.txt 2>/dev/null || echo "dev" > ./deploy/VERSION.txt
# Сборка для всех платформ
log "🏗️ Сборка образа для платформ: $PLATFORMS"
log "📦 Репозиторий: $REPOSITORY"
log "🏷️ Версия: $VERSION"
# Сборка и push
docker buildx build \
--platform "$PLATFORMS" \
--tag "$REPOSITORY:$VERSION" \
--tag "$REPOSITORY:latest" \
--push \
--file deploy/Dockerfile \
deploy/
# Очищаем скопированные файлы
rm -rf ./deploy/dist
success "🎉 Образ успешно собран и отправлен в репозиторий!"
# Показываем информацию о собранном образе
log "📊 Информация о собранном образе:"
echo " Репозиторий: $REPOSITORY"
echo " Версия: $VERSION"
echo " Платформы: $PLATFORMS"
echo " Теги: $REPOSITORY:$VERSION, $REPOSITORY:latest"
# Проверяем образы в репозитории
log "🔍 Проверка образов в репозитории..."
docker buildx imagetools inspect "$REPOSITORY:$VERSION" || warning "Не удалось проверить образ в репозитории"
success "🎉 Сборка завершена успешно!"
# Показываем команды для использования
echo ""
log "📝 Полезные команды:"
echo " docker pull $REPOSITORY:$VERSION"
echo " docker run -p 8080:8080 $REPOSITORY:$VERSION"
echo " docker buildx imagetools inspect $REPOSITORY:$VERSION"

202
deploy/6.full-build.sh Executable file
View File

@@ -0,0 +1,202 @@
#!/bin/bash
# 🚀 LCG Full Build Script
# Полный скрипт сборки: бинарные файлы + Docker образ
set -e
# Цвета для вывода
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Функция для вывода сообщений
log() {
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
}
success() {
echo -e "${GREEN}$1${NC}"
}
warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
error() {
echo -e "${RED}$1${NC}"
}
# Параметры
REPOSITORY=${1:-"kuznetcovay/lcg"}
VERSION=${2:-""}
PLATFORMS=${3:-"linux/amd64,linux/arm64"}
if [ -z "$VERSION" ]; then
error "Версия не указана! Использование: $0 <repository> <version> <platforms>"
echo "Пример: $0 kuznetcovay/lcg v1.0.0 linux/amd64,linux/arm64"
exit 1
fi
# Записываем версию в файл VERSION.txt (в корневой директории проекта)
cd "$(dirname "$0")/.."
echo "$VERSION" > VERSION.txt
log "📝 Версия записана в VERSION.txt: $VERSION"
log "🚀 Полная сборка LCG (бинарные файлы + Docker образ)..."
# Этап 1: Сборка бинарных файлов
log "📦 Этап 1: Сборка бинарных файлов с goreleaser..."
./deploy/4.build-binaries.sh "$VERSION"
if [ $? -ne 0 ]; then
error "Ошибка при сборке бинарных файлов"
exit 1
fi
success "✅ Бинарные файлы собраны успешно"
# Этап 2: Сборка Docker образа
log "🐳 Этап 2: Сборка Docker образа..."
./deploy/5.build-docker.sh "$REPOSITORY" "$VERSION" "$PLATFORMS"
if [ $? -ne 0 ]; then
error "Ошибка при сборке Docker образа"
exit 1
fi
success "✅ Docker образы собраны успешно"
# Этап 3: Генерация deployment.yaml
log "📝 Этап 3: Генерация deployment.yaml..."
# Generate deployment.yaml with env substitution
export REPOSITORY=$REPOSITORY
export VERSION=$VERSION
export PLATFORMS=$PLATFORMS
export KUBECONFIG="${HOME}/.kube/config_hlab" && kubectx default
if ! envsubst < deploy/1.configmap.yaml > kustomize/configmap.yaml; then
error "Ошибка при генерации deploy/1.configmap.yaml"
exit 1
fi
success "✅ kustomize/configmap.yaml сгенерирован успешно"
if ! envsubst < deploy/deployment.tmpl.yaml > kustomize/deployment.yaml; then
error "Ошибка при генерации kustomize/deployment.yaml"
exit 1
fi
success "✅ kustomize/deployment.yaml сгенерирован успешно"
if ! envsubst < deploy/ingress-route.tmpl.yaml > kustomize/ingress-route.yaml; then
error "Ошибка при генерации kustomize/ingress-route.yaml"
exit 1
fi
success "✅ kustomize/ingress-route.yaml сгенерирован успешно"
if ! envsubst < deploy/service.tmpl.yaml > kustomize/service.yaml; then
error "Ошибка при генерации kustomize/service.yaml"
exit 1
fi
success "✅ kustomize/service.yaml сгенерирован успешно"
if ! envsubst < deploy/kustomization.tmpl.yaml > kustomize/kustomization.yaml; then
error "Ошибка при генерации kustomize/kustomization.yaml"
exit 1
fi
success "✅ kustomize/kustomization.yaml сгенерирован успешно"
# отключить reconciliation flux
if kubectl get kustomization lcg -n flux-system > /dev/null 2>&1; then
kubectl patch kustomization lcg -n flux-system --type=merge -p '{"spec":{"suspend":true}}'
else
echo " Kustomization 'lcg' does not exist in 'flux-system' namespace. Skipping suspend."
fi
sleep 5
# зафиксировать изменения в текущей ветке, если она не main
current_branch=$(git rev-parse --abbrev-ref HEAD)
if [ "$current_branch" != "main" ]; then
log "🔧 Исправления в текущей ветке: $current_branch"
# считать, что изменения уже сделаны
git add .
git commit -m "Исправления в ветке $current_branch"
fi
# переключиться на ветку main и слить с текущей веткой, если не находимся на main
if [ "$current_branch" != "main" ]; then
git checkout main
git merge --no-ff -m "Merged branch '$current_branch' into main while building $VERSION" "$current_branch"
fi
# переключиться на ветку release и слить с веткой main
git checkout -b release
git merge --no-ff -m "Merged main into release while building $VERSION" main
# если тег $VERSION существует, удалить его и принудительно запушить
tag_exists=$(git tag -l "$VERSION")
if [ "$tag_exists" ]; then
log "🗑️ Удаление существующего тега $VERSION"
git tag -d "$VERSION"
git push origin ":refs/tags/$VERSION"
fi
# Create tag $VERSION and push to remote release branch and all tags
git tag "$VERSION"
git push origin release
git push origin --tags
# Push main branch
git checkout main
git push origin main
# Включить reconciliation flux
if kubectl get kustomization lcg -n flux-system > /dev/null 2>&1; then
kubectl patch kustomization lcg -n flux-system --type=merge -p '{"spec":{"suspend":false}}'
else
echo " Kustomization 'lcg' does not exist in 'flux-system' namespace. Skipping suspend."
fi
echo "🔄 Flux will automatically deploy $VERSION version in ~4-6 minutes..."
# Итоговая информация
echo ""
log "🎉 Полная сборка завершена успешно!"
echo ""
log "📊 Результат:"
echo " Репозиторий: $REPOSITORY"
echo " Версия: $VERSION"
echo " Платформы: $PLATFORMS"
echo " Теги: $REPOSITORY:$VERSION, $REPOSITORY:latest"
echo ""
echo ""
log "🔍 Информация о git коммитах:"
git_log=$(git log release -1 --pretty=format:"%H - %s")
echo "$git_log"
echo ""
log "📝 Команды для использования:"
echo " docker pull $REPOSITORY:$VERSION"
echo " docker run -p 8080:8080 $REPOSITORY:$VERSION"
echo " docker buildx imagetools inspect $REPOSITORY:$VERSION"
echo ""
log "🔍 Проверка образа:"
echo " docker run --rm $REPOSITORY:$VERSION /app/lcg --version"
echo ""
log "📝 Команды для использования:"
echo " kubectl apply -k kustomize"
echo " kubectl get pods"
echo " kubectl get services"
echo " kubectl get ingress"
echo " kubectl get hpa"
echo " kubectl get servicemonitor"
echo " kubectl get pods"
echo " kubectl get services"
echo " kubectl get ingress"
echo " kubectl get hpa"
echo " kubectl get servicemonitor"

39
deploy/Dockerfile Normal file
View File

@@ -0,0 +1,39 @@
# Однофазный build для LCG с предварительно собранным бинарным файлом
FROM alpine:3.22.2
# Устанавливаем зависимости
RUN apk --no-cache add ca-certificates tzdata
# Создаем пользователя
RUN adduser -D -s /bin/sh lcg
# Создаем директории и файлы
RUN mkdir -p /app/data /app/config /home/lcg/.config/lcg/gpt_results /home/lcg/.config/lcg/gpt_sys_prompts && \
echo '[]' > /home/lcg/.config/lcg/gpt_results/lcg_history.json && \
chown -R lcg:lcg /app /home/lcg/.config
# Копируем файл версии
COPY VERSION.txt /app/VERSION.txt
# Копируем предварительно собранный бинарный файл
# Ищем папку с бинарным файлом для текущей архитектуры
COPY dist/lcg_linux_${TARGETARCH}*/lcg_* /app/lcg
# Устанавливаем права
RUN chmod +x /app/lcg
# Переключаемся на пользователя lcg
USER lcg
# Устанавливаем рабочую директорию
WORKDIR /app
# Открываем порт
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/login || exit 1
# Запускаем приложение
CMD ["./lcg", "serve", "-H", "0.0.0.0", "-p", "8080"]

1
deploy/VERSION.txt Normal file
View File

@@ -0,0 +1 @@
v2.0.2

View File

@@ -0,0 +1,95 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: lcg
namespace: lcg
labels:
app: lcg
version: ${VERSION}
spec:
replicas: 1
selector:
matchLabels:
app: lcg
template:
metadata:
labels:
app: lcg
version: ${VERSION}
spec:
containers:
- name: lcg
image: ${REPOSITORY}:${VERSION}
imagePullPolicy: Always
ports:
- containerPort: 8080
protocol: TCP
envFrom:
- configMapRef:
name: lcg-config
- secretRef:
name: lcg-secret
env:
# Pod information
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 250m
memory: 512Mi
volumeMounts:
- name: lcg-data
mountPath: /app/data
- name: lcg-config
mountPath: /app/config
readOnly: true
# Health checks
startupProbe:
httpGet:
path: /login
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 30
readinessProbe:
httpGet:
path: /login
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /login
port: 8080
initialDelaySeconds: 10
periodSeconds: 60
volumes:
- name: lcg-data
persistentVolumeClaim:
claimName: lcg-data
- name: lcg-config
configMap:
name: lcg-config
# Security context
securityContext:
runAsNonRoot: true
runAsUser: 1001
readOnlyRootFilesystem: false
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
restartPolicy: Always

42
deploy/hpa.yaml Normal file
View File

@@ -0,0 +1,42 @@
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: lcg-hpa
namespace: lcg
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: lcg
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 10
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Percent
value: 50
periodSeconds: 60
- type: Pods
value: 2
periodSeconds: 60
selectPolicy: Max

View File

@@ -0,0 +1,64 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: lcg-route
namespace: lcg
labels:
app: lcg
version: ${VERSION}
spec:
entryPoints:
- websecure
routes:
- kind: Rule
match: Host(`direct-dev.ru`) && PathPrefix(`/lcg`)
services:
- name: lcg
port: 8080
tls:
secretName: le-root-direct-dev-ru
---
# apiVersion: traefik.io/v1alpha1
# kind: IngressRoute
# metadata:
# name: lcg-route
# namespace: lcg
# spec:
# entryPoints:
# - websecure
# routes:
# - kind: Rule
# match: Host(`direct-dev.ru`) && PathPrefix(`/lcg`)
# services:
# - name: lcg
# port: 8080
# middlewares:
# - name: lcg-strip-prefix
# tls:
# secretName: le-root-direct-dev-ru
# ---
# apiVersion: traefik.io/v1alpha1
# kind: Middleware
# metadata:
# name: lcg-strip-prefix
# namespace: lcg
# spec:
# stripPrefix:
# prefixes:
# - /lcg
# ---
# apiVersion: traefik.io/v1alpha1
# kind: Middleware
# metadata:
# name: lcg-headers
# namespace: lcg
# spec:
# headers:
# customRequestHeaders:
# X-Forwarded-Proto: "https"
# X-Forwarded-Port: "443"
# customResponseHeaders:
# X-Frame-Options: "DENY"
# X-Content-Type-Options: "nosniff"
# X-XSS-Protection: "1; mode=block"

View File

@@ -0,0 +1,25 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
# Namespace
namespace: lcg
# Resources
resources:
- configmap.yaml
- secret.yaml
- deployment.yaml
- service.yaml
- ingress-route.yaml
# Common labels
commonLabels:
app: lcg
version: ${VERSION}
managed-by: kustomize
# Images
images:
- name: lcg
newName: ${REPOSITORY}
newTag: ${VERSION}

18
deploy/service.tmpl.yaml Normal file
View File

@@ -0,0 +1,18 @@
apiVersion: v1
kind: Service
metadata:
name: lcg
namespace: lcg
labels:
app: lcg
version: ${VERSION}
spec:
type: ClusterIP
ports:
- port: 8080
targetPort: 8080
protocol: TCP
name: http
selector:
app: lcg
version: ${VERSION}

231
docs/CSRF_TESTING_GUIDE.md Normal file
View File

@@ -0,0 +1,231 @@
# 🛡️ Руководство по тестированию CSRF защиты
## 📋 Обзор
Это руководство поможет вам протестировать CSRF защиту в LCG приложении и понять, как работают CSRF атаки.
## 🚀 Быстрый старт
### 1. Запуск сервера с CSRF защитой
```bash
# Запуск с аутентификацией и CSRF защитой
LCG_SERVER_REQUIRE_AUTH=true ./lcg serve -p 8080
```
### 2. Автоматическое тестирование
```bash
# Запуск автоматических тестов
./test_csrf.sh
```
### 3. Ручное тестирование
```bash
# Откройте в браузере
open csrf_test.html
```
## 🧪 Типы тестов
### ✅ **Тест 1: Защищенные запросы**
- **Цель**: Проверить, что POST запросы без CSRF токена блокируются
- **Ожидаемый результат**: 403 Forbidden
- **Endpoints**: `/api/execute`, `/api/save-result`, `/api/add-to-history`
### ✅ **Тест 2: Разрешенные запросы**
- **Цель**: Проверить, что GET запросы работают
- **Ожидаемый результат**: 200 OK
- **Endpoints**: `/login`, `/`, `/history`
### ✅ **Тест 3: CSRF токены**
- **Цель**: Проверить наличие CSRF токенов в формах
- **Ожидаемый результат**: Токены присутствуют в HTML
### ✅ **Тест 4: Поддельные токены**
- **Цель**: Проверить защиту от поддельных токенов
- **Ожидаемый результат**: 403 Forbidden
## 🎯 Сценарии атак
### **Сценарий 1: Выполнение команд**
```html
<!-- Злонамеренная форма -->
<form action="http://localhost:8080/api/execute" method="POST">
<input type="hidden" name="prompt" value="rm -rf /">
<input type="hidden" name="system_id" value="1">
<button type="submit">Нажми меня!</button>
</form>
```
### **Сценарий 2: Сохранение данных**
```html
<!-- Злонамеренная форма -->
<form action="http://localhost:8080/api/save-result" method="POST">
<input type="hidden" name="result" value="Вредоносные данные">
<input type="hidden" name="command" value="malicious_command">
<button type="submit">Сохранить</button>
</form>
```
### **Сценарий 3: JavaScript атака**
```javascript
// Злонамеренный JavaScript
fetch('http://localhost:8080/api/execute', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({prompt: 'whoami', system_id: '1'})
});
```
## 🔍 Анализ результатов
### **✅ Защита работает, если:**
- Все POST запросы возвращают 403 Forbidden
- В ответах есть "CSRF token required"
- GET запросы работают нормально
- CSRF токены присутствуют в формах
### **❌ Уязвимость есть, если:**
- POST запросы выполняются успешно (200 OK)
- Команды выполняются на сервере
- Данные сохраняются без CSRF токенов
- Нет проверки Origin/Referer заголовков
## 🛠️ Инструменты тестирования
### **1. Автоматический скрипт**
```bash
./test_csrf.sh
```
- Тестирует все основные endpoints
- Проверяет CSRF токены
- Выводит подробный отчет
### **2. HTML тестовая страница**
```bash
open csrf_test.html
```
- Интерактивное тестирование
- Визуальная проверка результатов
- Тестирование в браузере
### **3. Демонстрационная атака**
```bash
open csrf_demo.html
```
- Показывает, как работают CSRF атаки
- Демонстрирует уязвимости
- Образовательные цели
## 🔧 Настройка тестов
### **Переменные окружения для тестирования:**
```bash
# Включить аутентификацию
LCG_SERVER_REQUIRE_AUTH=true
# Настроить CSRF защиту
LCG_COOKIE_SECURE=false
LCG_DOMAIN=.localhost
LCG_COOKIE_PATH=/
# Запуск сервера
./lcg serve -H 0.0.0.0 -p 8080
```
### **Настройка reverse proxy для тестирования:**
```bash
# Для тестирования за reverse proxy
LCG_SERVER_REQUIRE_AUTH=true \
LCG_SERVER_ALLOW_HTTP=true \
LCG_DOMAIN=.yourdomain.com \
LCG_COOKIE_PATH=/lcg \
LCG_COOKIE_SECURE=false \
./lcg serve -H 0.0.0.0 -p 8080
```
## 📊 Интерпретация результатов
### **Успешные тесты:**
``` text
✅ CSRF защита /api/execute: PASS - Запрос заблокирован (403 Forbidden)
✅ CSRF защита /api/save-result: PASS - Запрос заблокирован (403 Forbidden)
✅ CSRF защита /api/add-to-history: PASS - Запрос заблокирован (403 Forbidden)
✅ GET запросы: PASS - GET запросы работают (HTTP 200)
✅ CSRF токен на странице входа: PASS - Токен найден
✅ CSRF защита от поддельного токена: PASS - Поддельный токен заблокирован (403 Forbidden)
```
### **Проблемные тесты:**
``` text
❌ CSRF защита /api/execute: FAIL - Запрос прошел (HTTP 200)
❌ CSRF защита /api/save-result: FAIL - Запрос прошел (HTTP 200)
❌ CSRF токен на странице входа: FAIL - Токен не найден
```
## 🚨 Частые проблемы
### **1. Cookies не работают**
- Проверьте настройки `LCG_DOMAIN`
- Убедитесь, что `LCG_COOKIE_PATH` правильный
- Проверьте настройки reverse proxy
### **2. CSRF токены не генерируются**
- Убедитесь, что `LCG_SERVER_REQUIRE_AUTH=true`
- Проверьте инициализацию CSRF менеджера
- Проверьте логи сервера
### **3. Запросы проходят без токенов**
- Проверьте middleware в `serve/middleware.go`
- Убедитесь, что CSRF middleware применяется
- Проверьте исключения в middleware
## 📝 Рекомендации
### **Для разработчиков:**
1. Всегда тестируйте CSRF защиту
2. Используйте автоматические тесты
3. Проверяйте все POST endpoints
4. Валидируйте CSRF токены
### **Для администраторов:**
1. Регулярно запускайте тесты
2. Мониторьте логи на подозрительную активность
3. Настройте правильные заголовки в reverse proxy
4. Используйте HTTPS в продакшене
## 🎓 Образовательные материалы
- **OWASP CSRF Prevention Cheat Sheet**: <https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html>
- **CSRF атаки**: <https://owasp.org/www-community/attacks/csrf>
- **SameSite cookies**: <https://web.dev/samesite-cookies-explained/>
---
**⚠️ ВНИМАНИЕ**: Эти тесты предназначены только для проверки безопасности вашего собственного приложения. Не используйте их для атак на чужие системы!

258
docs/REVERSE_PROXY_GUIDE.md Normal file
View File

@@ -0,0 +1,258 @@
# 🔄 Гайд по настройке LCG за Reverse Proxy
## 📋 Переменные окружения для Reverse Proxy
### 🔧 **Основные настройки:**
```bash
# Включить аутентификацию
LCG_SERVER_REQUIRE_AUTH=true
# Настроить домен для cookies (опционально)
LCG_DOMAIN=.yourdomain.com
# Настроить путь для cookies (для префикса пути)
LCG_COOKIE_PATH=/lcg
# Управление Secure флагом cookies
LCG_COOKIE_SECURE=false
# Разрешить HTTP (для работы за reverse proxy)
LCG_SERVER_ALLOW_HTTP=true
# Настроить хост и порт
LCG_SERVER_HOST=0.0.0.0
LCG_SERVER_PORT=8080
# Пароль для входа (по умолчанию: admin#123456)
LCG_SERVER_PASSWORD=your_secure_password
```
## 🚀 **Запуск за Reverse Proxy**
### **1. Nginx конфигурация:**
```nginx
server {
listen 443 ssl;
server_name yourdomain.com;
# SSL настройки
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Важно для работы cookies
proxy_cookie_domain localhost yourdomain.com;
}
}
```
### **2. Apache конфигурация:**
```apache
<VirtualHost *:443>
ServerName yourdomain.com
SSLEngine on
SSLCertificateFile /path/to/cert.pem
SSLCertificateKeyFile /path/to/key.pem
ProxyPreserveHost On
ProxyPass / http://localhost:8080/
ProxyPassReverse / http://localhost:8080/
# Настройки для cookies
ProxyPassReverseCookieDomain localhost yourdomain.com
</VirtualHost>
```
### **3. Caddy конфигурация:**
```caddy
yourdomain.com {
reverse_proxy localhost:8080 {
header_up Host {host}
header_up X-Real-IP {remote}
header_up X-Forwarded-For {remote}
header_up X-Forwarded-Proto {scheme}
}
}
```
## 🏃‍♂️ **Команды запуска**
### **Базовый запуск:**
```bash
LCG_SERVER_REQUIRE_AUTH=true LCG_SERVER_ALLOW_HTTP=true ./lcg serve -H 0.0.0.0 -p 8080
```
### **С настройкой домена:**
```bash
LCG_SERVER_REQUIRE_AUTH=true \
LCG_SERVER_ALLOW_HTTP=true \
LCG_DOMAIN=.yourdomain.com \
LCG_COOKIE_SECURE=false \
./lcg serve -H 0.0.0.0 -p 8080
```
### **С кастомным паролем:**
```bash
LCG_SERVER_REQUIRE_AUTH=true \
LCG_SERVER_ALLOW_HTTP=true \
LCG_SERVER_PASSWORD=my_secure_password \
LCG_DOMAIN=.yourdomain.com \
./lcg serve -H 0.0.0.0 -p 8080
```
## 🔒 **Безопасность**
### **Рекомендуемые настройки:**
-`LCG_SERVER_REQUIRE_AUTH=true` - всегда включайте аутентификацию
-`LCG_COOKIE_SECURE=false` - для HTTP за reverse proxy
-`LCG_DOMAIN=.yourdomain.com` - для правильной работы cookies
- ✅ Сильный пароль в `LCG_SERVER_PASSWORD`
### **Настройки Reverse Proxy:**
- ✅ Передавайте заголовки `X-Forwarded-*`
- ✅ Настройте `proxy_cookie_domain` в Nginx
- ✅ Используйте HTTPS на уровне reverse proxy
## 🐳 **Docker Compose пример**
```yaml
version: '3.8'
services:
lcg:
image: your-lcg-image
environment:
- LCG_SERVER_REQUIRE_AUTH=true
- LCG_SERVER_ALLOW_HTTP=true
- LCG_DOMAIN=.yourdomain.com
- LCG_COOKIE_SECURE=false
- LCG_SERVER_PASSWORD=secure_password
ports:
- "8080:8080"
restart: unless-stopped
nginx:
image: nginx:alpine
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/nginx/ssl
ports:
- "443:443"
depends_on:
- lcg
```
## 🔍 **Диагностика проблем**
### **Проверка cookies:**
```bash
# Проверить установку cookies
curl -I https://yourdomain.com/login
# Проверить домен cookies
curl -v https://yourdomain.com/login 2>&1 | grep -i cookie
```
### **Логи приложения:**
```bash
# Запуск с debug режимом
LCG_SERVER_REQUIRE_AUTH=true \
LCG_SERVER_ALLOW_HTTP=true \
./lcg -d serve -H 0.0.0.0 -p 8080
```
## 📝 **Примечания**
- **SameSite=Lax** - более мягкий режим для reverse proxy
- **Domain cookies** - работают только с указанным доменом
- **Secure=false** - обязательно для HTTP за reverse proxy
- **X-Forwarded-* заголовки** - важны для правильной работы
## 🆘 **Частые проблемы**
1. **Cookies не работают** → Проверьте `LCG_DOMAIN` и настройки reverse proxy
2. **Ошибка 403 CSRF** → Проверьте передачу cookies через reverse proxy
3. **Не работает аутентификация** → Убедитесь что `LCG_SERVER_REQUIRE_AUTH=true`
4. **Проблемы с HTTPS** → Настройте `LCG_COOKIE_SECURE=false` для HTTP за reverse proxy
## 🛣️ **Конфигурация с префиксом пути**
### **Пример: example.com/lcg**
#### **Переменные окружения для префикса:**
```bash
LCG_SERVER_REQUIRE_AUTH=true \
LCG_SERVER_ALLOW_HTTP=true \
LCG_DOMAIN=.example.com \
LCG_COOKIE_PATH=/lcg \
LCG_COOKIE_SECURE=false \
./lcg serve -H 0.0.0.0 -p 8080
```
#### **Nginx с префиксом:**
```nginx
server {
listen 443 ssl;
server_name example.com;
location /lcg/ {
proxy_pass http://localhost:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Важно для работы cookies с префиксом
proxy_cookie_domain localhost example.com;
proxy_cookie_path / /lcg/;
}
}
```
#### **Apache с префиксом:**
```apache
<VirtualHost *:443>
ServerName example.com
SSLEngine on
ProxyPreserveHost On
ProxyPass /lcg/ http://localhost:8080/
ProxyPassReverse /lcg/ http://localhost:8080/
# Настройки для cookies с префиксом
ProxyPassReverseCookieDomain localhost example.com
ProxyPassReverseCookiePath / /lcg/
</VirtualHost>
```
#### **Caddy с префиксом:**
```caddy
example.com {
reverse_proxy /lcg/* localhost:8080 {
header_up Host {host}
header_up X-Real-IP {remote}
header_up X-Forwarded-For {remote}
header_up X-Forwarded-Proto {scheme}
}
}
```

View File

@@ -2,7 +2,7 @@
## Что это
Linux Command GPT (`lcg`) преобразует описание на естественном языке в готовую Linuxкоманду. Инструмент поддерживает сменные провайдеры LLM (Ollama или Proxy), управление системными промптами, историю запросов, сохранение результатов, HTTP сервер для просмотра результатов и интерактивные действия над сгенерированной командой.
Linux Command GPT (`lcg`) преобразует описание на естественном языке в готовую команду для Linux или Windows. Инструмент автоматически определяет операционную систему и использует соответствующие промпты. Поддерживает сменные провайдеры LLM (Ollama или Proxy), управление системными промптами, историю запросов, сохранение результатов, HTTP сервер для просмотра результатов, аутентификацию, CSRF защиту, интерактивные действия над сгенерированной командой и деплой в Kubernetes.
## Требования
@@ -60,11 +60,18 @@ lcg --file /path/to/context.txt "хочу вывести список дирек
Действия: (c)копировать, (s)сохранить, (r)перегенерировать, (e)выполнить, (v|vv|vvv)подробно, (n)ничего:
```
### Что нового в 2.0.1
### Что нового в 3.0.0
- Улучшена мобильная версия веб‑интерфейса: корректные размеры кнопок, шрифтов и отступов; адаптивная верстка
- Учитывается `prefers-reduced-motion` для снижения анимаций, если это задано в системе
- Добавлен REST эндпоинт `POST /execute` (только через curl) — см. подробности и примеры в `API_GUIDE.md`
- **Аутентификация**: Добавлена система аутентификации с JWT токенами и HTTP-only cookies
- **CSRF защита**: Полная защита от CSRF атак с токенами и middleware
- **Безопасность**: Улучшенная безопасность с проверкой токенов и сессий
- **Kubernetes деплой**: Полный набор манифестов для деплоя в Kubernetes с Traefik
- **Flux CD**: GitOps конфигурация для автоматического деплоя
- **Reverse Proxy**: Поддержка работы за reverse proxy с настройкой cookies
- **Веб-интерфейс**: Улучшенный веб-интерфейс с современным дизайном
- **Мониторинг**: Prometheus метрики и ServiceMonitor
- **Масштабирование**: HPA для автоматического масштабирования
- **Тестирование**: Инструменты для тестирования CSRF защиты
## Переменные окружения
@@ -90,6 +97,13 @@ lcg --file /path/to/context.txt "хочу вывести список дирек
| `LCG_ALLOW_EXECUTION` | пусто | Если `1`/`true` — включает возможность выполнения команд через опцию `(e)` в меню действий. |
| `LCG_SERVER_PORT` | `8080` | Порт для HTTP сервера просмотра результатов. |
| `LCG_SERVER_HOST` | `localhost` | Хост для HTTP сервера просмотра результатов. |
| `LCG_SERVER_REQUIRE_AUTH` | `false` | Требовать аутентификацию для доступа к веб-интерфейсу. |
| `LCG_SERVER_PASSWORD` | `admin#123456` | Пароль для аутентификации. |
| `LCG_COOKIE_SECURE` | `false` | Использовать Secure флаг для cookies (для HTTPS). |
| `LCG_DOMAIN` | пусто | Домен для cookies (для reverse proxy). |
| `LCG_COOKIE_PATH` | `/` | Путь для cookies (для reverse proxy). |
| `LCG_COOKIE_TTL_HOURS` | `168` | Время жизни cookies в часах (по умолчанию 7 дней). |
| `LCG_CSRF_SECRET` | пусто | Секрет для CSRF токенов (генерируется автоматически). |
Примеры настройки:
@@ -104,6 +118,14 @@ export LCG_PROVIDER=proxy
export LCG_HOST=http://localhost:8080
export LCG_MODEL=GigaChat-2
export LCG_JWT_TOKEN=your_jwt_token_here
# Аутентификация и безопасность
export LCG_SERVER_REQUIRE_AUTH=true
export LCG_SERVER_PASSWORD=my_secure_password
export LCG_COOKIE_SECURE=false
export LCG_DOMAIN=.example.com
export LCG_COOKIE_PATH=/lcg
export LCG_COOKIE_TTL_HOURS=72 # 3 дня
```
## Базовый синтаксис
@@ -146,6 +168,8 @@ lcg [глобальные опции] <описание команды>
- `--port, -p` — порт сервера (по умолчанию из `LCG_SERVER_PORT`)
- `--host, -H` — хост сервера (по умолчанию из `LCG_SERVER_HOST`)
- `--browser, -b` — открыть браузер автоматически после старта
- `--require-auth` — включить аутентификацию (переопределяет `LCG_SERVER_REQUIRE_AUTH`)
- `--password` — пароль для аутентификации (переопределяет `LCG_SERVER_PASSWORD`)
### Подробные объяснения (v/vv/vvv)
@@ -196,6 +220,39 @@ lcg [глобальные опции] <описание команды>
- Не вставляйте секреты в запросы. Перед выполнением (`e`) проверяйте команду вручную.
- Для структуры API см. `API_CONTRACT.md` (эндпоинты и форматы запросов/ответов).
## Поддержка операционных систем
### Автоматическое определение ОС
Приложение автоматически определяет операционную систему и использует соответствующие промпты:
- **Linux/Unix системы** (включая macOS): используются промпты для Linux команд
- **Windows**: используются промпты для Windows команд (PowerShell, CMD, Batch)
### Промпты для Windows
На Windows системах доступны следующие встроенные промпты:
| ID | Name | Описание |
| --- | --- | --- |
| 1 | windows-command | Основной промпт для генерации Windows команд |
| 2 | windows-command-with-explanation | Промпт с подробным объяснением команд |
| 3 | windows-command-safe | Безопасный анализ команд с предупреждениями |
| 4 | windows-command-verbose | Подробный анализ с техническими деталями |
| 5 | windows-command-simple | Простое и понятное объяснение |
### Примеры использования на Windows
```cmd
# PowerShell команды
lcg "хочу получить список всех процессов"
lcg "показать информацию о дисках"
# CMD команды
lcg "создать папку test и перейти в неё"
lcg "найти все файлы .txt в текущей директории"
```
## Системные промпты
### Управление промптами
@@ -210,6 +267,9 @@ lcg [глобальные опции] <описание команды>
### Встроенные промпты (ID 15)
Промпты автоматически выбираются в зависимости от операционной системы:
**Linux/Unix системы:**
| ID | Name | Описание |
| --- | --- | --- |
| 1 | linux-command | «Ответь только Linuxкомандой, без форматирования и объяснений». |
@@ -218,6 +278,15 @@ lcg [глобальные опции] <описание команды>
| 4 | linux-command-verbose | Команда с подробными объяснениями флагов и альтернатив. |
| 5 | linux-command-simple | Простые команды, избегать сложных опций. |
**Windows системы:**
| ID | Name | Описание |
| --- | --- | --- |
| 1 | windows-command | «Ответь только Windowsкомандой, без форматирования и объяснений». |
| 2 | windows-command-with-explanation | Сгенерируй команду и кратко объясни, что она делает (формат: COMMAND: explanation). |
| 3 | windows-command-safe | Безопасные команды (без потери данных). Вывод — только команда. |
| 4 | windows-command-verbose | Команда с подробными объяснениями флагов и альтернатив. |
| 5 | windows-command-simple | Простые команды, избегать сложных опций. |
### Промпты подробности (ID 68)
| ID | Name | Описание |
@@ -275,6 +344,12 @@ lcg serve
- **Современный дизайн** — адаптивный интерфейс с карточками файлов
- **Сортировка** — файлы отсортированы по дате изменения (новые сверху)
- **Превью содержимого** — первые 200 символов каждого файла
- **Аутентификация** — защищенный доступ с JWT токенами
- **CSRF защита** — защита от межсайтовых атак
- **История запросов** (`/history`) — просмотр истории всех запросов
- **Управление промптами** (`/prompts`) — редактирование системных промптов
- **Выполнение команд** (`/run`) — интерактивное выполнение команд
- **Безопасность** — HTTP-only cookies, проверка токенов
Структура файла (команда):
@@ -358,6 +433,14 @@ lcg serve --port 9090
# Запуск на всех интерфейсах
lcg serve --host 0.0.0.0 --port 8080
# Запуск с аутентификацией
lcg serve --require-auth --password my_secure_password
# Запуск с переменными окружения
export LCG_SERVER_REQUIRE_AUTH=true
export LCG_SERVER_PASSWORD=admin#123456
lcg serve
```
## История
@@ -377,6 +460,10 @@ lcg history list
- Для `ollama`/`proxy` APIключ не нужен; команды `update-key`/`delete-key` просто уведомят об этом.
- HTTP сервер не запускается: проверьте, что порт свободен, используйте `--port` для смены порта.
- Веб-интерфейс не отображает файлы: убедитесь, что в `LCG_RESULT_FOLDER` есть `.md` файлы.
- **Аутентификация не работает**: проверьте `LCG_SERVER_REQUIRE_AUTH=true` и правильность пароля.
- **CSRF ошибки**: убедитесь, что токены передаются в заголовках `X-CSRF-Token`.
- **Cookies не сохраняются**: проверьте настройки `LCG_DOMAIN` и `LCG_COOKIE_PATH` для reverse proxy.
- **Kubernetes деплой не работает**: проверьте права доступа к кластеру и наличие всех манифестов.
## JSONистория запросов
@@ -408,17 +495,148 @@ lcg history list
## Доступ к локальному API
Эндпоинт: `POST /execute` (только через curl).
### Основные эндпоинты
- `POST /api/execute` — выполнение запросов к LLM
- `POST /api/save-result` — сохранение результатов
- `POST /api/add-to-history` — добавление в историю
- `GET /api/login` — страница аутентификации
- `POST /api/login` — аутентификация
- `POST /api/logout` — выход из системы
- `GET /metrics` — Prometheus метрики
### Примеры использования
```bash
# Запустить сервер
lcg serve
# Выполнить запрос
curl -X POST http://localhost:8080/execute \
# Выполнить запрос (без аутентификации)
curl -X POST http://localhost:8080/api/execute \
-H "Content-Type: application/json" \
-A curl \
-d '{"prompt": "create directory test", "verbose": "vv"}'
# Аутентификация
curl -X POST http://localhost:8080/api/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "admin#123456"}'
# Выполнение с CSRF токеном
curl -X POST http://localhost:8080/api/execute \
-H "Content-Type: application/json" \
-H "X-CSRF-Token: your_csrf_token" \
-H "Cookie: auth_token=your_jwt_token" \
-d '{"prompt": "create directory test"}'
```
Подробности и примеры: `API_GUIDE.md`.
## Kubernetes деплой
### Быстрый деплой
```bash
# Переход в папку деплоя
cd deploy
# Полный деплой (сборка + деплой + проверка)
./full-deploy.sh
# Или поэтапно
./build.sh lcg latest
./deploy.sh
./health-check.sh
```
### Использование Make
```bash
# Справка
make help
# Сборка и деплой
make build
make deploy
# Мониторинг
make status
make logs
make monitor
# Удаление
make undeploy
```
### Flux CD (GitOps)
```bash
# Настройка Flux CD
cd deploy/flux
./setup-flux.sh
# Создание Kustomization
./create_kustomization.sh
# Мониторинг
kubectl get kustomization lcg -n flux-system
```
### Конфигурация для reverse proxy
```bash
# Настройка для работы за reverse proxy
export LCG_SERVER_REQUIRE_AUTH=true
export LCG_SERVER_ALLOW_HTTP=true
export LCG_DOMAIN=.example.com
export LCG_COOKIE_PATH=/lcg
export LCG_COOKIE_SECURE=false
# Запуск
./lcg serve -H 0.0.0.0 -p 8080
```
### Мониторинг и безопасность
- **Prometheus метрики**: `/metrics` endpoint
- **Health checks**: автоматические проверки готовности
- **HPA**: автоматическое масштабирование (2-10 replicas)
- **CSRF защита**: токены для всех POST запросов
- **Аутентификация**: JWT токены в HTTP-only cookies
- **Security context**: non-root пользователь, минимальные права
Подробности: `deploy/README.md` и `deploy/flux/README.md`.
## Тестирование CSRF защиты
### Автоматическое тестирование
```bash
# Запуск тестов CSRF защиты
./test_csrf.sh
# Проверка результатов
echo "Проверьте вывод на наличие ошибок 403 Forbidden"
```
### Ручное тестирование
```bash
# Откройте csrf_test.html в браузере
open csrf_test.html
# Или используйте curl для тестирования
curl -X POST http://localhost:8080/api/execute \
-H "Content-Type: application/json" \
-d '{"prompt": "test"}' \
-v
```
### Демонстрация уязвимости
```bash
# Откройте csrf_demo.html для демонстрации атаки
open csrf_demo.html
```
Подробности: `CSRF_TESTING_GUIDE.md`.

6
go.mod
View File

@@ -1,11 +1,15 @@
module github.com/direct-dev-ru/linux-command-gpt
go 1.18
go 1.21
toolchain go1.23.4
require github.com/atotto/clipboard v0.1.4
require gopkg.in/yaml.v3 v3.0.1
require github.com/golang-jwt/jwt/v5 v5.3.0
require (
github.com/cpuguy83/go-md2man/v2 v2.0.5 //indirect
github.com/russross/blackfriday/v2 v2.1.0

2
go.sum
View File

@@ -2,6 +2,8 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=

View File

@@ -2,6 +2,7 @@ package gpt
import (
_ "embed"
"runtime"
"gopkg.in/yaml.v3"
)
@@ -9,6 +10,9 @@ import (
//go:embed builtin_prompts.yaml
var builtinPromptsYAML string
//go:embed builtin_prompts_windows.yaml
var builtinPromptsWindowsYAML string
var builtinPrompts string
// BuiltinPromptsData структура для YAML файла
@@ -117,7 +121,12 @@ func GetBuiltinPromptByIDAndLanguage(id int, lang string) *SystemPrompt {
func InitBuiltinPrompts(embeddedBuiltinPromptsYAML string) {
// Используем встроенный YAML, если переданный параметр пустой
if embeddedBuiltinPromptsYAML == "" {
builtinPrompts = builtinPromptsYAML
// Выбираем промпты в зависимости от операционной системы
if runtime.GOOS == "windows" {
builtinPrompts = builtinPromptsWindowsYAML
} else {
builtinPrompts = builtinPromptsYAML
}
} else {
builtinPrompts = embeddedBuiltinPromptsYAML
}

View File

@@ -0,0 +1,262 @@
prompts:
- id: 1
name: "windows-command"
description:
en: "Main prompt for generating Windows commands"
ru: "Основной промпт для генерации Windows команд"
content:
en: |
You are a Windows command line expert.
Analyze the user's task, given in natural language, and suggest
a Windows command (PowerShell, CMD, or batch) that will help accomplish this task, and provide a detailed explanation of what it does,
its parameters and possible use cases.
Focus on practical examples and best practices.
In the response, you should only provide the commands or sequence of commands ready to copy and execute
in the command line without any explanation formatting or code blocks, without ```powershell``` or ```cmd```, ` or ``` symbols.
ru: |
Вы эксперт по Windows командам и командной строке.
Проанализируйте задачу пользователя на естественном языке и предложите Windows команду или набор команд (PowerShell, CMD или batch), которые помогут выполнить эту задачу, и предоставьте подробное объяснение того, что она делает, её параметры и возможные случаи использования.
Сосредоточьтесь на практических примерах и лучших практиках.
В ответе должна присутствовать только команда или последовательность команд,
готовая к копированию и выполнению в командной строке
без объяснений, выделений и форматирования наподобие ```powershell``` или ```cmd```, без символов ` или ```.
- id: 2
name: "windows-command-with-explanation"
description:
en: "Prompt with detailed command explanation"
ru: "Промпт с подробным объяснением команд"
content:
en: |
You are a Windows system administrator with extensive experience.
Generate Windows commands based on user task descriptions and provide comprehensive explanations.
Provide a detailed analysis including:
1. **Generated Command**: The Windows command that accomplishes the task
2. **Command Breakdown**: Explain each part of the command
3. **Parameters**: Explain each flag and option used
4. **Examples**: Show practical usage scenarios
5. **Security**: Highlight any security considerations
6. **Alternatives**: Suggest similar commands if applicable
7. **Best Practices**: Recommend optimal usage
Use clear formatting with headers and bullet points for readability.
ru: |
Вы системный администратор Windows с обширным опытом.
Генерируйте Windows команды на основе описаний задач пользователей и предоставляйте исчерпывающие объяснения.
Предоставьте подробный анализ, включая:
1. **Сгенерированная команда**: Windows команда, которая выполняет задачу
2. **Разбор команды**: Объясните каждую часть команды
3. **Параметры**: Объясните каждый используемый флаг и опцию
4. **Примеры**: Покажите практические сценарии использования
5. **Безопасность**: Выделите любые соображения безопасности
6. **Альтернативы**: Предложите похожие команды, если применимо
7. **Лучшие практики**: Рекомендуйте оптимальное использование
Используйте четкое форматирование с заголовками и маркерами для читаемости.
- id: 3
name: "windows-command-safe"
description:
en: "Safe command analysis with warnings"
ru: "Безопасный анализ команд с предупреждениями"
content:
en: |
You are a Windows security expert. Generate safe Windows commands based on user task descriptions with a focus on safety and security implications.
Provide a security-focused analysis:
1. **Generated Safe Command**: The secure Windows command for the task
2. **Safety Assessment**: Why this command is safe to run
3. **Potential Risks**: What could go wrong and how to mitigate
4. **Data Impact**: What files or data might be affected
5. **Permissions**: What permissions are required
6. **Recovery**: How to undo changes if needed
7. **Best Practices**: Safe alternatives or precautions
8. **Warnings**: Critical safety considerations
Always prioritize user safety and data protection.
ru: |
Вы эксперт по безопасности Windows. Генерируйте безопасные Windows команды на основе описаний задач пользователей с акцентом на безопасность и последствия для безопасности.
Предоставьте анализ, ориентированный на безопасность:
1. **Сгенерированная безопасная команда**: Безопасная Windows команда для задачи
2. **Оценка безопасности**: Почему эта команда безопасна для выполнения
3. **Потенциальные риски**: Что может пойти не так и как это смягчить
4. **Воздействие на данные**: Какие файлы или данные могут быть затронуты
5. **Разрешения**: Какие разрешения требуются
6. **Восстановление**: Как отменить изменения при необходимости
7. **Лучшие практики**: Безопасные альтернативы или меры предосторожности
8. **Предупреждения**: Критические соображения безопасности
Всегда приоритизируйте безопасность пользователя и защиту данных.
- id: 4
name: "windows-command-verbose"
description:
en: "Detailed analysis with technical details"
ru: "Подробный анализ с техническими деталями"
content:
en: |
You are a Windows kernel and system expert. Generate Windows commands based on user task descriptions and provide an in-depth technical analysis.
Deliver a comprehensive technical breakdown:
1. **Generated Command**: The Windows command that accomplishes the task
2. **System Level**: How the command interacts with the Windows kernel
3. **Process Flow**: Step-by-step execution details
4. **Resource Usage**: CPU, memory, I/O implications
5. **Registry**: Impact on Windows registry
6. **Services**: Windows services interactions
7. **Performance**: Optimization considerations
8. **Debugging**: Troubleshooting approaches
9. **Advanced Usage**: Expert-level techniques
Include technical details, system calls, and low-level operations.
ru: |
Вы эксперт по ядру Windows и системам. Генерируйте Windows команды на основе описаний задач пользователей и предоставляйте глубокий технический анализ.
Предоставьте исчерпывающий технический разбор:
1. **Сгенерированная команда**: Windows команда, которая выполняет задачу
2. **Системный уровень**: Как команда взаимодействует с ядром Windows
3. **Поток выполнения**: Детали пошагового выполнения
4. **Использование ресурсов**: Последствия для CPU, памяти, I/O
5. **Реестр**: Воздействие на реестр Windows
6. **Службы**: Взаимодействие со службами Windows
7. **Производительность**: Соображения по оптимизации
8. **Отладка**: Подходы к устранению неполадок
9. **Продвинутое использование**: Техники экспертного уровня
Включите технические детали, системные вызовы и низкоуровневые операции.
- id: 5
name: "windows-command-simple"
description:
en: "Simple and clear explanation"
ru: "Простое и понятное объяснение"
content:
en: |
You are a friendly Windows mentor. Explain the given command in simple, easy-to-understand terms.
Command: {{.command}}
Provide a beginner-friendly explanation:
1. **What it does**: Simple, clear description
2. **Why use it**: Common reasons to use this command
3. **Basic example**: Simple usage example
4. **What to expect**: Expected output or behavior
5. **Tips**: Helpful hints for beginners
Use plain language, avoid jargon, and focus on practical understanding.
ru: |
Вы дружелюбный наставник по Windows. Объясните данную команду простыми, понятными терминами.
Команда: {{.command}}
Предоставьте объяснение, подходящее для начинающих:
1. **Что она делает**: Простое, четкое описание
2. **Зачем использовать**: Общие причины использования этой команды
3. **Базовый пример**: Простой пример использования
4. **Что ожидать**: Ожидаемый вывод или поведение
5. **Советы**: Полезные подсказки для начинающих
Используйте простой язык, избегайте жаргона и сосредоточьтесь на практическом понимании.
- id: 6
name: "verbose-v"
description:
en: "Prompt for v mode (basic explanation)"
ru: "Промпт для режима v (базовое объяснение)"
content:
en: |
You are a Windows command expert. You can provide a clear and concise explanation of the given Windows command.
Your explanation should include:
1. What this command does for the task
2. Main parameters and their purpose
3. Common use cases
4. Any important warnings or considerations
ru: |
Вы эксперт по Windows командам. Вы можете предоставьте четкое и краткое объяснение заданной Windows команды.
Ваши краткие объяснения должны включать:
1. Что делает эта команда
2. Основные параметры и их назначение
3. Общие случаи использования
4. Любые важные предупреждения или соображения
- id: 7
name: "verbose-vv"
description:
en: "Prompt for vv mode (detailed explanation)"
ru: "Промпт для режима vv (подробное объяснение)"
content:
en: |
You are a Windows system expert. Provide a detailed technical explanation of the given command.
Provide a comprehensive analysis:
1. **Command Purpose**: What it accomplishes
2. **Syntax Breakdown**: Detailed parameter analysis
3. **Technical Details**: How it works internally
4. **Use Cases**: Practical scenarios and examples
5. **Performance Impact**: Resource usage and optimization
6. **Security Considerations**: Potential risks and mitigations
7. **Advanced Usage**: Expert techniques and tips
8. **Troubleshooting**: Common issues and solutions
Include technical depth while maintaining clarity.
ru: |
Вы эксперт по Windows системам. Предоставьте подробное техническое объяснение заданной команды.
Предоставьте исчерпывающий анализ:
1. **Цель команды**: Что она достигает
2. **Разбор синтаксиса**: Подробный анализ параметров
3. **Технические детали**: Как она работает внутренне
4. **Случаи использования**: Практические сценарии и примеры
5. **Влияние на производительность**: Использование ресурсов и оптимизация
6. **Соображения безопасности**: Потенциальные риски и меры по их снижению
7. **Продвинутое использование**: Экспертные техники и советы
8. **Устранение неполадок**: Общие проблемы и решения
Включите техническую глубину, сохраняя ясность.
- id: 8
name: "verbose-vvv"
description:
en: "Prompt for vvv mode (maximum detailed explanation)"
ru: "Промпт для режима vvv (максимально подробное объяснение)"
content:
en: |
You are a Windows kernel and system architecture expert. Provide an exhaustive technical analysis of the given command.
Deliver a comprehensive technical deep-dive:
1. **System Architecture**: How it fits into the Windows ecosystem
2. **Kernel Interaction**: System calls and kernel operations
3. **Process Management**: Process creation, scheduling, and lifecycle
4. **Memory Management**: Memory allocation and management
5. **Registry Operations**: Registry I/O operations and impact
6. **Network Stack**: Network operations and protocols
7. **Security Model**: Permissions, capabilities, and security implications
8. **Performance Analysis**: CPU, memory, I/O, and network impact
9. **Debugging and Profiling**: Advanced troubleshooting techniques
10. **Source Code Analysis**: Key implementation details
11. **Alternative Implementations**: Different approaches and trade-offs
12. **Historical Context**: Evolution and development history
Provide maximum technical depth with system-level insights, code examples, and architectural understanding.
ru: |
Вы эксперт по ядру Windows и системной архитектуре. Предоставьте исчерпывающий технический анализ заданной команды.
Предоставьте исчерпывающий технический глубокий анализ:
1. **Системная архитектура**: Как она вписывается в экосистему Windows
2. **Взаимодействие с ядром**: Системные вызовы и операции ядра
3. **Управление процессами**: Создание, планирование и жизненный цикл процессов
4. **Управление памятью**: Выделение и управление памятью
5. **Операции реестра**: I/O операции реестра и воздействие
6. **Сетевой стек**: Сетевые операции и протоколы
7. **Модель безопасности**: Разрешения, возможности и последствия безопасности
8. **Анализ производительности**: Воздействие на CPU, память, I/O и сеть
9. **Отладка и профилирование**: Продвинутые техники устранения неполадок
10. **Анализ исходного кода**: Ключевые детали реализации
11. **Альтернативные реализации**: Разные подходы и компромиссы
12. **Исторический контекст**: Эволюция и история разработки
Предоставьте максимальную техническую глубину с системными инсайтами, примерами кода и архитектурным пониманием.

View File

@@ -57,6 +57,7 @@ func (pm *PromptManager) createInitialPromptsFile() {
pm.Language = "ru"
// Загружаем все встроенные промпты из YAML на русском языке
// Функция GetBuiltinPromptsByLanguage уже учитывает операционную систему
pm.Prompts = GetBuiltinPromptsByLanguage("ru")
// Сохраняем все промпты в файл
@@ -65,40 +66,8 @@ func (pm *PromptManager) createInitialPromptsFile() {
// loadDefaultPrompts загружает предустановленные промпты
func (pm *PromptManager) LoadDefaultPrompts() {
defaultPrompts := []SystemPrompt{
{
ID: 1,
Name: "linux-command",
Description: "Generate Linux commands (default)",
Content: "Reply with linux command and nothing else. Output with plain response - no need formatting. No need explanation. No need code blocks. No need ` symbols.",
},
{
ID: 2,
Name: "linux-command-with-explanation",
Description: "Generate Linux commands with explanation",
Content: "Generate a Linux command and provide a brief explanation of what it does. Format: COMMAND: explanation",
},
{
ID: 3,
Name: "linux-command-safe",
Description: "Generate safe Linux commands",
Content: "Generate a safe Linux command that won't cause data loss or system damage. Reply with linux command and nothing else. Output with plain response - no need formatting.",
},
{
ID: 4,
Name: "linux-command-verbose",
Description: "Generate Linux commands with detailed explanation",
Content: "Generate a Linux command and provide detailed explanation including what each flag does and potential alternatives.",
},
{
ID: 5,
Name: "linux-command-simple",
Description: "Generate simple Linux commands",
Content: "Generate a simple, easy-to-understand Linux command. Avoid complex flags and options when possible.",
},
}
pm.Prompts = defaultPrompts
// Используем встроенные промпты, которые автоматически выбираются по ОС
pm.Prompts = GetBuiltinPromptsByLanguage("en")
}
// loadAllPrompts загружает все промпты из файла sys_prompts

View File

@@ -8,6 +8,8 @@ import (
"net/http"
"strings"
"time"
"github.com/direct-dev-ru/linux-command-gpt/config"
)
// Provider интерфейс для работы с разными LLM провайдерами
@@ -112,7 +114,7 @@ func (p *ProxyAPIProvider) Chat(messages []Chat) (string, error) {
return "", fmt.Errorf("ошибка маршалинга запроса: %w", err)
}
req, err := http.NewRequest("POST", p.BaseURL+"/api/v1/protected/sberchat/chat", bytes.NewBuffer(jsonData))
req, err := http.NewRequest("POST", p.BaseURL+config.AppConfig.Server.ProxyUrl, bytes.NewBuffer(jsonData))
if err != nil {
return "", fmt.Errorf("ошибка создания запроса: %w", err)
}
@@ -155,7 +157,7 @@ func (p *ProxyAPIProvider) Chat(messages []Chat) (string, error) {
// Health для ProxyAPIProvider
func (p *ProxyAPIProvider) Health() error {
req, err := http.NewRequest("GET", p.BaseURL+"/api/v1/protected/sberchat/health", nil)
req, err := http.NewRequest("GET", p.BaseURL+config.AppConfig.Server.HealthUrl, nil)
if err != nil {
return fmt.Errorf("ошибка создания health check запроса: %w", err)
}

46
kustomize/configmap.yaml Normal file
View File

@@ -0,0 +1,46 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: lcg-config
namespace: lcg
data:
# Основные настройки
LCG_VERSION: "v2.0.2"
LCG_BASE_PATH: "/lcg"
LCG_SERVER_HOST: "0.0.0.0"
LCG_SERVER_PORT: "8080"
LCG_SERVER_ALLOW_HTTP: "true"
LCG_APP_NAME: "Linux Command GPT"
LCG_RESULT_FOLDER: "/app/data/results"
LCG_PROMPT_FOLDER: "/app/data/prompts"
LCG_CONFIG_FOLDER: "/app/data/config"
LCG_NO_HISTORY: "false"
LCG_ALLOW_EXECUTION: "false"
LCG_DEBUG: "false"
# Настройки аутентификации
LCG_SERVER_REQUIRE_AUTH: "true"
LCG_COOKIE_SECURE: "true"
LCG_COOKIE_TTL_HOURS: "168"
LCG_DOMAIN: "direct-dev.ru"
LCG_COOKIE_PATH: "/lcg"
# Настройки провайдера (по умолчанию)
LCG_PROVIDER_TYPE: "proxy"
LCG_HOST: "https://direct-dev.ru"
LCG_HEALTH_URL: "/api/v1/protected/sberchat/health"
LCG_PROXY_URL: "/api/v1/protected/sberchat/chat"
LCG_MODEL: "GigaChat-2-Max"
# Настройки валидации
LCG_MAX_SYSTEM_PROMPT_LENGTH: "2000"
LCG_MAX_USER_MESSAGE_LENGTH: "4000"
LCG_MAX_PROMPT_NAME_LENGTH: "2000"
LCG_MAX_PROMPT_DESC_LENGTH: "50000"
# Настройки таймаутов
LCG_TIMEOUT: "300"
# Настройки отладки
LCG_DEBUG: "false"

95
kustomize/deployment.yaml Normal file
View File

@@ -0,0 +1,95 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: lcg
namespace: lcg
labels:
app: lcg
version: v2.0.2
spec:
replicas: 1
selector:
matchLabels:
app: lcg
template:
metadata:
labels:
app: lcg
version: v2.0.2
spec:
containers:
- name: lcg
image: kuznetcovay/lcg:v2.0.2
imagePullPolicy: Always
ports:
- containerPort: 8080
protocol: TCP
envFrom:
- configMapRef:
name: lcg-config
- secretRef:
name: lcg-secret
env:
# Pod information
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 250m
memory: 512Mi
volumeMounts:
- name: lcg-data
mountPath: /app/data
- name: lcg-config
mountPath: /app/config
readOnly: true
# Health checks
startupProbe:
httpGet:
path: /login
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 30
readinessProbe:
httpGet:
path: /login
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /login
port: 8080
initialDelaySeconds: 10
periodSeconds: 60
volumes:
- name: lcg-data
persistentVolumeClaim:
claimName: lcg-data
- name: lcg-config
configMap:
name: lcg-config
# Security context
securityContext:
runAsNonRoot: true
runAsUser: 1001
readOnlyRootFilesystem: false
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
restartPolicy: Always

View File

@@ -0,0 +1,64 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: lcg-route
namespace: lcg
labels:
app: lcg
version: v2.0.2
spec:
entryPoints:
- websecure
routes:
- kind: Rule
match: Host(`direct-dev.ru`) && PathPrefix(`/lcg`)
services:
- name: lcg
port: 8080
tls:
secretName: le-root-direct-dev-ru
---
# apiVersion: traefik.io/v1alpha1
# kind: IngressRoute
# metadata:
# name: lcg-route
# namespace: lcg
# spec:
# entryPoints:
# - websecure
# routes:
# - kind: Rule
# match: Host(`direct-dev.ru`) && PathPrefix(`/lcg`)
# services:
# - name: lcg
# port: 8080
# middlewares:
# - name: lcg-strip-prefix
# tls:
# secretName: le-root-direct-dev-ru
# ---
# apiVersion: traefik.io/v1alpha1
# kind: Middleware
# metadata:
# name: lcg-strip-prefix
# namespace: lcg
# spec:
# stripPrefix:
# prefixes:
# - /lcg
# ---
# apiVersion: traefik.io/v1alpha1
# kind: Middleware
# metadata:
# name: lcg-headers
# namespace: lcg
# spec:
# headers:
# customRequestHeaders:
# X-Forwarded-Proto: "https"
# X-Forwarded-Port: "443"
# customResponseHeaders:
# X-Frame-Options: "DENY"
# X-Content-Type-Options: "nosniff"
# X-XSS-Protection: "1; mode=block"

View File

@@ -0,0 +1,25 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
# Namespace
namespace: lcg
# Resources
resources:
- configmap.yaml
- secret.yaml
- deployment.yaml
- service.yaml
- ingress-route.yaml
# Common labels
commonLabels:
app: lcg
version: v2.0.2
managed-by: kustomize
# Images
images:
- name: lcg
newName: kuznetcovay/lcg
newTag: v2.0.2

18
kustomize/secret.yaml Normal file
View File

@@ -0,0 +1,18 @@
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
creationTimestamp: null
name: lcg-secrets
namespace: lcg
spec:
encryptedData:
LCG_CSRF_SECRET: AgDYqxr2Xlf0Hmqgb9u1OildXogus4pFcqB80Dfc1uYw7wnHNOZZP+Hto1OAifXGg2eOYbB1QEsbW/lLSim/C2zpAjNUfRTkwDhQhK3FWmLOjruKBYHZxHyMuhbBmGhVTFbKApOc9zQXTASMLgIeP2vK5yDhVRr97/OJmMKeTOq8FX8CxXZPKTjehronFS3syUlIocboi0PveFf0dgj7nkYePBMST/FKGe2I1NbpOXYH2VrZWOHeoLYDUrEQMQvSAS1mnxAPkdEOuqbWMJA7cs+KKiIFsoy5eBkOXe8hoQqLfddi7ifnJ6h32StQg67qrAm4IMClS/U9iZK5C3Rm/COsywp8Mp3e1iF0hQ2yPuiHMBibxb3aRKwIryJjeo7x51PMMjJaErbTU+bwaAvMav7znOQ14N410bp2Io6KBNsOLW0DG8/mvmQcEn4Q5f4ZtSLcvaq5BQ0NjzrrIv+eCyvNzA28oAyuR0VieJdrUPqpgELrT64MwC+4m5dqdrNPdWkxbOXPG8ghs1nMSSfI+aU+JjT1vupvJA8m83NPqh+YBewmZiTwq5PdvnlAYZ+SYFLZwDWpdUTnK/hqiUkSDOJbE0pbcm03BaG9FjTJpNsmOcKu05Rs8QfWFJMOqYKC4dTA1itxTkyCxz9Y8KFJzX0RXuksN19xzLiZm+e586VtxExEfkYuBo01R39LOtTey9e8pIB9dKwFsHRAORDaZwkaKRe3RlyF8FoSq4khr79CYdGw6IAYzrg8sXAbWawXPg1osQpNR825PejO3/TwbJDPKfjo8RaHHiFa7UhaHbrm/4FwAj4gSGVL
LCG_JWT_SECRET: AgBq2Eb0fh1lO0FP2/ebnwuyiTyrAF+dD3iu7uLVgo7JuLnKRA4998rB1OZGRcZ+MNLpajIa20ezJ3aV2wCl24PSJErhzKC3IPSXoLnpJ0EfXrFGS0Pd86TQqAf0TMN2ALFrHY/LywYW09I90/Ct93WqjDKY4yVPbyVni+njUSwavp0iMEWDjENRTdWDK3f/ym1i8SMJgSjX5N6dazwyWVSeWC8oHAGPBF8rr+rhPnY1Ds7QeR/8M0GK7YLiI2nnKljxo7LjXA61jbgCLyiATcWYRylkylcO+ZU8bAsaOSJGXEjtO6s0GHY7Y9CcHQlb1VTZCVfzu/Pb7OSbIiL4wVWHi0DYScTJluVr5SorUjti8HprW9LJE0sy7GIxMdsRZ/7jfJY/s1nxFJph3Q+wkImoPD7R0Njpkwph14CH3xfhPxBpXTQO14hlhx1VYtVkYeWAtcFht+z1qFz0vn+eYkm9B28U+2uxP4WA5AyyMZBPJVekUxQiEmr2nnowbV+zu0+wRtor/lPREQBInbf9tpvth5fpnXtUCS4P2Ne4WWlY0Y5Sij5Z20l0FA0CwmMOliWUcl9vIAy+of3RvM9XGgGqMWTuJP2QFEt7E8VKzM/aI8XgdJuExSCDjd/cyOIgnz5Byy1YvdICqL1CrVpvL0mdy+KMobr59Fgdv+5q0hiHvXF8p2bkG1DHgWiSK3WARDpKzAs+cfsPomD3jMhJZFOopp1Vu9Jpy+l4IsWoY8W1KajPW2DQWjwULRt+ewNr1mAfj+8ESVSXH8zqLv+hkXeef28h7McPcfH9fx9ejs9ovS/E6Sks6EWp
LCG_JWT_TOKEN: AgB6PPDflcIav6fqhCi80Ysv9HPkI5zXIjqfot3jaYON3fNmpKNDIhyvKk4YvLbT4PEZfY+JZP/f17MoJ1eikeiZAO7klkg3wNq3h6TcRTuwM/ST9R/KsWqnfLxm5HzGBsqh39cwv9eU2ovAMXqXPJeO/23HcjOqZg7cWZ2WfknaAUydJc39Cue4zmgxlpIxF37p6/rvJqUGByOOUzlDHoVV3TORi+j6dui352PGG6gVCzcCVGNSsbf4j1VibJ1Lz06WEayMi7ZYkD18rsiKHcFGo2SBhEjGGo00Cbdq0EOUTu6k1Q47evHMLFAhdFK3T2gESB4NfMaAL+6gHS7ouI6SbyOCeAZIGT8e3ggM7MlIsNBrLUeDLEwZG8DjHGItY9KcJG/YxbjZ24b9/IzWDduR8VIUG6XCIrGwQd2jlH8GXmrsq+3KkQr81Tj6Z4/QIa4mcgqSKBr8nCzf31GQhhWgj143VwZtPuHUaAbSsZ4ISbo1PoISUaHymWh1J6qjjrzsvfOdeiKHihA8nLe4ggnV8nrQ5EusA+DzmL/Ti+K+2cc277nC0J1pFhuZs37xi+eQT6TMyUeE12uyCHlG1SiwG8t0wfv2N/yzdugW2eILZbRDZnEoN46lLoeXrTGRiFi25/6Jue0/iTo1AV7ameK4J2teGIhYROqB06kResWVECWm2mWhMpJ7Am5ij7tho3Ot75wrmgXXWCb962MzmIpJG8VIimtoIRNVtlu7+cxMb4D9KFb/i69cMkb+7R+Vm6c37T0T0R/o6QCY+MP0w29xgbGz5PNcLEh//avz4E1JI+AsbvtHOi8/aZ9F7c2DcTfXDcxoA2suxJjRy8Y4uu6rrKUWhklla5G/hs0rZsuTM9iruFasV+AybXDLN2/YNqSAj4oDzi/lYNwvQm5CTZwklHK/fwNPbfCNkY5C94rvzW+OJJ2mR1rcHCfHVWYW/IQRfE11mZVX2m0HT70rDPVopYSHrmlvuuTk3ky5gXym+/FOKBq+BcE0GiDDGl3C5VFtiREhpW4J7zRux5QnHk5fIVyEAZlidKsNSNLwq73+E6W77kMNDU7KCRH23A9BIJPOpN87oZDVX1eBghiM/qBOzP04fw3C6dxu+W/OQvTwZmxLtod07Y64EbdaeqJtjnd7GihAEW2jj+Wkcfz9WHTw38cNpyLqcU3ap13790qVJO6V27b0OmiEiloMYyYHUwcHs8wQA946ns0XOz7zw3r1goJgJS6il93dAThK2UBzw3DIY4yJGrmscPZesWSqL3a+ElGjZWz6n9idmIN7L7oViR7A+p17zwFnpczz/VsV+vj8DwSBwLsw6Q==
LCG_SERVER_PASSWORD: AgBYXu3YpewbdveXVFDGKBzJe8Gur70LxYSL3kmM7Q65lTU/Q4smpPKhb/bDPntNc4XmNFUfVZ/P5iv2bCdgZB0ccMp6eGZUKKfa7PVJ1/I1hAVwSWYmkD673tbfF7CoGrgtTYw93IaU4t1aWBRiqgapktRawFY1SEIoU6dAx3nrKivyFFJx5akHnm+7vr0GMAtE+H8P/htW+bg1peg5rVYkQwMyeYdefQ+AA/RMDG74XlRGv0EfU6S/LmJ0HF84jb5VNjyJMAD7NssuSUXglpVhfTRwZZAD789/hTgElQnR1JUAIyRHeY6zerU5sXaaS7l+MJGxMMNGsfgOFQ57kzA7Eafumf0ChKfdGl7c4UaEcq6zENSp1Rrzc84WLoghwhvVIhszuGkSE9aLelJJamBMOnm/rlwOuQJJsMAlTiuGO7pewsHKDQXnuZGKOQyAflEX/SNXzUSdYEGonuWGkjWopZrvjO9TwBReFUfsV/ALoDgjA2b7nFpXff0Ffx+EvYtuudDZFw1PobBAHy2aFkMuUP0GxVh14nbmC38VNMiug4+xfl92TxbxRkOkx/tufb2p2QFaglW5TK9I3ysHxiCODhq+YdhlW+gDS+mzBEQZbXd+3TMjh7sE1DGqbloMDhfvJJA6u6C2t6olW3BghU/AS20tOgNC+aIgWzetC6ODOKJQLAq2jjXG+PmUoJDUhs1SbS7uIsIlLIt/Y62b6roVAK81zQPt3w==
template:
metadata:
creationTimestamp: null
name: lcg-secrets
namespace: lcg
---

18
kustomize/service.yaml Normal file
View File

@@ -0,0 +1,18 @@
apiVersion: v1
kind: Service
metadata:
name: lcg
namespace: lcg
labels:
app: lcg
version: v2.0.2
spec:
type: ClusterIP
ports:
- port: 8080
targetPort: 8080
protocol: TCP
name: http
selector:
app: lcg
version: v2.0.2

57
main.go
View File

@@ -21,12 +21,20 @@ import (
"github.com/direct-dev-ru/linux-command-gpt/serve"
"github.com/direct-dev-ru/linux-command-gpt/validation"
"github.com/urfave/cli/v2"
"gopkg.in/yaml.v3"
)
//go:embed VERSION.txt
var Version string
// используем глобальный экземпляр конфига из пакета config
//go:embed build-conditions.yaml
var BuildConditionsFromYaml string
type buildConditions struct {
NoServe bool `yaml:"no-serve"`
}
var CompileConditions buildConditions
// disableHistory управляет записью/обновлением истории на уровне процесса (флаг имеет приоритет над env)
var disableHistory bool
@@ -46,6 +54,14 @@ const (
)
func main() {
if err := yaml.Unmarshal([]byte(BuildConditionsFromYaml), &CompileConditions); err != nil {
fmt.Println("Error parsing build conditions:", err)
CompileConditions.NoServe = false
}
fmt.Println("Build conditions:", CompileConditions)
_ = colorBlue
gpt.InitBuiltinPrompts("")
@@ -57,7 +73,7 @@ func main() {
app := &cli.App{
Name: "lcg",
Usage: "Linux Command GPT - Генерация Linux команд из описаний",
Usage: config.AppConfig.AppName + " - Генерация Linux команд из описаний",
Version: Version,
Commands: getCommands(),
UsageText: `
@@ -68,7 +84,7 @@ lcg [опции] <описание команды>
lcg --file /path/to/file.txt "хочу вывести все директории с помощью ls"
`,
Description: `
Linux Command GPT - инструмент для генерации Linux команд из описаний на естественном языке.
{{.AppName}} - инструмент для генерации Linux команд из описаний на естественном языке.
Поддерживает чтение частей промпта из файлов и позволяет сохранять, копировать или перегенерировать результаты.
может задавать системный промпт или выбирать из предустановленных промптов.
Переменные окружения:
@@ -159,6 +175,12 @@ Linux Command GPT - инструмент для генерации Linux ком
}
}
if CompileConditions.NoServe {
if len(args) > 1 && args[0] == "serve" {
printColored("❌ Error: serve command is disabled in this build\n", colorRed)
os.Exit(1)
}
}
executeMain(file, system, strings.Join(args, " "), timeout)
return nil
},
@@ -180,7 +202,7 @@ Linux Command GPT - инструмент для генерации Linux ком
}
func getCommands() []*cli.Command {
return []*cli.Command{
commands := []*cli.Command{
{
Name: "update-key",
Aliases: []string{"u"},
@@ -578,7 +600,15 @@ func getCommands() []*cli.Command {
if host == "0.0.0.0" {
browserHost = "localhost"
}
url := fmt.Sprintf("%s://%s:%s", protocol, browserHost, port)
// Учитываем BasePath в URL
basePath := config.AppConfig.Server.BasePath
if basePath == "" || basePath == "/" {
basePath = ""
} else {
basePath = strings.TrimSuffix(basePath, "/")
}
url := fmt.Sprintf("%s://%s:%s%s", protocol, browserHost, port, basePath)
if openBrowser {
printColored("🌍 Открываю браузер...\n", colorGreen)
@@ -596,6 +626,19 @@ func getCommands() []*cli.Command {
},
},
}
if CompileConditions.NoServe {
filteredCommands := []*cli.Command{}
for _, cmd := range commands {
if cmd.Name != "serve" {
filteredCommands = append(filteredCommands, cmd)
}
}
commands = filteredCommands
}
return commands
}
func executeMain(file, system, commandInput string, timeout int) {
@@ -972,7 +1015,6 @@ func showFullConfig() {
type SafeConfig struct {
Cwd string `json:"cwd"`
Host string `json:"host"`
ProxyUrl string `json:"proxy_url"`
Completions string `json:"completions"`
Model string `json:"model"`
Prompt string `json:"prompt"`
@@ -995,7 +1037,6 @@ func showFullConfig() {
safeConfig := SafeConfig{
Cwd: config.AppConfig.Cwd,
Host: config.AppConfig.Host,
ProxyUrl: config.AppConfig.ProxyUrl,
Completions: config.AppConfig.Completions,
Model: config.AppConfig.Model,
Prompt: config.AppConfig.Prompt,
@@ -1024,6 +1065,8 @@ func showFullConfig() {
Validation: config.AppConfig.Validation,
}
safeConfig.Server.Password = "***"
// Выводим JSON с отступами
jsonData, err := json.MarshalIndent(safeConfig, "", " ")
if err != nil {

View File

@@ -1 +0,0 @@
package main

269
serve/auth.go Normal file
View File

@@ -0,0 +1,269 @@
package serve
import (
"crypto/rand"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
"github.com/direct-dev-ru/linux-command-gpt/config"
"github.com/golang-jwt/jwt/v5"
)
// JWTClaims представляет claims для JWT токена
type JWTClaims struct {
Username string `json:"username"`
jwt.RegisteredClaims
}
// AuthRequest представляет запрос на аутентификацию
type AuthRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
// AuthResponse представляет ответ на аутентификацию
type AuthResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Error string `json:"error,omitempty"`
}
// JWTSecretKey генерирует или загружает секретный ключ для JWT
func getJWTSecretKey() ([]byte, error) {
// Пытаемся загрузить из переменной окружения
if secret := os.Getenv("LCG_JWT_SECRET"); secret != "" {
return []byte(secret), nil
}
// Пытаемся загрузить из файла
secretFile := fmt.Sprintf("%s/server/jwt_secret", config.AppConfig.Server.ConfigFolder)
if data, err := os.ReadFile(secretFile); err == nil {
return data, nil
}
// Генерируем новый секретный ключ
secret := make([]byte, 32)
if _, err := rand.Read(secret); err != nil {
return nil, fmt.Errorf("failed to generate JWT secret: %v", err)
}
// Создаем директорию если не существует
if err := os.MkdirAll(fmt.Sprintf("%s/server", config.AppConfig.Server.ConfigFolder), 0755); err != nil {
return nil, fmt.Errorf("failed to create config directory: %v", err)
}
// Сохраняем секретный ключ в файл
if err := os.WriteFile(secretFile, secret, 0600); err != nil {
return nil, fmt.Errorf("failed to save JWT secret: %v", err)
}
return secret, nil
}
// generateJWTToken создает JWT токен для пользователя
func generateJWTToken(username string) (string, error) {
secret, err := getJWTSecretKey()
if err != nil {
return "", err
}
// Создаем claims
claims := JWTClaims{
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), // Токен действителен 24 часа
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "lcg-server",
},
}
// Создаем токен
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(secret)
}
// validateJWTToken проверяет JWT токен
func validateJWTToken(tokenString string) (*JWTClaims, error) {
secret, err := getJWTSecretKey()
if err != nil {
return nil, err
}
// Парсим токен
token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
// Проверяем метод подписи
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return secret, nil
})
if err != nil {
return nil, err
}
// Проверяем валидность токена
if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
return claims, nil
}
return nil, fmt.Errorf("invalid token")
}
// getTokenFromCookie извлекает JWT токен из cookies
func getTokenFromCookie(r *http.Request) (string, error) {
cookie, err := r.Cookie("auth_token")
if err != nil {
return "", err
}
return cookie.Value, nil
}
// setAuthCookie устанавливает HTTP-only cookie с JWT токеном
func setAuthCookie(w http.ResponseWriter, token string) {
cookie := &http.Cookie{
Name: "auth_token",
Domain: config.AppConfig.Server.Domain,
Value: token,
Path: config.AppConfig.Server.CookiePath,
HttpOnly: true,
Secure: config.AppConfig.Server.CookieSecure,
SameSite: http.SameSiteLaxMode, // Более мягкий режим для reverse proxy
MaxAge: config.AppConfig.Server.CookieTTLHours * 60 * 60,
}
// Добавляем домен если указан
if config.AppConfig.Server.Domain != "" {
cookie.Domain = config.AppConfig.Server.Domain
}
http.SetCookie(w, cookie)
}
// clearAuthCookie удаляет cookie с токеном
func clearAuthCookie(w http.ResponseWriter) {
cookie := &http.Cookie{
Name: "auth_token",
Value: "",
Path: config.AppConfig.Server.CookiePath,
HttpOnly: true,
Secure: config.AppConfig.Server.CookieSecure,
SameSite: http.SameSiteLaxMode,
MaxAge: -1, // Удаляем cookie
}
// Добавляем домен если указан
if config.AppConfig.Server.Domain != "" {
cookie.Domain = config.AppConfig.Server.Domain
}
http.SetCookie(w, cookie)
}
// handleLogin обрабатывает запрос на вход
func handleLogin(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req AuthRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
apiJsonResponse(w, AuthResponse{
Success: false,
Error: "Invalid request body",
})
return
}
// Проверяем пароль
if req.Password != config.AppConfig.Server.Password {
apiJsonResponse(w, AuthResponse{
Success: false,
Error: "Неверный пароль",
})
return
}
// Генерируем JWT токен
token, err := generateJWTToken(req.Username)
if err != nil {
apiJsonResponse(w, AuthResponse{
Success: false,
Error: "Failed to generate token",
})
return
}
// Устанавливаем cookie
setAuthCookie(w, token)
apiJsonResponse(w, AuthResponse{
Success: true,
Message: "Успешная авторизация",
})
}
// handleLogout обрабатывает запрос на выход
func handleLogout(w http.ResponseWriter, r *http.Request) {
clearAuthCookie(w)
http.Redirect(w, r, "/login", http.StatusSeeOther)
}
// handleValidateToken обрабатывает проверку валидности токена
func handleValidateToken(w http.ResponseWriter, r *http.Request) {
token, err := getTokenFromCookie(r)
if err != nil {
apiJsonResponse(w, AuthResponse{
Success: false,
Error: "Token not found",
})
return
}
_, err = validateJWTToken(token)
if err != nil {
apiJsonResponse(w, AuthResponse{
Success: false,
Error: "Invalid token",
})
return
}
apiJsonResponse(w, AuthResponse{
Success: true,
Message: "Token is valid",
})
}
// requireAuth middleware проверяет аутентификацию
func requireAuth(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Проверяем, требуется ли аутентификация
if !config.AppConfig.Server.RequireAuth {
next(w, r)
return
}
// Получаем токен из cookie
token, err := getTokenFromCookie(r)
if err != nil {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
// Проверяем валидность токена
_, err = validateJWTToken(token)
if err != nil {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
// Токен валиден, продолжаем
next(w, r)
}
}

263
serve/csrf.go Normal file
View File

@@ -0,0 +1,263 @@
package serve
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"net/http"
"os"
"time"
"github.com/direct-dev-ru/linux-command-gpt/config"
)
// CSRFManager управляет CSRF токенами
type CSRFManager struct {
secretKey []byte
}
// CSRFData содержит данные для CSRF токена
type CSRFData struct {
Token string
Timestamp int64
UserID string
}
// NewCSRFManager создает новый менеджер CSRF
func NewCSRFManager() (*CSRFManager, error) {
secret, err := getCSRFSecretKey()
if err != nil {
return nil, err
}
return &CSRFManager{secretKey: secret}, nil
}
// getCSRFSecretKey получает или генерирует секретный ключ для CSRF
func getCSRFSecretKey() ([]byte, error) {
// Пытаемся загрузить из переменной окружения
if secret := os.Getenv("LCG_CSRF_SECRET"); secret != "" {
return []byte(secret), nil
}
// Пытаемся загрузить из файла
secretFile := fmt.Sprintf("%s/server/csrf_secret", config.AppConfig.Server.ConfigFolder)
if data, err := os.ReadFile(secretFile); err == nil {
return data, nil
}
// Генерируем новый секретный ключ
secret := make([]byte, 32)
if _, err := rand.Read(secret); err != nil {
return nil, fmt.Errorf("failed to generate CSRF secret: %v", err)
}
// Создаем директорию если не существует
if err := os.MkdirAll(fmt.Sprintf("%s/server", config.AppConfig.Server.ConfigFolder), 0755); err != nil {
return nil, fmt.Errorf("failed to create config directory: %v", err)
}
// Сохраняем секретный ключ в файл
if err := os.WriteFile(secretFile, secret, 0600); err != nil {
return nil, fmt.Errorf("failed to save CSRF secret: %v", err)
}
return secret, nil
}
// GenerateToken генерирует CSRF токен для пользователя
func (c *CSRFManager) GenerateToken(userID string) (string, error) {
// Создаем данные токена
data := CSRFData{
Token: generateRandomString(32),
Timestamp: time.Now().Unix(),
UserID: userID,
}
// Создаем подпись
signature := c.createSignature(data)
// Кодируем данные в base64
encodedData := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%d:%s", data.Token, data.Timestamp, data.UserID)))
return fmt.Sprintf("%s.%s", encodedData, signature), nil
}
// ValidateToken проверяет CSRF токен
func (c *CSRFManager) ValidateToken(token, userID string) bool {
// Разделяем токен на данные и подпись
parts := splitToken(token)
if len(parts) != 2 {
return false
}
encodedData, signature := parts[0], parts[1]
// Декодируем данные
dataBytes, err := base64.StdEncoding.DecodeString(encodedData)
if err != nil {
return false
}
// Парсим данные
dataParts := splitString(string(dataBytes), ":")
if len(dataParts) != 3 {
return false
}
tokenValue, timestampStr, tokenUserID := dataParts[0], dataParts[1], dataParts[2]
// Проверяем пользователя
if tokenUserID != userID {
return false
}
// Проверяем время жизни токена (24 часа)
timestamp, err := parseInt64(timestampStr)
if err != nil {
return false
}
if time.Now().Unix()-timestamp > 24*60*60 {
return false
}
// Создаем данные для проверки подписи
data := CSRFData{
Token: tokenValue,
Timestamp: timestamp,
UserID: tokenUserID,
}
// Проверяем подпись
expectedSignature := c.createSignature(data)
return signature == expectedSignature
}
// createSignature создает подпись для данных
func (c *CSRFManager) createSignature(data CSRFData) string {
message := fmt.Sprintf("%s:%d:%s", data.Token, data.Timestamp, data.UserID)
hash := sha256.Sum256(append(c.secretKey, []byte(message)...))
return hex.EncodeToString(hash[:])
}
// getTokenFromCookie извлекает CSRF токен из cookie
func GetCSRFTokenFromCookie(r *http.Request) string {
cookie, err := r.Cookie("csrf_token")
if err != nil {
return ""
}
return cookie.Value
}
// setCSRFCookie устанавливает CSRF токен в cookie
func setCSRFCookie(w http.ResponseWriter, token string) {
cookie := &http.Cookie{
Name: "csrf_token",
Value: token,
Path: config.AppConfig.Server.CookiePath,
HttpOnly: true,
Secure: config.AppConfig.Server.CookieSecure,
SameSite: http.SameSiteLaxMode, // Более мягкий режим для reverse proxy
MaxAge: 1 * 60 * 60,
}
// Добавляем домен если указан
if config.AppConfig.Server.Domain != "" {
cookie.Domain = config.AppConfig.Server.Domain
}
http.SetCookie(w, cookie)
}
// clearCSRFCookie удаляет CSRF cookie
func СlearCSRFCookie(w http.ResponseWriter) {
cookie := &http.Cookie{
Name: "csrf_token",
Value: "",
Path: config.AppConfig.Server.CookiePath,
HttpOnly: true,
Secure: config.AppConfig.Server.CookieSecure,
SameSite: http.SameSiteLaxMode,
MaxAge: -1,
}
// Добавляем домен если указан
if config.AppConfig.Server.Domain != "" {
cookie.Domain = config.AppConfig.Server.Domain
}
http.SetCookie(w, cookie)
}
// generateRandomString генерирует случайную строку
func generateRandomString(length int) string {
bytes := make([]byte, length)
rand.Read(bytes)
return base64.URLEncoding.EncodeToString(bytes)[:length]
}
// splitToken разделяет токен на части
func splitToken(token string) []string {
// Ищем последнюю точку
lastDot := -1
for i := len(token) - 1; i >= 0; i-- {
if token[i] == '.' {
lastDot = i
break
}
}
if lastDot == -1 {
return []string{token}
}
return []string{token[:lastDot], token[lastDot+1:]}
}
// splitString разделяет строку по разделителю
func splitString(s, sep string) []string {
if s == "" {
return []string{}
}
var result []string
start := 0
for i := 0; i < len(s); i++ {
if i+len(sep) <= len(s) && s[i:i+len(sep)] == sep {
result = append(result, s[start:i])
start = i + len(sep)
i += len(sep) - 1
}
}
result = append(result, s[start:])
return result
}
// parseInt64 парсит строку в int64
func parseInt64(s string) (int64, error) {
var result int64
for _, char := range s {
if char < '0' || char > '9' {
return 0, fmt.Errorf("invalid number: %s", s)
}
result = result*10 + int64(char-'0')
}
return result, nil
}
// Глобальный экземпляр CSRF менеджера
var csrfManager *CSRFManager
// InitCSRFManager инициализирует глобальный CSRF менеджер
func InitCSRFManager() error {
var err error
csrfManager, err = NewCSRFManager()
return err
}
// GetCSRFManager возвращает глобальный CSRF менеджер
func GetCSRFManager() *CSRFManager {
return csrfManager
}

View File

@@ -23,6 +23,12 @@ type ExecutePageData struct {
ResultSection template.HTML
VerboseButtons template.HTML
ActionButtons template.HTML
CSRFToken string
ProviderType string
Model string
Host string
BasePath string
AppName string
// Поля конфигурации для валидации
MaxUserMessageLength int
}
@@ -50,7 +56,7 @@ func handleExecutePage(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
// Показываем форму
showExecuteForm(w)
showExecuteForm(w, r)
case http.MethodPost:
// Обрабатываем выполнение
handleExecuteRequest(w, r)
@@ -60,7 +66,25 @@ func handleExecutePage(w http.ResponseWriter, r *http.Request) {
}
// showExecuteForm показывает форму выполнения
func showExecuteForm(w http.ResponseWriter) {
func showExecuteForm(w http.ResponseWriter, r *http.Request) {
// Генерируем CSRF токен
csrfManager := GetCSRFManager()
if csrfManager == nil {
http.Error(w, "CSRF manager not initialized", http.StatusInternalServerError)
return
}
// Получаем сессионный ID
sessionID := getSessionID(r)
csrfToken, err := csrfManager.GenerateToken(sessionID)
if err != nil {
http.Error(w, "Failed to generate CSRF token", http.StatusInternalServerError)
return
}
// Устанавливаем CSRF токен в cookie
setCSRFCookie(w, csrfToken)
// Получаем системные промпты
pm := gpt.NewPromptManager(config.AppConfig.PromptFolder)
@@ -84,6 +108,12 @@ func showExecuteForm(w http.ResponseWriter) {
ResultSection: template.HTML(""),
VerboseButtons: template.HTML(""),
ActionButtons: template.HTML(""),
CSRFToken: csrfToken,
ProviderType: config.AppConfig.ProviderType,
Model: config.AppConfig.Model,
Host: config.AppConfig.Host,
BasePath: getBasePath(),
AppName: config.AppConfig.AppName,
MaxUserMessageLength: config.AppConfig.Validation.MaxUserMessageLength,
}
@@ -194,6 +224,15 @@ func handleExecuteRequest(w http.ResponseWriter, r *http.Request) {
}
}
// Генерируем CSRF токен для результата
csrfManager := GetCSRFManager()
sessionID := getSessionID(r)
csrfToken, err := csrfManager.GenerateToken(sessionID)
if err != nil {
http.Error(w, "Failed to generate CSRF token", http.StatusInternalServerError)
return
}
data := ExecutePageData{
Title: "Результат выполнения",
Header: "Результат выполнения",
@@ -202,6 +241,12 @@ func handleExecuteRequest(w http.ResponseWriter, r *http.Request) {
ResultSection: template.HTML(formatResultSection(result)),
VerboseButtons: template.HTML(formatVerboseButtons(result)),
ActionButtons: template.HTML(formatActionButtons(result)),
CSRFToken: csrfToken,
ProviderType: config.AppConfig.ProviderType,
Model: config.AppConfig.Model,
Host: config.AppConfig.Host,
BasePath: getBasePath(),
AppName: config.AppConfig.AppName,
MaxUserMessageLength: config.AppConfig.Validation.MaxUserMessageLength,
}

View File

@@ -5,6 +5,7 @@ import (
"html/template"
"net/http"
"os"
"sort"
"strconv"
"strings"
@@ -38,9 +39,13 @@ func handleHistoryPage(w http.ResponseWriter, r *http.Request) {
}
data := struct {
Entries []HistoryEntryInfo
Entries []HistoryEntryInfo
BasePath string
AppName string
}{
Entries: historyEntries,
Entries: historyEntries,
BasePath: getBasePath(),
AppName: config.AppConfig.AppName,
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
@@ -54,6 +59,11 @@ func readHistoryEntries() ([]HistoryEntryInfo, error) {
return nil, err
}
// Сортируем записи по времени в убывающем порядке (новые сначала)
sort.Slice(entries, func(i, j int) bool {
return entries[i].Timestamp.After(entries[j].Timestamp)
})
var result []HistoryEntryInfo
for _, entry := range entries {
result = append(result, HistoryEntryInfo{
@@ -74,7 +84,15 @@ func handleDeleteHistoryEntry(w http.ResponseWriter, r *http.Request) {
return
}
indexStr := strings.TrimPrefix(r.URL.Path, "/history/delete/")
// Убираем BasePath из URL перед извлечением индекса
basePath := config.AppConfig.Server.BasePath
var indexStr string
if basePath != "" && basePath != "/" {
basePath = strings.TrimSuffix(basePath, "/")
indexStr = strings.TrimPrefix(r.URL.Path, basePath+"/history/delete/")
} else {
indexStr = strings.TrimPrefix(r.URL.Path, "/history/delete/")
}
index, err := strconv.Atoi(indexStr)
if err != nil {
http.Error(w, "Invalid index", http.StatusBadRequest)
@@ -110,8 +128,15 @@ func handleClearHistory(w http.ResponseWriter, r *http.Request) {
// handleHistoryView обрабатывает просмотр записи истории
func handleHistoryView(w http.ResponseWriter, r *http.Request) {
// Получаем индекс из URL
indexStr := strings.TrimPrefix(r.URL.Path, "/history/view/")
// Получаем индекс из URL, учитывая BasePath
basePath := config.AppConfig.Server.BasePath
var indexStr string
if basePath != "" && basePath != "/" {
basePath = strings.TrimSuffix(basePath, "/")
indexStr = strings.TrimPrefix(r.URL.Path, basePath+"/history/view/")
} else {
indexStr = strings.TrimPrefix(r.URL.Path, "/history/view/")
}
index, err := strconv.Atoi(indexStr)
if err != nil {
http.NotFound(w, r)
@@ -158,12 +183,14 @@ func handleHistoryView(w http.ResponseWriter, r *http.Request) {
Command string
Response string
ExplanationHTML template.HTML
BasePath string
}{
Index: index,
Timestamp: targetEntry.Timestamp.Format("02.01.2006 15:04:05"),
Command: targetEntry.Command,
Response: targetEntry.Response,
ExplanationHTML: template.HTML(explanationSection),
BasePath: getBasePath(),
}
// Парсим и выполняем шаблон

View File

@@ -21,9 +21,20 @@ type HistoryEntry struct {
// read читает записи истории из файла
func Read(historyPath string) ([]HistoryEntry, error) {
data, err := os.ReadFile(historyPath)
if err != nil || len(data) == 0 {
if err != nil {
// Если файл не существует, создаем пустой файл истории
if os.IsNotExist(err) {
emptyHistory := []HistoryEntry{}
if writeErr := Write(historyPath, emptyHistory); writeErr != nil {
return nil, fmt.Errorf("не удалось создать файл истории: %v", writeErr)
}
return emptyHistory, nil
}
return nil, err
}
if len(data) == 0 {
return []HistoryEntry{}, nil
}
var items []HistoryEntry
if err := json.Unmarshal(data, &items); err != nil {
return nil, err

103
serve/login.go Normal file
View File

@@ -0,0 +1,103 @@
package serve
import (
"crypto/sha256"
"encoding/hex"
"html/template"
"net/http"
"github.com/direct-dev-ru/linux-command-gpt/config"
"github.com/direct-dev-ru/linux-command-gpt/serve/templates"
)
// handleLoginPage обрабатывает страницу входа
func handleLoginPage(w http.ResponseWriter, r *http.Request) {
// Если пользователь уже авторизован, перенаправляем на главную
if isAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
// Генерируем CSRF токен
csrfManager := GetCSRFManager()
if csrfManager == nil {
http.Error(w, "CSRF manager not initialized", http.StatusInternalServerError)
return
}
// Для неавторизованных пользователей используем сессионный ID
sessionID := getSessionID(r)
csrfToken, err := csrfManager.GenerateToken(sessionID)
if err != nil {
http.Error(w, "Failed to generate CSRF token", http.StatusInternalServerError)
return
}
// Устанавливаем CSRF токен в cookie
setCSRFCookie(w, csrfToken)
data := LoginPageData{
Title: "Авторизация - LCG",
Message: "",
Error: "",
CSRFToken: csrfToken,
}
if err := RenderLoginPage(w, data); err != nil {
http.Error(w, "Failed to render login page", http.StatusInternalServerError)
return
}
}
// isAuthenticated проверяет, авторизован ли пользователь
func isAuthenticated(r *http.Request) bool {
// Проверяем, требуется ли аутентификация
if !config.AppConfig.Server.RequireAuth {
return true
}
// Получаем токен из cookie
token, err := getTokenFromCookie(r)
if err != nil {
return false
}
// Проверяем валидность токена
_, err = validateJWTToken(token)
return err == nil
}
// LoginPageData представляет данные для страницы входа
type LoginPageData struct {
Title string
Message string
Error string
CSRFToken string
}
// RenderLoginPage рендерит страницу входа
func RenderLoginPage(w http.ResponseWriter, data LoginPageData) error {
tmpl, err := template.New("login").Parse(templates.LoginPageTemplate)
if err != nil {
return err
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
return tmpl.Execute(w, data)
}
// getSessionID получает или создает сессионный ID для пользователя
func getSessionID(r *http.Request) string {
// Пытаемся получить из cookie
if cookie, err := r.Cookie("session_id"); err == nil {
return cookie.Value
}
// Если нет cookie, генерируем новый ID на основе IP и User-Agent
ip := r.RemoteAddr
userAgent := r.Header.Get("User-Agent")
// Создаем простой хеш для сессии
hash := sha256.Sum256([]byte(ip + userAgent))
return hex.EncodeToString(hash[:])[:16]
}

112
serve/middleware.go Normal file
View File

@@ -0,0 +1,112 @@
package serve
import (
"net/http"
"github.com/direct-dev-ru/linux-command-gpt/config"
)
// AuthMiddleware проверяет аутентификацию для всех запросов
func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Проверяем, требуется ли аутентификация
if !config.AppConfig.Server.RequireAuth {
next(w, r)
return
}
// Исключаем страницу входа и API логина из проверки
if r.URL.Path == "/login" || r.URL.Path == "/api/login" || r.URL.Path == "/api/validate-token" {
next(w, r)
return
}
// Проверяем аутентификацию
if !isAuthenticated(r) {
// Для API запросов возвращаем JSON ошибку
if isAPIRequest(r) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"success": false, "error": "Authentication required"}`))
return
}
// Для веб-запросов перенаправляем на страницу входа
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
// Пользователь аутентифицирован, продолжаем
next(w, r)
}
}
// CSRFMiddleware проверяет CSRF токены для POST/PUT/DELETE запросов
func CSRFMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Проверяем только изменяющие запросы
if r.Method == "GET" || r.Method == "HEAD" || r.Method == "OPTIONS" {
next(w, r)
return
}
// Исключаем некоторые API endpoints
if r.URL.Path == "/api/login" || r.URL.Path == "/api/logout" {
next(w, r)
return
}
// Получаем CSRF токен из заголовка или формы
csrfToken := r.Header.Get("X-CSRF-Token")
if csrfToken == "" {
csrfToken = r.FormValue("csrf_token")
}
if csrfToken == "" {
// Для API запросов возвращаем JSON ошибку
if isAPIRequest(r) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusForbidden)
w.Write([]byte(`{"success": false, "error": "CSRF token required"}`))
return
}
// Для веб-запросов возвращаем ошибку
http.Error(w, "CSRF token required", http.StatusForbidden)
return
}
// Получаем сессионный ID
sessionID := getSessionID(r)
// Проверяем CSRF токен
csrfManager := GetCSRFManager()
if csrfManager == nil || !csrfManager.ValidateToken(csrfToken, sessionID) {
// Для API запросов возвращаем JSON ошибку
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
}
// CSRF токен валиден, продолжаем
next(w, r)
}
}
// isAPIRequest проверяет, является ли запрос API запросом
func isAPIRequest(r *http.Request) bool {
path := r.URL.Path
return len(path) >= 4 && path[:4] == "/api"
}
// RequireAuth обертка для requireAuth из auth.go
func RequireAuth(next http.HandlerFunc) http.HandlerFunc {
return requireAuth(next)
}

View File

@@ -90,6 +90,8 @@ func handlePromptsPage(w http.ResponseWriter, r *http.Request) {
MaxSystemPromptLength int
MaxPromptNameLength int
MaxPromptDescLength int
BasePath string
AppName string
}{
Prompts: promptsWithDefault,
VerbosePrompts: verbosePrompts,
@@ -97,6 +99,8 @@ func handlePromptsPage(w http.ResponseWriter, r *http.Request) {
MaxSystemPromptLength: config.AppConfig.Validation.MaxSystemPromptLength,
MaxPromptNameLength: config.AppConfig.Validation.MaxPromptNameLength,
MaxPromptDescLength: config.AppConfig.Validation.MaxPromptDescLength,
BasePath: getBasePath(),
AppName: config.AppConfig.AppName,
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")

View File

@@ -8,18 +8,41 @@ import (
"path/filepath"
"strings"
"time"
"unicode"
"github.com/direct-dev-ru/linux-command-gpt/config"
"github.com/direct-dev-ru/linux-command-gpt/serve/templates"
"github.com/russross/blackfriday/v2"
)
// generateAbbreviation создает аббревиатуру из первых букв слов в названии приложения
func generateAbbreviation(appName string) string {
words := strings.Fields(appName)
var abbreviation strings.Builder
for _, word := range words {
if len(word) > 0 {
// Берем первую букву слова, если это буква
firstRune := []rune(word)[0]
if unicode.IsLetter(firstRune) {
abbreviation.WriteRune(unicode.ToUpper(firstRune))
}
}
}
result := abbreviation.String()
if result == "" {
return "LCG" // Fallback если не удалось сгенерировать аббревиатуру
}
return result
}
// FileInfo содержит информацию о файле
type FileInfo struct {
Name string
Size string
ModTime string
Preview string
Preview template.HTML
Content string // Полное содержимое для поиска
}
@@ -52,13 +75,19 @@ func handleResultsPage(w http.ResponseWriter, r *http.Request) {
}
data := struct {
Files []FileInfo
TotalFiles int
RecentFiles int
Files []FileInfo
TotalFiles int
RecentFiles int
BasePath string
AppName string
AppAbbreviation string
}{
Files: files,
TotalFiles: len(files),
RecentFiles: recentCount,
Files: files,
TotalFiles: len(files),
RecentFiles: recentCount,
BasePath: getBasePath(),
AppName: config.AppConfig.AppName,
AppAbbreviation: generateAbbreviation(config.AppConfig.AppName),
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
@@ -83,44 +112,15 @@ func getResultFiles() ([]FileInfo, error) {
continue
}
// Читаем превью файла (первые 200 символов) и конвертируем Markdown
// Читаем превью файла (первые 200 символов) как обычный текст
preview := ""
fullContent := ""
if content, err := os.ReadFile(filepath.Join(config.AppConfig.ResultFolder, entry.Name())); err == nil {
// Сохраняем полное содержимое для поиска
fullContent = string(content)
// Конвертируем Markdown в HTML для превью
htmlContent := blackfriday.Run(content)
preview = strings.TrimSpace(string(htmlContent))
// Удаляем HTML теги для превью
preview = strings.ReplaceAll(preview, "<h1>", "")
preview = strings.ReplaceAll(preview, "</h1>", "")
preview = strings.ReplaceAll(preview, "<h2>", "")
preview = strings.ReplaceAll(preview, "</h2>", "")
preview = strings.ReplaceAll(preview, "<h3>", "")
preview = strings.ReplaceAll(preview, "</h3>", "")
preview = strings.ReplaceAll(preview, "<p>", "")
preview = strings.ReplaceAll(preview, "</p>", "")
preview = strings.ReplaceAll(preview, "<code>", "")
preview = strings.ReplaceAll(preview, "</code>", "")
preview = strings.ReplaceAll(preview, "<pre>", "")
preview = strings.ReplaceAll(preview, "</pre>", "")
preview = strings.ReplaceAll(preview, "<strong>", "")
preview = strings.ReplaceAll(preview, "</strong>", "")
preview = strings.ReplaceAll(preview, "<em>", "")
preview = strings.ReplaceAll(preview, "</em>", "")
preview = strings.ReplaceAll(preview, "<ul>", "")
preview = strings.ReplaceAll(preview, "</ul>", "")
preview = strings.ReplaceAll(preview, "<li>", "• ")
preview = strings.ReplaceAll(preview, "</li>", "")
preview = strings.ReplaceAll(preview, "<ol>", "")
preview = strings.ReplaceAll(preview, "</ol>", "")
preview = strings.ReplaceAll(preview, "<blockquote>", "")
preview = strings.ReplaceAll(preview, "</blockquote>", "")
preview = strings.ReplaceAll(preview, "<br>", "")
preview = strings.ReplaceAll(preview, "<br/>", "")
preview = strings.ReplaceAll(preview, "<br />", "")
// Берем первые 200 символов как превью
preview = string(content)
// Очищаем от лишних пробелов и переносов
preview = strings.ReplaceAll(preview, "\n", " ")
preview = strings.ReplaceAll(preview, "\r", "")
@@ -136,7 +136,7 @@ func getResultFiles() ([]FileInfo, error) {
Name: entry.Name(),
Size: formatFileSize(info.Size()),
ModTime: info.ModTime().Format("02.01.2006 15:04"),
Preview: preview,
Preview: template.HTML(preview),
Content: fullContent,
})
}

View File

@@ -5,13 +5,49 @@ import (
"fmt"
"net/http"
"os"
"strings"
"github.com/direct-dev-ru/linux-command-gpt/config"
"github.com/direct-dev-ru/linux-command-gpt/ssl"
)
// makePath создает путь с учетом BasePath
func makePath(path string) string {
basePath := config.AppConfig.Server.BasePath
if basePath == "" || basePath == "/" {
return path
}
// Убираем слэш в конце basePath если есть
basePath = strings.TrimSuffix(basePath, "/")
// Убираем слэш в начале path если есть
path = strings.TrimPrefix(path, "/")
// Если path пустой, возвращаем basePath с слэшем в конце
if path == "" {
return basePath + "/"
}
return basePath + "/" + path
}
// getBasePath возвращает BasePath для использования в шаблонах
func getBasePath() string {
basePath := config.AppConfig.Server.BasePath
if basePath == "" || basePath == "/" {
return ""
}
return strings.TrimSuffix(basePath, "/")
}
// StartResultServer запускает HTTP/HTTPS сервер для просмотра сохраненных результатов
func StartResultServer(host, port string) error {
// Инициализируем CSRF менеджер
if err := InitCSRFManager(); err != nil {
return fmt.Errorf("failed to initialize CSRF manager: %v", err)
}
addr := fmt.Sprintf("%s:%s", host, port)
// Проверяем, нужно ли использовать HTTPS
@@ -103,78 +139,116 @@ func registerHTTPSRoutes() {
registerRoutesExceptHome()
// Регистрируем главную страницу с проверкой HTTPS
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.HandleFunc(makePath("/"), func(w http.ResponseWriter, r *http.Request) {
// Проверяем, пришел ли запрос по HTTP (не HTTPS)
if r.TLS == nil {
handleHTTPSRedirect(w, r)
return
}
// Если уже HTTPS, обрабатываем как обычно
handleResultsPage(w, r)
AuthMiddleware(handleResultsPage)(w, r)
})
// Регистрируем главную страницу без слэша в конце для BasePath
basePath := config.AppConfig.Server.BasePath
if basePath != "" && basePath != "/" {
basePath = strings.TrimSuffix(basePath, "/")
http.HandleFunc(basePath, func(w http.ResponseWriter, r *http.Request) {
// Проверяем, пришел ли запрос по HTTP (не HTTPS)
if r.TLS == nil {
handleHTTPSRedirect(w, r)
return
}
// Если уже HTTPS, обрабатываем как обычно
AuthMiddleware(handleResultsPage)(w, r)
})
}
}
// registerRoutesExceptHome регистрирует все маршруты кроме главной страницы
func registerRoutesExceptHome() {
// Страница входа (без аутентификации)
http.HandleFunc(makePath("/login"), handleLoginPage)
// API для аутентификации (без аутентификации)
http.HandleFunc(makePath("/api/login"), handleLogin)
http.HandleFunc(makePath("/api/logout"), handleLogout)
http.HandleFunc(makePath("/api/validate-token"), handleValidateToken)
// Файлы
http.HandleFunc("/file/", handleFileView)
http.HandleFunc("/delete/", handleDeleteFile)
http.HandleFunc(makePath("/file/"), AuthMiddleware(handleFileView))
http.HandleFunc(makePath("/delete/"), AuthMiddleware(handleDeleteFile))
// История запросов
http.HandleFunc("/history", handleHistoryPage)
http.HandleFunc("/history/view/", handleHistoryView)
http.HandleFunc("/history/delete/", handleDeleteHistoryEntry)
http.HandleFunc("/history/clear", handleClearHistory)
http.HandleFunc(makePath("/history"), AuthMiddleware(handleHistoryPage))
http.HandleFunc(makePath("/history/view/"), AuthMiddleware(handleHistoryView))
http.HandleFunc(makePath("/history/delete/"), AuthMiddleware(handleDeleteHistoryEntry))
http.HandleFunc(makePath("/history/clear"), AuthMiddleware(handleClearHistory))
// Управление промптами
http.HandleFunc("/prompts", handlePromptsPage)
http.HandleFunc("/prompts/add", handleAddPrompt)
http.HandleFunc("/prompts/edit/", handleEditPrompt)
http.HandleFunc("/prompts/edit-verbose/", handleEditVerbosePrompt)
http.HandleFunc("/prompts/delete/", handleDeletePrompt)
http.HandleFunc("/prompts/restore/", handleRestorePrompt)
http.HandleFunc("/prompts/restore-verbose/", handleRestoreVerbosePrompt)
http.HandleFunc("/prompts/save-lang", handleSaveLang)
http.HandleFunc(makePath("/prompts"), AuthMiddleware(handlePromptsPage))
http.HandleFunc(makePath("/prompts/add"), AuthMiddleware(handleAddPrompt))
http.HandleFunc(makePath("/prompts/edit/"), AuthMiddleware(handleEditPrompt))
http.HandleFunc(makePath("/prompts/edit-verbose/"), AuthMiddleware(handleEditVerbosePrompt))
http.HandleFunc(makePath("/prompts/delete/"), AuthMiddleware(handleDeletePrompt))
http.HandleFunc(makePath("/prompts/restore/"), AuthMiddleware(handleRestorePrompt))
http.HandleFunc(makePath("/prompts/restore-verbose/"), AuthMiddleware(handleRestoreVerbosePrompt))
http.HandleFunc(makePath("/prompts/save-lang"), AuthMiddleware(handleSaveLang))
// Веб-страница для выполнения запросов
http.HandleFunc("/run", handleExecutePage)
http.HandleFunc(makePath("/run"), AuthMiddleware(CSRFMiddleware(handleExecutePage)))
// API для выполнения запросов
http.HandleFunc("/api/execute", handleExecute)
http.HandleFunc(makePath("/api/execute"), AuthMiddleware(CSRFMiddleware(handleExecute)))
// API для сохранения результатов и истории
http.HandleFunc("/api/save-result", handleSaveResult)
http.HandleFunc("/api/add-to-history", handleAddToHistory)
http.HandleFunc(makePath("/api/save-result"), AuthMiddleware(CSRFMiddleware(handleSaveResult)))
http.HandleFunc(makePath("/api/add-to-history"), AuthMiddleware(CSRFMiddleware(handleAddToHistory)))
}
// registerRoutes регистрирует все маршруты сервера
func registerRoutes() {
// Страница входа (без аутентификации)
http.HandleFunc(makePath("/login"), handleLoginPage)
// API для аутентификации (без аутентификации)
http.HandleFunc(makePath("/api/login"), handleLogin)
http.HandleFunc(makePath("/api/logout"), handleLogout)
http.HandleFunc(makePath("/api/validate-token"), handleValidateToken)
// Главная страница и файлы
http.HandleFunc("/", handleResultsPage)
http.HandleFunc("/file/", handleFileView)
http.HandleFunc("/delete/", handleDeleteFile)
http.HandleFunc(makePath("/"), AuthMiddleware(handleResultsPage))
http.HandleFunc(makePath("/file/"), AuthMiddleware(handleFileView))
http.HandleFunc(makePath("/delete/"), AuthMiddleware(handleDeleteFile))
// История запросов
http.HandleFunc("/history", handleHistoryPage)
http.HandleFunc("/history/view/", handleHistoryView)
http.HandleFunc("/history/delete/", handleDeleteHistoryEntry)
http.HandleFunc("/history/clear", handleClearHistory)
http.HandleFunc(makePath("/history"), AuthMiddleware(handleHistoryPage))
http.HandleFunc(makePath("/history/view/"), AuthMiddleware(handleHistoryView))
http.HandleFunc(makePath("/history/delete/"), AuthMiddleware(handleDeleteHistoryEntry))
http.HandleFunc(makePath("/history/clear"), AuthMiddleware(handleClearHistory))
// Управление промптами
http.HandleFunc("/prompts", handlePromptsPage)
http.HandleFunc("/prompts/add", handleAddPrompt)
http.HandleFunc("/prompts/edit/", handleEditPrompt)
http.HandleFunc("/prompts/edit-verbose/", handleEditVerbosePrompt)
http.HandleFunc("/prompts/delete/", handleDeletePrompt)
http.HandleFunc("/prompts/restore/", handleRestorePrompt)
http.HandleFunc("/prompts/restore-verbose/", handleRestoreVerbosePrompt)
http.HandleFunc("/prompts/save-lang", handleSaveLang)
http.HandleFunc(makePath("/prompts"), AuthMiddleware(handlePromptsPage))
http.HandleFunc(makePath("/prompts/add"), AuthMiddleware(handleAddPrompt))
http.HandleFunc(makePath("/prompts/edit/"), AuthMiddleware(handleEditPrompt))
http.HandleFunc(makePath("/prompts/edit-verbose/"), AuthMiddleware(handleEditVerbosePrompt))
http.HandleFunc(makePath("/prompts/delete/"), AuthMiddleware(handleDeletePrompt))
http.HandleFunc(makePath("/prompts/restore/"), AuthMiddleware(handleRestorePrompt))
http.HandleFunc(makePath("/prompts/restore-verbose/"), AuthMiddleware(handleRestoreVerbosePrompt))
http.HandleFunc(makePath("/prompts/save-lang"), AuthMiddleware(handleSaveLang))
// Веб-страница для выполнения запросов
http.HandleFunc("/run", handleExecutePage)
http.HandleFunc(makePath("/run"), AuthMiddleware(CSRFMiddleware(handleExecutePage)))
// API для выполнения запросов
http.HandleFunc("/api/execute", handleExecute)
http.HandleFunc(makePath("/api/execute"), AuthMiddleware(CSRFMiddleware(handleExecute)))
// API для сохранения результатов и истории
http.HandleFunc("/api/save-result", handleSaveResult)
http.HandleFunc("/api/add-to-history", handleAddToHistory)
http.HandleFunc(makePath("/api/save-result"), AuthMiddleware(CSRFMiddleware(handleSaveResult)))
http.HandleFunc(makePath("/api/add-to-history"), AuthMiddleware(CSRFMiddleware(handleAddToHistory)))
// Регистрируем главную страницу без слэша в конце для BasePath
basePath := config.AppConfig.Server.BasePath
if basePath != "" && basePath != "/" {
basePath = strings.TrimSuffix(basePath, "/")
http.HandleFunc(basePath, AuthMiddleware(handleResultsPage))
}
}

View File

@@ -79,6 +79,14 @@ var ExecutePageCSSTemplate = template.Must(template.New("execute_css").Parse(`
opacity: 0.9;
font-size: 1.1em;
}
.config-info {
margin: 5px 0 0 0 !important;
opacity: 0.7 !important;
font-size: 0.9em !important;
font-style: italic;
color: rgba(255, 255, 255, 0.8);
}
.content {
padding: 30px;
}

View File

@@ -8,7 +8,7 @@ var ExecutePageTemplate = template.Must(template.New("execute").Parse(`<!DOCTYPE
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Title}} - Linux Command GPT</title>
<title>{{.Title}} - {{.AppName}}</title>
<style>
{{template "execute_css" .}}
</style>
@@ -17,16 +17,19 @@ var ExecutePageTemplate = template.Must(template.New("execute").Parse(`<!DOCTYPE
<div class="container">
<div class="header">
<h1>{{.Header}}</h1>
<p>Выполнение запросов к Linux Command GPT через веб-интерфейс</p>
<p>Выполнение запросов к {{.AppName}} через веб-интерфейс</p>
<p class="config-info">({{.ProviderType}}{{.Model}}{{.Host}})</p>
</div>
<div class="content">
<div class="nav-buttons">
<a href="/" class="nav-btn">🏠 Главная</a>
<a href="/history" class="nav-btn">📝 История</a>
<a href="/prompts" class="nav-btn">⚙️ Промпты</a>
<a href="{{.BasePath}}/" class="nav-btn">🏠 Главная</a>
<a href="{{.BasePath}}/history" class="nav-btn">📝 История</a>
<a href="{{.BasePath}}/prompts" class="nav-btn">⚙️ Промпты</a>
</div>
<form method="POST" id="executeForm">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<div class="form-section">
<div class="form-group">
<label for="system_id">🤖 Системный промпт:</label>

View File

@@ -89,6 +89,7 @@ var ExecutePageScriptsTemplate = template.Must(template.New("execute_scripts").P
function saveResult() {
const resultDataField = document.getElementById('resultData');
const prompt = document.getElementById('prompt').value;
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
if (!resultDataField.value || !prompt.trim()) {
alert('Нет данных для сохранения');
@@ -104,10 +105,11 @@ var ExecutePageScriptsTemplate = template.Must(template.New("execute_scripts").P
model: resultData.model || 'Unknown'
};
fetch('/api/save-result', {
fetch('{{.BasePath}}/api/save-result', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken,
},
body: JSON.stringify(requestData)
})
@@ -134,6 +136,7 @@ var ExecutePageScriptsTemplate = template.Must(template.New("execute_scripts").P
const resultDataField = document.getElementById('resultData');
const prompt = document.getElementById('prompt').value;
const systemId = document.getElementById('system_id').value;
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
if (!resultDataField.value || !prompt.trim()) {
alert('Нет данных для сохранения в историю');
@@ -152,10 +155,11 @@ var ExecutePageScriptsTemplate = template.Must(template.New("execute_scripts").P
system: systemName
};
fetch('/api/add-to-history', {
fetch('{{.BasePath}}/api/add-to-history', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken,
},
body: JSON.stringify(requestData)
})

View File

@@ -155,13 +155,13 @@ const HistoryPageTemplate = `
<div class="container">
<div class="header">
<h1>📝 История запросов</h1>
<p>Управление историей запросов Linux Command GPT</p>
<p>Управление историей запросов {{.AppName}}</p>
</div>
<div class="content">
<div class="nav-buttons">
<a href="/" class="nav-btn">🏠 Главная</a>
<a href="/run" class="nav-btn">🚀 Выполнение</a>
<a href="/prompts" class="nav-btn">⚙️ Промпты</a>
<a href="{{.BasePath}}/" class="nav-btn">🏠 Главная</a>
<a href="{{.BasePath}}/run" class="nav-btn">🚀 Выполнение</a>
<a href="{{.BasePath}}/prompts" class="nav-btn">⚙️ Промпты</a>
<button class="nav-btn clear-btn" onclick="clearHistory()">🗑️ Очистить всю историю</button>
</div>
@@ -197,12 +197,12 @@ const HistoryPageTemplate = `
<script>
function viewHistoryEntry(index) {
window.location.href = '/history/view/' + index;
window.location.href = '{{.BasePath}}/history/view/' + index;
}
function deleteHistoryEntry(index) {
if (confirm('Вы уверены, что хотите удалить запись #' + index + '?')) {
fetch('/history/delete/' + index, {
fetch('{{.BasePath}}/history/delete/' + index, {
method: 'DELETE'
})
.then(response => {
@@ -221,7 +221,7 @@ const HistoryPageTemplate = `
function clearHistory() {
if (confirm('Вы уверены, что хотите очистить всю историю?\\n\\nЭто действие нельзя отменить.')) {
fetch('/history/clear', {
fetch('{{.BasePath}}/history/clear', {
method: 'DELETE'
})
.then(response => {

View File

@@ -224,7 +224,7 @@ const HistoryViewTemplate = `
<div class="container">
<div class="header">
<h1>📝 Запись #{{.Index}}</h1>
<a href="/history" class="back-btn">← Назад к истории</a>
<a href="{{.BasePath}}/history" class="back-btn">← Назад к истории</a>
</div>
<div class="content">
<div class="history-meta">
@@ -249,7 +249,7 @@ const HistoryViewTemplate = `
{{.ExplanationHTML}}
<div class="actions">
<a href="/history" class="action-btn">📝 К истории</a>
<a href="{{.BasePath}}/history" class="action-btn">📝 К истории</a>
<button class="action-btn delete-btn" onclick="deleteHistoryEntry({{.Index}})">🗑️ Удалить запись</button>
</div>
</div>
@@ -258,12 +258,12 @@ const HistoryViewTemplate = `
<script>
function deleteHistoryEntry(index) {
if (confirm('Вы уверены, что хотите удалить запись #' + index + '?')) {
fetch('/history/delete/' + index, {
fetch('{{.BasePath}}/history/delete/' + index, {
method: 'DELETE'
})
.then(response => {
if (response.ok) {
window.location.href = '/history';
window.location.href = '{{.BasePath}}/history';
} else {
alert('Ошибка при удалении записи');
}

323
serve/templates/login.go Normal file
View File

@@ -0,0 +1,323 @@
package templates
// LoginPageTemplate шаблон страницы авторизации
const LoginPageTemplate = `
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Title}}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(-45deg, #667eea, #764ba2, #f093fb, #f5576c, #4facfe, #00f2fe);
background-size: 400% 400%;
animation: gradientShift 15s ease infinite;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
}
@keyframes gradientShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* Плавающие элементы */
.floating-elements {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
z-index: 1;
}
.floating-element {
position: absolute;
opacity: 0.1;
animation: float 20s infinite linear;
}
.floating-element:nth-child(1) { left: 10%; animation-delay: 0s; animation-duration: 25s; }
.floating-element:nth-child(2) { left: 20%; animation-delay: 2s; animation-duration: 30s; }
.floating-element:nth-child(3) { left: 30%; animation-delay: 4s; animation-duration: 20s; }
.floating-element:nth-child(4) { left: 40%; animation-delay: 6s; animation-duration: 35s; }
.floating-element:nth-child(5) { left: 50%; animation-delay: 8s; animation-duration: 28s; }
.floating-element:nth-child(6) { left: 60%; animation-delay: 10s; animation-duration: 22s; }
.floating-element:nth-child(7) { left: 70%; animation-delay: 12s; animation-duration: 32s; }
.floating-element:nth-child(8) { left: 80%; animation-delay: 14s; animation-duration: 26s; }
.floating-element:nth-child(9) { left: 90%; animation-delay: 16s; animation-duration: 24s; }
@keyframes float {
0% { transform: translateY(100vh) rotate(0deg); opacity: 0; }
10% { opacity: 0.1; }
90% { opacity: 0.1; }
100% { transform: translateY(-100px) rotate(360deg); opacity: 0; }
}
.lock-icon {
font-size: 2rem;
color: rgba(255, 255, 255, 0.3);
}
.key-icon {
font-size: 1.5rem;
color: rgba(255, 255, 255, 0.3);
}
.shield-icon {
font-size: 1.8rem;
color: rgba(255, 255, 255, 0.3);
}
.star-icon {
font-size: 1.2rem;
color: rgba(255, 255, 255, 0.3);
}
.login-container {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
padding: 2rem;
border-radius: 20px;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.2);
width: 100%;
max-width: 400px;
position: relative;
z-index: 10;
border: 1px solid rgba(255, 255, 255, 0.3);
}
.login-header {
text-align: center;
margin-bottom: 2rem;
}
.login-header h1 {
color: #333;
font-size: 1.8rem;
margin-bottom: 0.5rem;
}
.login-header p {
color: #666;
font-size: 0.9rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
color: #333;
font-weight: 500;
}
.form-group input {
width: 100%;
padding: 0.75rem;
border: 2px solid #e1e5e9;
border-radius: 10px;
font-size: 1rem;
transition: all 0.3s ease;
background: rgba(255, 255, 255, 0.9);
}
.form-group input:focus {
outline: none;
border-color: #667eea;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.2);
background: rgba(255, 255, 255, 1);
}
.login-button {
width: 100%;
padding: 0.75rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 10px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.login-button::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s;
}
.login-button:hover {
transform: translateY(-3px);
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
}
.login-button:hover::before {
left: 100%;
}
.login-button:active {
transform: translateY(-1px);
}
.message {
margin-top: 1rem;
padding: 0.75rem;
border-radius: 5px;
text-align: center;
}
.message.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.message.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.loading {
display: none;
text-align: center;
margin-top: 1rem;
}
.spinner {
border: 2px solid #f3f3f3;
border-top: 2px solid #667eea;
border-radius: 50%;
width: 20px;
height: 20px;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<!-- Плавающие элементы фона -->
<div class="floating-elements">
<div class="floating-element lock-icon">🔒</div>
<div class="floating-element key-icon">🔑</div>
<div class="floating-element shield-icon">🛡️</div>
<div class="floating-element star-icon">⭐</div>
<div class="floating-element lock-icon">🔐</div>
<div class="floating-element key-icon">🗝️</div>
<div class="floating-element shield-icon">🔒</div>
<div class="floating-element star-icon">✨</div>
<div class="floating-element lock-icon">🔒</div>
</div>
<div class="login-container">
<div class="login-header">
<h1>🔐 Авторизация</h1>
<p>Войдите в систему для доступа к LCG</p>
</div>
<form id="loginForm">
<input type="hidden" id="csrf_token" name="csrf_token" value="{{.CSRFToken}}">
<div class="form-group">
<label for="username">Имя пользователя:</label>
<input type="text" id="username" name="username" required placeholder="Введите имя пользователя">
</div>
<div class="form-group">
<label for="password">Пароль:</label>
<input type="password" id="password" name="password" required placeholder="Введите пароль">
</div>
<button type="submit" class="login-button">Войти</button>
</form>
<div class="loading" id="loading">
<div class="spinner"></div>
<p>Проверка авторизации...</p>
</div>
<div id="message"></div>
</div>
<script>
document.getElementById('loginForm').addEventListener('submit', async function(e) {
e.preventDefault();
const form = e.target;
const formData = new FormData(form);
const username = formData.get('username');
const password = formData.get('password');
// Показываем загрузку
document.getElementById('loading').style.display = 'block';
document.getElementById('message').innerHTML = '';
try {
const csrfToken = document.getElementById('csrf_token').value;
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
username: username,
password: password,
csrf_token: csrfToken
})
});
const data = await response.json();
if (data.success) {
// Успешная авторизация, перенаправляем на главную страницу
window.location.href = '/';
} else {
// Ошибка авторизации
showMessage(data.error || 'Ошибка авторизации', 'error');
}
} catch (error) {
showMessage('Ошибка соединения с сервером', 'error');
} finally {
document.getElementById('loading').style.display = 'none';
}
});
function showMessage(text, type) {
const messageDiv = document.getElementById('message');
messageDiv.innerHTML = '<div class="message ' + type + '">' + text + '</div>';
}
</script>
</body>
</html>`

View File

@@ -235,13 +235,13 @@ const PromptsPageTemplate = `
<div class="container">
<div class="header">
<h1>⚙️ Системные промпты</h1>
<p>Управление системными промптами Linux Command GPT</p>
<p>Управление системными промптами {{.AppName}}</p>
</div>
<div class="content">
<div class="nav-buttons">
<a href="/" class="nav-btn">🏠 Главная</a>
<a href="/run" class="nav-btn">🚀 Выполнение</a>
<a href="/history" class="nav-btn">📝 История</a>
<a href="{{.BasePath}}/" class="nav-btn">🏠 Главная</a>
<a href="{{.BasePath}}/run" class="nav-btn">🚀 Выполнение</a>
<a href="{{.BasePath}}/history" class="nav-btn">📝 История</a>
<button class="nav-btn add-btn" onclick="showAddForm()"> Добавить промпт</button>
<div class="lang-switcher">
<button class="lang-btn {{if eq .Lang "ru"}}active{{end}}" onclick="switchLang('ru')">🇷🇺 RU</button>
@@ -391,7 +391,7 @@ const PromptsPageTemplate = `
function saveCurrentPrompts(lang) {
// Отправляем запрос для сохранения текущих промптов с новым языком
fetch('/prompts/save-lang', {
fetch('{{.BasePath}}/prompts/save-lang', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -417,7 +417,7 @@ const PromptsPageTemplate = `
function deletePrompt(id) {
if (confirm('Вы уверены, что хотите удалить промпт #' + id + '?')) {
fetch('/prompts/delete/' + id, {
fetch('{{.BasePath}}/prompts/delete/' + id, {
method: 'DELETE'
})
.then(response => {
@@ -467,10 +467,10 @@ const PromptsPageTemplate = `
let url, method;
if (isVerbosePrompt) {
url = '/prompts/edit-verbose/' + id;
url = '{{.BasePath}}/prompts/edit-verbose/' + id;
method = 'PUT';
} else {
url = id ? '/prompts/edit/' + id : '/prompts/add';
url = id ? '{{.BasePath}}/prompts/edit/' + id : '{{.BasePath}}/prompts/add';
method = id ? 'PUT' : 'POST';
}
@@ -501,7 +501,7 @@ const PromptsPageTemplate = `
// Функция восстановления системного промпта
function restorePrompt(id) {
if (confirm('Восстановить промпт к значению по умолчанию?')) {
fetch('/prompts/restore/' + id, {
fetch('{{.BasePath}}/prompts/restore/' + id, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -526,7 +526,7 @@ const PromptsPageTemplate = `
// Функция восстановления verbose промпта
function restoreVerbosePrompt(mode) {
if (confirm('Восстановить промпт к значению по умолчанию?')) {
fetch('/prompts/restore-verbose/' + mode, {
fetch('{{.BasePath}}/prompts/restore-verbose/' + mode, {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View File

@@ -7,7 +7,7 @@ const ResultsPageTemplate = `
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LCG Results - Linux Command GPT</title>
<title>{{.AppAbbreviation}} Результаты - {{.AppName}}</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
@@ -182,15 +182,15 @@ const ResultsPageTemplate = `
<body>
<div class="container">
<div class="header">
<h1>🚀 LCG Results</h1>
<p>Просмотр сохраненных результатов Linux Command GPT</p>
<h1>🚀 {{.AppAbbreviation}} - {{.AppName}}</h1>
<p>Просмотр сохраненных результатов {{.AppName}}</p>
</div>
<div class="content">
<div class="nav-buttons">
<button class="nav-btn" onclick="location.reload()">🔄 Обновить</button>
<a href="/run" class="nav-btn">🚀 Выполнение</a>
<a href="/history" class="nav-btn">📝 История</a>
<a href="/prompts" class="nav-btn">⚙️ Промпты</a>
<a href="{{.BasePath}}/run" class="nav-btn">🚀 Выполнение</a>
<a href="{{.BasePath}}/history" class="nav-btn">📝 История</a>
<a href="{{.BasePath}}/prompts" class="nav-btn">⚙️ Промпты</a>
</div>
<!-- Поиск -->
@@ -218,7 +218,7 @@ const ResultsPageTemplate = `
<div class="file-actions">
<button class="delete-btn" onclick="deleteFile('{{.Name}}')" title="Удалить файл">🗑️</button>
</div>
<div class="file-card-content" onclick="window.location.href='/file/{{.Name}}'">
<div class="file-card-content" onclick="window.location.href='{{$.BasePath}}/file/{{.Name}}'">
<div class="file-name">{{.Name}}</div>
<div class="file-info">
📅 {{.ModTime}} | 📏 {{.Size}}
@@ -240,7 +240,7 @@ const ResultsPageTemplate = `
<script>
function deleteFile(filename) {
if (confirm('Вы уверены, что хотите удалить файл "' + filename + '"?\\n\\nЭто действие нельзя отменить.')) {
fetch('/delete/' + encodeURIComponent(filename), {
fetch('{{.BasePath}}/delete/' + encodeURIComponent(filename), {
method: 'DELETE'
})
.then(response => {

View File

@@ -0,0 +1,16 @@
#! /usr/bin/bash
VERSION=$1
if [ -z "$VERSION" ]; then
VERSION=latest
fi
docker pull kuznetcovay/lcg:"${VERSION}"
docker run -p 8080:8080 \
-e LCG_PROVIDER=proxy \
-e LCG_HOST=https://direct-dev.ru \
-e LCG_MODEL=GigaChat-2-Max \
-e LCG_JWT_TOKEN="$(go-ansible-vault --key "$(cat ~/.config/gak)" \
-i ~/.config/jwt.direct-dev.ru get -m JWT_TOKEN -q)" \
kuznetcovay/lcg:"${VERSION}"

158
test_csrf.sh Executable file
View File

@@ -0,0 +1,158 @@
#!/bin/bash
# 🛡️ CSRF Protection Test Script
# Тестирует CSRF защиту LCG приложения
echo "🛡️ Тестирование CSRF защиты LCG"
echo "=================================="
# Цвета для вывода
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Функция для вывода результатов
print_result() {
local test_name="$1"
local status="$2"
local message="$3"
if [ "$status" = "PASS" ]; then
echo -e "${GREEN}$test_name: PASS${NC} - $message"
elif [ "$status" = "FAIL" ]; then
echo -e "${RED}$test_name: FAIL${NC} - $message"
else
echo -e "${YELLOW}⚠️ $test_name: $status${NC} - $message"
fi
}
# Проверяем, запущен ли сервер
echo -e "${BLUE}🔍 Проверяем доступность сервера...${NC}"
if ! curl -s http://localhost:8080/login > /dev/null 2>&1; then
echo -e "${RED}❌ Сервер не доступен на localhost:8080${NC}"
echo "Запустите сервер командой: LCG_SERVER_REQUIRE_AUTH=true ./lcg serve -p 8080"
exit 1
fi
echo -e "${GREEN}✅ Сервер доступен${NC}"
# Тест 1: Попытка выполнения команды без CSRF токена
echo -e "\n${BLUE}🧪 Тест 1: Выполнение команды без CSRF токена${NC}"
response=$(curl -s -w "%{http_code}" -X POST http://localhost:8080/api/execute \
-H "Content-Type: application/json" \
-d '{"prompt":"whoami","system_id":"1"}' \
-o /dev/null)
if [ "$response" = "403" ]; then
print_result "CSRF защита /api/execute" "PASS" "Запрос заблокирован (403 Forbidden)"
else
print_result "CSRF защита /api/execute" "FAIL" "Запрос прошел (HTTP $response)"
fi
# Тест 2: Попытка сохранения результата без CSRF токена
echo -e "\n${BLUE}🧪 Тест 2: Сохранение результата без CSRF токена${NC}"
response=$(curl -s -w "%{http_code}" -X POST http://localhost:8080/api/save-result \
-H "Content-Type: application/json" \
-d '{"result":"test result","command":"test command"}' \
-o /dev/null)
if [ "$response" = "403" ]; then
print_result "CSRF защита /api/save-result" "PASS" "Запрос заблокирован (403 Forbidden)"
else
print_result "CSRF защита /api/save-result" "FAIL" "Запрос прошел (HTTP $response)"
fi
# Тест 3: Попытка добавления в историю без CSRF токена
echo -e "\n${BLUE}🧪 Тест 3: Добавление в историю без CSRF токена${NC}"
response=$(curl -s -w "%{http_code}" -X POST http://localhost:8080/api/add-to-history \
-H "Content-Type: application/json" \
-d '{"prompt":"test prompt","result":"test result"}' \
-o /dev/null)
if [ "$response" = "403" ]; then
print_result "CSRF защита /api/add-to-history" "PASS" "Запрос заблокирован (403 Forbidden)"
else
print_result "CSRF защита /api/add-to-history" "FAIL" "Запрос прошел (HTTP $response)"
fi
# Тест 4: Проверка GET запросов (должны работать)
echo -e "\n${BLUE}🧪 Тест 4: GET запросы (должны работать)${NC}"
response=$(curl -s -w "%{http_code}" http://localhost:8080/login -o /dev/null)
if [ "$response" = "200" ]; then
print_result "GET запросы" "PASS" "GET запросы работают (HTTP $response)"
else
print_result "GET запросы" "FAIL" "GET запросы не работают (HTTP $response)"
fi
# Тест 5: Проверка наличия CSRF токена на странице входа
echo -e "\n${BLUE}🧪 Тест 5: Наличие CSRF токена на странице входа${NC}"
csrf_token=$(curl -s http://localhost:8080/login | grep -o 'name="csrf_token"[^>]*value="[^"]*"' | sed 's/.*value="\([^"]*\)".*/\1/')
if [ -n "$csrf_token" ]; then
print_result "CSRF токен на странице входа" "PASS" "Токен найден: ${csrf_token:0:20}..."
else
print_result "CSRF токен на странице входа" "FAIL" "Токен не найден"
fi
# Тест 6: Попытка атаки с поддельным CSRF токеном
echo -e "\n${BLUE}🧪 Тест 6: Атака с поддельным CSRF токеном${NC}"
response=$(curl -s -w "%{http_code}" -X POST http://localhost:8080/api/execute \
-H "Content-Type: application/json" \
-H "X-CSRF-Token: fake_token" \
-d '{"prompt":"whoami","system_id":"1"}' \
-o /dev/null)
if [ "$response" = "403" ]; then
print_result "CSRF защита от поддельного токена" "PASS" "Поддельный токен заблокирован (403 Forbidden)"
else
print_result "CSRF защита от поддельного токена" "FAIL" "Поддельный токен принят (HTTP $response)"
fi
# Итоговый отчет
echo -e "\n${BLUE}📊 Итоговый отчет:${NC}"
echo "=================================="
# Подсчитываем результаты
total_tests=6
passed_tests=0
# Проверяем каждый тест
if curl -s -w "%{http_code}" -X POST http://localhost:8080/api/execute -H "Content-Type: application/json" -d '{"prompt":"test"}' -o /dev/null | grep -q "403"; then
((passed_tests++))
fi
if curl -s -w "%{http_code}" -X POST http://localhost:8080/api/save-result -H "Content-Type: application/json" -d '{"result":"test"}' -o /dev/null | grep -q "403"; then
((passed_tests++))
fi
if curl -s -w "%{http_code}" -X POST http://localhost:8080/api/add-to-history -H "Content-Type: application/json" -d '{"prompt":"test"}' -o /dev/null | grep -q "403"; then
((passed_tests++))
fi
if curl -s -w "%{http_code}" http://localhost:8080/login -o /dev/null | grep -q "200"; then
((passed_tests++))
fi
if curl -s http://localhost:8080/login | grep -q 'name="csrf_token"'; then
((passed_tests++))
fi
if curl -s -w "%{http_code}" -X POST http://localhost:8080/api/execute -H "Content-Type: application/json" -H "X-CSRF-Token: fake" -d '{"prompt":"test"}' -o /dev/null | grep -q "403"; then
((passed_tests++))
fi
echo -e "Пройдено тестов: ${GREEN}$passed_tests${NC} из ${BLUE}$total_tests${NC}"
if [ $passed_tests -eq $total_tests ]; then
echo -e "${GREEN}🎉 Все тесты пройдены! CSRF защита работает корректно.${NC}"
exit 0
elif [ $passed_tests -ge 4 ]; then
echo -e "${YELLOW}⚠️ Большинство тестов пройдено, но есть проблемы с CSRF защитой.${NC}"
exit 1
else
echo -e "${RED}❌ Критические проблемы с CSRF защитой!${NC}"
exit 2
fi