mirror of
https://github.com/Direct-Dev-Ru/go-lcg.git
synced 2025-11-15 17:20:00 +00:00
Исправления в ветке auth-feature
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,3 +17,4 @@ gpt_results
|
||||
shell-code/jwt.admin.token
|
||||
run.sh
|
||||
lcg_history.json
|
||||
deploy/0.create_sealed_secrets.sh
|
||||
|
||||
@@ -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
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"djlint.showInstallError": false
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
v2.0.1
|
||||
v2.0.2
|
||||
|
||||
1
build-conditions.yaml
Normal file
1
build-conditions.yaml
Normal file
@@ -0,0 +1 @@
|
||||
no-serve: false
|
||||
58
build.sh
Executable file
58
build.sh
Executable 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
|
||||
@@ -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")
|
||||
|
||||
@@ -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
29
deploy/.goreleaser.yaml
Normal 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 }}"
|
||||
21
deploy/0.create_sealed_secrets.example.sh
Normal file
21
deploy/0.create_sealed_secrets.example.sh
Normal 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
7
deploy/0.namespace.yaml
Normal 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
46
deploy/1.configmap.yaml
Normal 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"
|
||||
12
deploy/2.gitrepository.yaml
Normal file
12
deploy/2.gitrepository.yaml
Normal 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
|
||||
19
deploy/3.lcg-kustomization.yaml
Normal file
19
deploy/3.lcg-kustomization.yaml
Normal 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
114
deploy/4.build-binaries.sh
Executable 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
12
deploy/4.pvc.yaml
Normal 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
183
deploy/5.build-docker.sh
Executable 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
202
deploy/6.full-build.sh
Executable 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
39
deploy/Dockerfile
Normal 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
1
deploy/VERSION.txt
Normal file
@@ -0,0 +1 @@
|
||||
v2.0.2
|
||||
95
deploy/deployment.tmpl.yaml
Normal file
95
deploy/deployment.tmpl.yaml
Normal 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
42
deploy/hpa.yaml
Normal 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
|
||||
64
deploy/ingress-route.tmpl.yaml
Normal file
64
deploy/ingress-route.tmpl.yaml
Normal 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"
|
||||
25
deploy/kustomization.tmpl.yaml
Normal file
25
deploy/kustomization.tmpl.yaml
Normal 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
18
deploy/service.tmpl.yaml
Normal 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
231
docs/CSRF_TESTING_GUIDE.md
Normal 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
258
docs/REVERSE_PROXY_GUIDE.md
Normal 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}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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 1–5)
|
||||
|
||||
Промпты автоматически выбираются в зависимости от операционной системы:
|
||||
|
||||
**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 6–8)
|
||||
|
||||
| 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
6
go.mod
@@ -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
2
go.sum
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
262
gpt/builtin_prompts_windows.yaml
Normal file
262
gpt/builtin_prompts_windows.yaml
Normal 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. **Исторический контекст**: Эволюция и история разработки
|
||||
|
||||
Предоставьте максимальную техническую глубину с системными инсайтами, примерами кода и архитектурным пониманием.
|
||||
@@ -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
|
||||
|
||||
@@ -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
46
kustomize/configmap.yaml
Normal 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
95
kustomize/deployment.yaml
Normal 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
|
||||
64
kustomize/ingress-route.yaml
Normal file
64
kustomize/ingress-route.yaml
Normal 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"
|
||||
25
kustomize/kustomization.yaml
Normal file
25
kustomize/kustomization.yaml
Normal 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
18
kustomize/secret.yaml
Normal 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
18
kustomize/service.yaml
Normal 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
57
main.go
@@ -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 {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
package main
|
||||
269
serve/auth.go
Normal file
269
serve/auth.go
Normal 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
263
serve/csrf.go
Normal 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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
// Парсим и выполняем шаблон
|
||||
|
||||
@@ -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
103
serve/login.go
Normal 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
112
serve/middleware.go
Normal 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)
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
152
serve/serve.go
152
serve/serve.go
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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
323
serve/templates/login.go
Normal 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>`
|
||||
@@ -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',
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
16
shell-code/docker-proxy-max.sh
Normal file
16
shell-code/docker-proxy-max.sh
Normal 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
158
test_csrf.sh
Executable 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
|
||||
Reference in New Issue
Block a user