mirror of
https://github.com/Direct-Dev-Ru/go-lcg.git
synced 2025-11-16 01:29:55 +00:00
Merged branch 'auth-feature' into main while building v2.0.2
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,3 +17,4 @@ gpt_results
|
|||||||
shell-code/jwt.admin.token
|
shell-code/jwt.admin.token
|
||||||
run.sh
|
run.sh
|
||||||
lcg_history.json
|
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"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -20,9 +21,20 @@ type HistoryEntry struct {
|
|||||||
|
|
||||||
func read(historyPath string) ([]HistoryEntry, error) {
|
func read(historyPath string) ([]HistoryEntry, error) {
|
||||||
data, err := os.ReadFile(historyPath)
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if len(data) == 0 {
|
||||||
|
return []HistoryEntry{}, nil
|
||||||
|
}
|
||||||
var items []HistoryEntry
|
var items []HistoryEntry
|
||||||
if err := json.Unmarshal(data, &items); err != nil {
|
if err := json.Unmarshal(data, &items); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -50,6 +62,12 @@ func ShowHistory(historyPath string, printColored func(string, string), colorYel
|
|||||||
printColored("📝 История пуста\n", colorYellow)
|
printColored("📝 История пуста\n", colorYellow)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Сортируем записи по времени в убывающем порядке (новые сначала)
|
||||||
|
sort.Slice(items, func(i, j int) bool {
|
||||||
|
return items[i].Timestamp.After(items[j].Timestamp)
|
||||||
|
})
|
||||||
|
|
||||||
printColored("📝 История (из файла):\n", colorYellow)
|
printColored("📝 История (из файла):\n", colorYellow)
|
||||||
for _, h := range items {
|
for _, h := range items {
|
||||||
ts := h.Timestamp.Format("2006-01-02 15:04:05")
|
ts := h.Timestamp.Format("2006-01-02 15:04:05")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package config
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -11,10 +12,12 @@ type Config struct {
|
|||||||
Cwd string
|
Cwd string
|
||||||
Host string
|
Host string
|
||||||
ProxyUrl string
|
ProxyUrl string
|
||||||
|
AppName string
|
||||||
Completions string
|
Completions string
|
||||||
Model string
|
Model string
|
||||||
Prompt string
|
Prompt string
|
||||||
ApiKeyFile string
|
ApiKeyFile string
|
||||||
|
ConfigFolder string
|
||||||
ResultFolder string
|
ResultFolder string
|
||||||
PromptFolder string
|
PromptFolder string
|
||||||
ProviderType string
|
ProviderType string
|
||||||
@@ -41,10 +44,19 @@ type MainFlags struct {
|
|||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
Port string
|
Port string
|
||||||
Host string
|
Host string
|
||||||
|
HealthUrl string
|
||||||
|
ProxyUrl string
|
||||||
|
BasePath string
|
||||||
ConfigFolder string
|
ConfigFolder string
|
||||||
AllowHTTP bool
|
AllowHTTP bool
|
||||||
SSLCertFile string
|
SSLCertFile string
|
||||||
SSLKeyFile string
|
SSLKeyFile string
|
||||||
|
RequireAuth bool
|
||||||
|
Password string
|
||||||
|
Domain string
|
||||||
|
CookieSecure bool
|
||||||
|
CookiePath string
|
||||||
|
CookieTTLHours int
|
||||||
}
|
}
|
||||||
|
|
||||||
type ValidationConfig struct {
|
type ValidationConfig struct {
|
||||||
@@ -87,12 +99,7 @@ func getServerAllowHTTP() bool {
|
|||||||
|
|
||||||
func isSecureHost(host string) bool {
|
func isSecureHost(host string) bool {
|
||||||
secureHosts := []string{"localhost", "127.0.0.1", "::1"}
|
secureHosts := []string{"localhost", "127.0.0.1", "::1"}
|
||||||
for _, secureHost := range secureHosts {
|
return slices.Contains(secureHosts, host)
|
||||||
if host == secureHost {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load() Config {
|
func Load() Config {
|
||||||
@@ -102,14 +109,21 @@ func Load() Config {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
homedir = cwd
|
homedir = cwd
|
||||||
}
|
}
|
||||||
os.MkdirAll(path.Join(homedir, ".config", "lcg", "gpt_results"), 0755)
|
privateResultsDir := path.Join(homedir, ".config", "lcg", "gpt_results")
|
||||||
resultFolder := getEnv("LCG_RESULT_FOLDER", 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)
|
privatePromptsDir := path.Join(homedir, ".config", "lcg", "gpt_sys_prompts")
|
||||||
promptFolder := getEnv("LCG_PROMPT_FOLDER", 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{
|
return Config{
|
||||||
Cwd: cwd,
|
Cwd: cwd,
|
||||||
|
AppName: getEnv("LCG_APP_NAME", "Linux Command GPT"),
|
||||||
Host: getEnv("LCG_HOST", "http://192.168.87.108:11434/"),
|
Host: getEnv("LCG_HOST", "http://192.168.87.108:11434/"),
|
||||||
ProxyUrl: getEnv("LCG_PROXY_URL", "/api/v1/protected/sberchat/chat"),
|
ProxyUrl: getEnv("LCG_PROXY_URL", "/api/v1/protected/sberchat/chat"),
|
||||||
Completions: getEnv("LCG_COMPLETIONS_PATH", "api/chat"),
|
Completions: getEnv("LCG_COMPLETIONS_PATH", "api/chat"),
|
||||||
@@ -118,6 +132,7 @@ func Load() Config {
|
|||||||
ApiKeyFile: getEnv("LCG_API_KEY_FILE", ".openai_api_key"),
|
ApiKeyFile: getEnv("LCG_API_KEY_FILE", ".openai_api_key"),
|
||||||
ResultFolder: resultFolder,
|
ResultFolder: resultFolder,
|
||||||
PromptFolder: promptFolder,
|
PromptFolder: promptFolder,
|
||||||
|
ConfigFolder: configFolder,
|
||||||
ProviderType: getEnv("LCG_PROVIDER", "ollama"),
|
ProviderType: getEnv("LCG_PROVIDER", "ollama"),
|
||||||
JwtToken: getEnv("LCG_JWT_TOKEN", ""),
|
JwtToken: getEnv("LCG_JWT_TOKEN", ""),
|
||||||
PromptID: getEnv("LCG_PROMPT_ID", "1"),
|
PromptID: getEnv("LCG_PROMPT_ID", "1"),
|
||||||
@@ -132,6 +147,15 @@ func Load() Config {
|
|||||||
AllowHTTP: getServerAllowHTTP(),
|
AllowHTTP: getServerAllowHTTP(),
|
||||||
SSLCertFile: getEnv("LCG_SERVER_SSL_CERT_FILE", ""),
|
SSLCertFile: getEnv("LCG_SERVER_SSL_CERT_FILE", ""),
|
||||||
SSLKeyFile: getEnv("LCG_SERVER_SSL_KEY_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{
|
Validation: ValidationConfig{
|
||||||
MaxSystemPromptLength: getEnvInt("LCG_MAX_SYSTEM_PROMPT_LENGTH", 2000),
|
MaxSystemPromptLength: getEnvInt("LCG_MAX_SYSTEM_PROMPT_LENGTH", 2000),
|
||||||
@@ -162,6 +186,24 @@ func isAllowExecutionEnabled() bool {
|
|||||||
return vLower == "1" || vLower == "true"
|
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
|
var AppConfig Config
|
||||||
|
|
||||||
func init() {
|
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)ничего:
|
Действия: (c)копировать, (s)сохранить, (r)перегенерировать, (e)выполнить, (v|vv|vvv)подробно, (n)ничего:
|
||||||
```
|
```
|
||||||
|
|
||||||
### Что нового в 2.0.1
|
### Что нового в 3.0.0
|
||||||
|
|
||||||
- Улучшена мобильная версия веб‑интерфейса: корректные размеры кнопок, шрифтов и отступов; адаптивная верстка
|
- **Аутентификация**: Добавлена система аутентификации с JWT токенами и HTTP-only cookies
|
||||||
- Учитывается `prefers-reduced-motion` для снижения анимаций, если это задано в системе
|
- **CSRF защита**: Полная защита от CSRF атак с токенами и middleware
|
||||||
- Добавлен REST эндпоинт `POST /execute` (только через curl) — см. подробности и примеры в `API_GUIDE.md`
|
- **Безопасность**: Улучшенная безопасность с проверкой токенов и сессий
|
||||||
|
- **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_ALLOW_EXECUTION` | пусто | Если `1`/`true` — включает возможность выполнения команд через опцию `(e)` в меню действий. |
|
||||||
| `LCG_SERVER_PORT` | `8080` | Порт для HTTP сервера просмотра результатов. |
|
| `LCG_SERVER_PORT` | `8080` | Порт для HTTP сервера просмотра результатов. |
|
||||||
| `LCG_SERVER_HOST` | `localhost` | Хост для 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_HOST=http://localhost:8080
|
||||||
export LCG_MODEL=GigaChat-2
|
export LCG_MODEL=GigaChat-2
|
||||||
export LCG_JWT_TOKEN=your_jwt_token_here
|
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`)
|
- `--port, -p` — порт сервера (по умолчанию из `LCG_SERVER_PORT`)
|
||||||
- `--host, -H` — хост сервера (по умолчанию из `LCG_SERVER_HOST`)
|
- `--host, -H` — хост сервера (по умолчанию из `LCG_SERVER_HOST`)
|
||||||
- `--browser, -b` — открыть браузер автоматически после старта
|
- `--browser, -b` — открыть браузер автоматически после старта
|
||||||
|
- `--require-auth` — включить аутентификацию (переопределяет `LCG_SERVER_REQUIRE_AUTH`)
|
||||||
|
- `--password` — пароль для аутентификации (переопределяет `LCG_SERVER_PASSWORD`)
|
||||||
|
|
||||||
### Подробные объяснения (v/vv/vvv)
|
### Подробные объяснения (v/vv/vvv)
|
||||||
|
|
||||||
@@ -196,6 +220,39 @@ lcg [глобальные опции] <описание команды>
|
|||||||
- Не вставляйте секреты в запросы. Перед выполнением (`e`) проверяйте команду вручную.
|
- Не вставляйте секреты в запросы. Перед выполнением (`e`) проверяйте команду вручную.
|
||||||
- Для структуры API см. `API_CONTRACT.md` (эндпоинты и форматы запросов/ответов).
|
- Для структуры 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)
|
### Встроенные промпты (ID 1–5)
|
||||||
|
|
||||||
|
Промпты автоматически выбираются в зависимости от операционной системы:
|
||||||
|
|
||||||
|
**Linux/Unix системы:**
|
||||||
| ID | Name | Описание |
|
| ID | Name | Описание |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 1 | linux-command | «Ответь только Linux‑командой, без форматирования и объяснений». |
|
| 1 | linux-command | «Ответь только Linux‑командой, без форматирования и объяснений». |
|
||||||
@@ -218,6 +278,15 @@ lcg [глобальные опции] <описание команды>
|
|||||||
| 4 | linux-command-verbose | Команда с подробными объяснениями флагов и альтернатив. |
|
| 4 | linux-command-verbose | Команда с подробными объяснениями флагов и альтернатив. |
|
||||||
| 5 | linux-command-simple | Простые команды, избегать сложных опций. |
|
| 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 6–8)
|
||||||
|
|
||||||
| ID | Name | Описание |
|
| ID | Name | Описание |
|
||||||
@@ -275,6 +344,12 @@ lcg serve
|
|||||||
- **Современный дизайн** — адаптивный интерфейс с карточками файлов
|
- **Современный дизайн** — адаптивный интерфейс с карточками файлов
|
||||||
- **Сортировка** — файлы отсортированы по дате изменения (новые сверху)
|
- **Сортировка** — файлы отсортированы по дате изменения (новые сверху)
|
||||||
- **Превью содержимого** — первые 200 символов каждого файла
|
- **Превью содержимого** — первые 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 --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` просто уведомят об этом.
|
- Для `ollama`/`proxy` API‑ключ не нужен; команды `update-key`/`delete-key` просто уведомят об этом.
|
||||||
- HTTP сервер не запускается: проверьте, что порт свободен, используйте `--port` для смены порта.
|
- HTTP сервер не запускается: проверьте, что порт свободен, используйте `--port` для смены порта.
|
||||||
- Веб-интерфейс не отображает файлы: убедитесь, что в `LCG_RESULT_FOLDER` есть `.md` файлы.
|
- Веб-интерфейс не отображает файлы: убедитесь, что в `LCG_RESULT_FOLDER` есть `.md` файлы.
|
||||||
|
- **Аутентификация не работает**: проверьте `LCG_SERVER_REQUIRE_AUTH=true` и правильность пароля.
|
||||||
|
- **CSRF ошибки**: убедитесь, что токены передаются в заголовках `X-CSRF-Token`.
|
||||||
|
- **Cookies не сохраняются**: проверьте настройки `LCG_DOMAIN` и `LCG_COOKIE_PATH` для reverse proxy.
|
||||||
|
- **Kubernetes деплой не работает**: проверьте права доступа к кластеру и наличие всех манифестов.
|
||||||
|
|
||||||
## JSON‑история запросов
|
## JSON‑история запросов
|
||||||
|
|
||||||
@@ -408,17 +495,148 @@ lcg history list
|
|||||||
|
|
||||||
## Доступ к локальному API
|
## Доступ к локальному 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
|
```bash
|
||||||
# Запустить сервер
|
# Запустить сервер
|
||||||
lcg serve
|
lcg serve
|
||||||
|
|
||||||
# Выполнить запрос
|
# Выполнить запрос (без аутентификации)
|
||||||
curl -X POST http://localhost:8080/execute \
|
curl -X POST http://localhost:8080/api/execute \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-A curl \
|
-A curl \
|
||||||
-d '{"prompt": "create directory test", "verbose": "vv"}'
|
-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`.
|
Подробности и примеры: `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
|
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 github.com/atotto/clipboard v0.1.4
|
||||||
|
|
||||||
require gopkg.in/yaml.v3 v3.0.1
|
require gopkg.in/yaml.v3 v3.0.1
|
||||||
|
|
||||||
|
require github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 //indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.5 //indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0
|
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/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 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
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 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
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=
|
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package gpt
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
@@ -9,6 +10,9 @@ import (
|
|||||||
//go:embed builtin_prompts.yaml
|
//go:embed builtin_prompts.yaml
|
||||||
var builtinPromptsYAML string
|
var builtinPromptsYAML string
|
||||||
|
|
||||||
|
//go:embed builtin_prompts_windows.yaml
|
||||||
|
var builtinPromptsWindowsYAML string
|
||||||
|
|
||||||
var builtinPrompts string
|
var builtinPrompts string
|
||||||
|
|
||||||
// BuiltinPromptsData структура для YAML файла
|
// BuiltinPromptsData структура для YAML файла
|
||||||
@@ -117,7 +121,12 @@ func GetBuiltinPromptByIDAndLanguage(id int, lang string) *SystemPrompt {
|
|||||||
func InitBuiltinPrompts(embeddedBuiltinPromptsYAML string) {
|
func InitBuiltinPrompts(embeddedBuiltinPromptsYAML string) {
|
||||||
// Используем встроенный YAML, если переданный параметр пустой
|
// Используем встроенный YAML, если переданный параметр пустой
|
||||||
if embeddedBuiltinPromptsYAML == "" {
|
if embeddedBuiltinPromptsYAML == "" {
|
||||||
|
// Выбираем промпты в зависимости от операционной системы
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
builtinPrompts = builtinPromptsWindowsYAML
|
||||||
|
} else {
|
||||||
builtinPrompts = builtinPromptsYAML
|
builtinPrompts = builtinPromptsYAML
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
builtinPrompts = embeddedBuiltinPromptsYAML
|
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"
|
pm.Language = "ru"
|
||||||
|
|
||||||
// Загружаем все встроенные промпты из YAML на русском языке
|
// Загружаем все встроенные промпты из YAML на русском языке
|
||||||
|
// Функция GetBuiltinPromptsByLanguage уже учитывает операционную систему
|
||||||
pm.Prompts = GetBuiltinPromptsByLanguage("ru")
|
pm.Prompts = GetBuiltinPromptsByLanguage("ru")
|
||||||
|
|
||||||
// Сохраняем все промпты в файл
|
// Сохраняем все промпты в файл
|
||||||
@@ -65,40 +66,8 @@ func (pm *PromptManager) createInitialPromptsFile() {
|
|||||||
|
|
||||||
// loadDefaultPrompts загружает предустановленные промпты
|
// loadDefaultPrompts загружает предустановленные промпты
|
||||||
func (pm *PromptManager) LoadDefaultPrompts() {
|
func (pm *PromptManager) LoadDefaultPrompts() {
|
||||||
defaultPrompts := []SystemPrompt{
|
// Используем встроенные промпты, которые автоматически выбираются по ОС
|
||||||
{
|
pm.Prompts = GetBuiltinPromptsByLanguage("en")
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadAllPrompts загружает все промпты из файла sys_prompts
|
// loadAllPrompts загружает все промпты из файла sys_prompts
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Provider интерфейс для работы с разными LLM провайдерами
|
// Provider интерфейс для работы с разными LLM провайдерами
|
||||||
@@ -112,7 +114,7 @@ func (p *ProxyAPIProvider) Chat(messages []Chat) (string, error) {
|
|||||||
return "", fmt.Errorf("ошибка маршалинга запроса: %w", err)
|
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 {
|
if err != nil {
|
||||||
return "", fmt.Errorf("ошибка создания запроса: %w", err)
|
return "", fmt.Errorf("ошибка создания запроса: %w", err)
|
||||||
}
|
}
|
||||||
@@ -155,7 +157,7 @@ func (p *ProxyAPIProvider) Chat(messages []Chat) (string, error) {
|
|||||||
|
|
||||||
// Health для ProxyAPIProvider
|
// Health для ProxyAPIProvider
|
||||||
func (p *ProxyAPIProvider) Health() error {
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("ошибка создания health check запроса: %w", err)
|
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/serve"
|
||||||
"github.com/direct-dev-ru/linux-command-gpt/validation"
|
"github.com/direct-dev-ru/linux-command-gpt/validation"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed VERSION.txt
|
//go:embed VERSION.txt
|
||||||
var Version string
|
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)
|
// disableHistory управляет записью/обновлением истории на уровне процесса (флаг имеет приоритет над env)
|
||||||
var disableHistory bool
|
var disableHistory bool
|
||||||
@@ -46,6 +54,14 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
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
|
_ = colorBlue
|
||||||
|
|
||||||
gpt.InitBuiltinPrompts("")
|
gpt.InitBuiltinPrompts("")
|
||||||
@@ -57,7 +73,7 @@ func main() {
|
|||||||
|
|
||||||
app := &cli.App{
|
app := &cli.App{
|
||||||
Name: "lcg",
|
Name: "lcg",
|
||||||
Usage: "Linux Command GPT - Генерация Linux команд из описаний",
|
Usage: config.AppConfig.AppName + " - Генерация Linux команд из описаний",
|
||||||
Version: Version,
|
Version: Version,
|
||||||
Commands: getCommands(),
|
Commands: getCommands(),
|
||||||
UsageText: `
|
UsageText: `
|
||||||
@@ -68,7 +84,7 @@ lcg [опции] <описание команды>
|
|||||||
lcg --file /path/to/file.txt "хочу вывести все директории с помощью ls"
|
lcg --file /path/to/file.txt "хочу вывести все директории с помощью ls"
|
||||||
`,
|
`,
|
||||||
Description: `
|
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)
|
executeMain(file, system, strings.Join(args, " "), timeout)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
@@ -180,7 +202,7 @@ Linux Command GPT - инструмент для генерации Linux ком
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getCommands() []*cli.Command {
|
func getCommands() []*cli.Command {
|
||||||
return []*cli.Command{
|
commands := []*cli.Command{
|
||||||
{
|
{
|
||||||
Name: "update-key",
|
Name: "update-key",
|
||||||
Aliases: []string{"u"},
|
Aliases: []string{"u"},
|
||||||
@@ -578,7 +600,15 @@ func getCommands() []*cli.Command {
|
|||||||
if host == "0.0.0.0" {
|
if host == "0.0.0.0" {
|
||||||
browserHost = "localhost"
|
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 {
|
if openBrowser {
|
||||||
printColored("🌍 Открываю браузер...\n", colorGreen)
|
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) {
|
func executeMain(file, system, commandInput string, timeout int) {
|
||||||
@@ -972,7 +1015,6 @@ func showFullConfig() {
|
|||||||
type SafeConfig struct {
|
type SafeConfig struct {
|
||||||
Cwd string `json:"cwd"`
|
Cwd string `json:"cwd"`
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
ProxyUrl string `json:"proxy_url"`
|
|
||||||
Completions string `json:"completions"`
|
Completions string `json:"completions"`
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
Prompt string `json:"prompt"`
|
Prompt string `json:"prompt"`
|
||||||
@@ -995,7 +1037,6 @@ func showFullConfig() {
|
|||||||
safeConfig := SafeConfig{
|
safeConfig := SafeConfig{
|
||||||
Cwd: config.AppConfig.Cwd,
|
Cwd: config.AppConfig.Cwd,
|
||||||
Host: config.AppConfig.Host,
|
Host: config.AppConfig.Host,
|
||||||
ProxyUrl: config.AppConfig.ProxyUrl,
|
|
||||||
Completions: config.AppConfig.Completions,
|
Completions: config.AppConfig.Completions,
|
||||||
Model: config.AppConfig.Model,
|
Model: config.AppConfig.Model,
|
||||||
Prompt: config.AppConfig.Prompt,
|
Prompt: config.AppConfig.Prompt,
|
||||||
@@ -1024,6 +1065,8 @@ func showFullConfig() {
|
|||||||
Validation: config.AppConfig.Validation,
|
Validation: config.AppConfig.Validation,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
safeConfig.Server.Password = "***"
|
||||||
|
|
||||||
// Выводим JSON с отступами
|
// Выводим JSON с отступами
|
||||||
jsonData, err := json.MarshalIndent(safeConfig, "", " ")
|
jsonData, err := json.MarshalIndent(safeConfig, "", " ")
|
||||||
if err != nil {
|
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
|
ResultSection template.HTML
|
||||||
VerboseButtons template.HTML
|
VerboseButtons template.HTML
|
||||||
ActionButtons template.HTML
|
ActionButtons template.HTML
|
||||||
|
CSRFToken string
|
||||||
|
ProviderType string
|
||||||
|
Model string
|
||||||
|
Host string
|
||||||
|
BasePath string
|
||||||
|
AppName string
|
||||||
// Поля конфигурации для валидации
|
// Поля конфигурации для валидации
|
||||||
MaxUserMessageLength int
|
MaxUserMessageLength int
|
||||||
}
|
}
|
||||||
@@ -50,7 +56,7 @@ func handleExecutePage(w http.ResponseWriter, r *http.Request) {
|
|||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
// Показываем форму
|
// Показываем форму
|
||||||
showExecuteForm(w)
|
showExecuteForm(w, r)
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
// Обрабатываем выполнение
|
// Обрабатываем выполнение
|
||||||
handleExecuteRequest(w, r)
|
handleExecuteRequest(w, r)
|
||||||
@@ -60,7 +66,25 @@ func handleExecutePage(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// showExecuteForm показывает форму выполнения
|
// 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)
|
pm := gpt.NewPromptManager(config.AppConfig.PromptFolder)
|
||||||
|
|
||||||
@@ -84,6 +108,12 @@ func showExecuteForm(w http.ResponseWriter) {
|
|||||||
ResultSection: template.HTML(""),
|
ResultSection: template.HTML(""),
|
||||||
VerboseButtons: template.HTML(""),
|
VerboseButtons: template.HTML(""),
|
||||||
ActionButtons: 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,
|
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{
|
data := ExecutePageData{
|
||||||
Title: "Результат выполнения",
|
Title: "Результат выполнения",
|
||||||
Header: "Результат выполнения",
|
Header: "Результат выполнения",
|
||||||
@@ -202,6 +241,12 @@ func handleExecuteRequest(w http.ResponseWriter, r *http.Request) {
|
|||||||
ResultSection: template.HTML(formatResultSection(result)),
|
ResultSection: template.HTML(formatResultSection(result)),
|
||||||
VerboseButtons: template.HTML(formatVerboseButtons(result)),
|
VerboseButtons: template.HTML(formatVerboseButtons(result)),
|
||||||
ActionButtons: template.HTML(formatActionButtons(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,
|
MaxUserMessageLength: config.AppConfig.Validation.MaxUserMessageLength,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -39,8 +40,12 @@ func handleHistoryPage(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
data := struct {
|
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")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
@@ -54,6 +59,11 @@ func readHistoryEntries() ([]HistoryEntryInfo, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Сортируем записи по времени в убывающем порядке (новые сначала)
|
||||||
|
sort.Slice(entries, func(i, j int) bool {
|
||||||
|
return entries[i].Timestamp.After(entries[j].Timestamp)
|
||||||
|
})
|
||||||
|
|
||||||
var result []HistoryEntryInfo
|
var result []HistoryEntryInfo
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
result = append(result, HistoryEntryInfo{
|
result = append(result, HistoryEntryInfo{
|
||||||
@@ -74,7 +84,15 @@ func handleDeleteHistoryEntry(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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)
|
index, err := strconv.Atoi(indexStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Invalid index", http.StatusBadRequest)
|
http.Error(w, "Invalid index", http.StatusBadRequest)
|
||||||
@@ -110,8 +128,15 @@ func handleClearHistory(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// handleHistoryView обрабатывает просмотр записи истории
|
// handleHistoryView обрабатывает просмотр записи истории
|
||||||
func handleHistoryView(w http.ResponseWriter, r *http.Request) {
|
func handleHistoryView(w http.ResponseWriter, r *http.Request) {
|
||||||
// Получаем индекс из URL
|
// Получаем индекс из URL, учитывая BasePath
|
||||||
indexStr := strings.TrimPrefix(r.URL.Path, "/history/view/")
|
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)
|
index, err := strconv.Atoi(indexStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
@@ -158,12 +183,14 @@ func handleHistoryView(w http.ResponseWriter, r *http.Request) {
|
|||||||
Command string
|
Command string
|
||||||
Response string
|
Response string
|
||||||
ExplanationHTML template.HTML
|
ExplanationHTML template.HTML
|
||||||
|
BasePath string
|
||||||
}{
|
}{
|
||||||
Index: index,
|
Index: index,
|
||||||
Timestamp: targetEntry.Timestamp.Format("02.01.2006 15:04:05"),
|
Timestamp: targetEntry.Timestamp.Format("02.01.2006 15:04:05"),
|
||||||
Command: targetEntry.Command,
|
Command: targetEntry.Command,
|
||||||
Response: targetEntry.Response,
|
Response: targetEntry.Response,
|
||||||
ExplanationHTML: template.HTML(explanationSection),
|
ExplanationHTML: template.HTML(explanationSection),
|
||||||
|
BasePath: getBasePath(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Парсим и выполняем шаблон
|
// Парсим и выполняем шаблон
|
||||||
|
|||||||
@@ -21,9 +21,20 @@ type HistoryEntry struct {
|
|||||||
// read читает записи истории из файла
|
// read читает записи истории из файла
|
||||||
func Read(historyPath string) ([]HistoryEntry, error) {
|
func Read(historyPath string) ([]HistoryEntry, error) {
|
||||||
data, err := os.ReadFile(historyPath)
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if len(data) == 0 {
|
||||||
|
return []HistoryEntry{}, nil
|
||||||
|
}
|
||||||
var items []HistoryEntry
|
var items []HistoryEntry
|
||||||
if err := json.Unmarshal(data, &items); err != nil {
|
if err := json.Unmarshal(data, &items); err != nil {
|
||||||
return nil, err
|
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
|
MaxSystemPromptLength int
|
||||||
MaxPromptNameLength int
|
MaxPromptNameLength int
|
||||||
MaxPromptDescLength int
|
MaxPromptDescLength int
|
||||||
|
BasePath string
|
||||||
|
AppName string
|
||||||
}{
|
}{
|
||||||
Prompts: promptsWithDefault,
|
Prompts: promptsWithDefault,
|
||||||
VerbosePrompts: verbosePrompts,
|
VerbosePrompts: verbosePrompts,
|
||||||
@@ -97,6 +99,8 @@ func handlePromptsPage(w http.ResponseWriter, r *http.Request) {
|
|||||||
MaxSystemPromptLength: config.AppConfig.Validation.MaxSystemPromptLength,
|
MaxSystemPromptLength: config.AppConfig.Validation.MaxSystemPromptLength,
|
||||||
MaxPromptNameLength: config.AppConfig.Validation.MaxPromptNameLength,
|
MaxPromptNameLength: config.AppConfig.Validation.MaxPromptNameLength,
|
||||||
MaxPromptDescLength: config.AppConfig.Validation.MaxPromptDescLength,
|
MaxPromptDescLength: config.AppConfig.Validation.MaxPromptDescLength,
|
||||||
|
BasePath: getBasePath(),
|
||||||
|
AppName: config.AppConfig.AppName,
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
|||||||
@@ -8,18 +8,41 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||||
"github.com/direct-dev-ru/linux-command-gpt/serve/templates"
|
"github.com/direct-dev-ru/linux-command-gpt/serve/templates"
|
||||||
"github.com/russross/blackfriday/v2"
|
"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 содержит информацию о файле
|
// FileInfo содержит информацию о файле
|
||||||
type FileInfo struct {
|
type FileInfo struct {
|
||||||
Name string
|
Name string
|
||||||
Size string
|
Size string
|
||||||
ModTime string
|
ModTime string
|
||||||
Preview string
|
Preview template.HTML
|
||||||
Content string // Полное содержимое для поиска
|
Content string // Полное содержимое для поиска
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,10 +78,16 @@ func handleResultsPage(w http.ResponseWriter, r *http.Request) {
|
|||||||
Files []FileInfo
|
Files []FileInfo
|
||||||
TotalFiles int
|
TotalFiles int
|
||||||
RecentFiles int
|
RecentFiles int
|
||||||
|
BasePath string
|
||||||
|
AppName string
|
||||||
|
AppAbbreviation string
|
||||||
}{
|
}{
|
||||||
Files: files,
|
Files: files,
|
||||||
TotalFiles: len(files),
|
TotalFiles: len(files),
|
||||||
RecentFiles: recentCount,
|
RecentFiles: recentCount,
|
||||||
|
BasePath: getBasePath(),
|
||||||
|
AppName: config.AppConfig.AppName,
|
||||||
|
AppAbbreviation: generateAbbreviation(config.AppConfig.AppName),
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
@@ -83,44 +112,15 @@ func getResultFiles() ([]FileInfo, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Читаем превью файла (первые 200 символов) и конвертируем Markdown
|
// Читаем превью файла (первые 200 символов) как обычный текст
|
||||||
preview := ""
|
preview := ""
|
||||||
fullContent := ""
|
fullContent := ""
|
||||||
if content, err := os.ReadFile(filepath.Join(config.AppConfig.ResultFolder, entry.Name())); err == nil {
|
if content, err := os.ReadFile(filepath.Join(config.AppConfig.ResultFolder, entry.Name())); err == nil {
|
||||||
// Сохраняем полное содержимое для поиска
|
// Сохраняем полное содержимое для поиска
|
||||||
fullContent = string(content)
|
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, "\n", " ")
|
||||||
preview = strings.ReplaceAll(preview, "\r", "")
|
preview = strings.ReplaceAll(preview, "\r", "")
|
||||||
@@ -136,7 +136,7 @@ func getResultFiles() ([]FileInfo, error) {
|
|||||||
Name: entry.Name(),
|
Name: entry.Name(),
|
||||||
Size: formatFileSize(info.Size()),
|
Size: formatFileSize(info.Size()),
|
||||||
ModTime: info.ModTime().Format("02.01.2006 15:04"),
|
ModTime: info.ModTime().Format("02.01.2006 15:04"),
|
||||||
Preview: preview,
|
Preview: template.HTML(preview),
|
||||||
Content: fullContent,
|
Content: fullContent,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
152
serve/serve.go
152
serve/serve.go
@@ -5,13 +5,49 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||||
"github.com/direct-dev-ru/linux-command-gpt/ssl"
|
"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 сервер для просмотра сохраненных результатов
|
// StartResultServer запускает HTTP/HTTPS сервер для просмотра сохраненных результатов
|
||||||
func StartResultServer(host, port string) error {
|
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)
|
addr := fmt.Sprintf("%s:%s", host, port)
|
||||||
|
|
||||||
// Проверяем, нужно ли использовать HTTPS
|
// Проверяем, нужно ли использовать HTTPS
|
||||||
@@ -103,78 +139,116 @@ func registerHTTPSRoutes() {
|
|||||||
registerRoutesExceptHome()
|
registerRoutesExceptHome()
|
||||||
|
|
||||||
// Регистрируем главную страницу с проверкой HTTPS
|
// Регистрируем главную страницу с проверкой HTTPS
|
||||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc(makePath("/"), func(w http.ResponseWriter, r *http.Request) {
|
||||||
// Проверяем, пришел ли запрос по HTTP (не HTTPS)
|
// Проверяем, пришел ли запрос по HTTP (не HTTPS)
|
||||||
if r.TLS == nil {
|
if r.TLS == nil {
|
||||||
handleHTTPSRedirect(w, r)
|
handleHTTPSRedirect(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Если уже HTTPS, обрабатываем как обычно
|
// Если уже 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 регистрирует все маршруты кроме главной страницы
|
// registerRoutesExceptHome регистрирует все маршруты кроме главной страницы
|
||||||
func 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(makePath("/file/"), AuthMiddleware(handleFileView))
|
||||||
http.HandleFunc("/delete/", handleDeleteFile)
|
http.HandleFunc(makePath("/delete/"), AuthMiddleware(handleDeleteFile))
|
||||||
|
|
||||||
// История запросов
|
// История запросов
|
||||||
http.HandleFunc("/history", handleHistoryPage)
|
http.HandleFunc(makePath("/history"), AuthMiddleware(handleHistoryPage))
|
||||||
http.HandleFunc("/history/view/", handleHistoryView)
|
http.HandleFunc(makePath("/history/view/"), AuthMiddleware(handleHistoryView))
|
||||||
http.HandleFunc("/history/delete/", handleDeleteHistoryEntry)
|
http.HandleFunc(makePath("/history/delete/"), AuthMiddleware(handleDeleteHistoryEntry))
|
||||||
http.HandleFunc("/history/clear", handleClearHistory)
|
http.HandleFunc(makePath("/history/clear"), AuthMiddleware(handleClearHistory))
|
||||||
|
|
||||||
// Управление промптами
|
// Управление промптами
|
||||||
http.HandleFunc("/prompts", handlePromptsPage)
|
http.HandleFunc(makePath("/prompts"), AuthMiddleware(handlePromptsPage))
|
||||||
http.HandleFunc("/prompts/add", handleAddPrompt)
|
http.HandleFunc(makePath("/prompts/add"), AuthMiddleware(handleAddPrompt))
|
||||||
http.HandleFunc("/prompts/edit/", handleEditPrompt)
|
http.HandleFunc(makePath("/prompts/edit/"), AuthMiddleware(handleEditPrompt))
|
||||||
http.HandleFunc("/prompts/edit-verbose/", handleEditVerbosePrompt)
|
http.HandleFunc(makePath("/prompts/edit-verbose/"), AuthMiddleware(handleEditVerbosePrompt))
|
||||||
http.HandleFunc("/prompts/delete/", handleDeletePrompt)
|
http.HandleFunc(makePath("/prompts/delete/"), AuthMiddleware(handleDeletePrompt))
|
||||||
http.HandleFunc("/prompts/restore/", handleRestorePrompt)
|
http.HandleFunc(makePath("/prompts/restore/"), AuthMiddleware(handleRestorePrompt))
|
||||||
http.HandleFunc("/prompts/restore-verbose/", handleRestoreVerbosePrompt)
|
http.HandleFunc(makePath("/prompts/restore-verbose/"), AuthMiddleware(handleRestoreVerbosePrompt))
|
||||||
http.HandleFunc("/prompts/save-lang", handleSaveLang)
|
http.HandleFunc(makePath("/prompts/save-lang"), AuthMiddleware(handleSaveLang))
|
||||||
|
|
||||||
// Веб-страница для выполнения запросов
|
// Веб-страница для выполнения запросов
|
||||||
http.HandleFunc("/run", handleExecutePage)
|
http.HandleFunc(makePath("/run"), AuthMiddleware(CSRFMiddleware(handleExecutePage)))
|
||||||
|
|
||||||
// API для выполнения запросов
|
// API для выполнения запросов
|
||||||
http.HandleFunc("/api/execute", handleExecute)
|
http.HandleFunc(makePath("/api/execute"), AuthMiddleware(CSRFMiddleware(handleExecute)))
|
||||||
// API для сохранения результатов и истории
|
// API для сохранения результатов и истории
|
||||||
http.HandleFunc("/api/save-result", handleSaveResult)
|
http.HandleFunc(makePath("/api/save-result"), AuthMiddleware(CSRFMiddleware(handleSaveResult)))
|
||||||
http.HandleFunc("/api/add-to-history", handleAddToHistory)
|
http.HandleFunc(makePath("/api/add-to-history"), AuthMiddleware(CSRFMiddleware(handleAddToHistory)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// registerRoutes регистрирует все маршруты сервера
|
// registerRoutes регистрирует все маршруты сервера
|
||||||
func 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(makePath("/"), AuthMiddleware(handleResultsPage))
|
||||||
http.HandleFunc("/file/", handleFileView)
|
http.HandleFunc(makePath("/file/"), AuthMiddleware(handleFileView))
|
||||||
http.HandleFunc("/delete/", handleDeleteFile)
|
http.HandleFunc(makePath("/delete/"), AuthMiddleware(handleDeleteFile))
|
||||||
|
|
||||||
// История запросов
|
// История запросов
|
||||||
http.HandleFunc("/history", handleHistoryPage)
|
http.HandleFunc(makePath("/history"), AuthMiddleware(handleHistoryPage))
|
||||||
http.HandleFunc("/history/view/", handleHistoryView)
|
http.HandleFunc(makePath("/history/view/"), AuthMiddleware(handleHistoryView))
|
||||||
http.HandleFunc("/history/delete/", handleDeleteHistoryEntry)
|
http.HandleFunc(makePath("/history/delete/"), AuthMiddleware(handleDeleteHistoryEntry))
|
||||||
http.HandleFunc("/history/clear", handleClearHistory)
|
http.HandleFunc(makePath("/history/clear"), AuthMiddleware(handleClearHistory))
|
||||||
|
|
||||||
// Управление промптами
|
// Управление промптами
|
||||||
http.HandleFunc("/prompts", handlePromptsPage)
|
http.HandleFunc(makePath("/prompts"), AuthMiddleware(handlePromptsPage))
|
||||||
http.HandleFunc("/prompts/add", handleAddPrompt)
|
http.HandleFunc(makePath("/prompts/add"), AuthMiddleware(handleAddPrompt))
|
||||||
http.HandleFunc("/prompts/edit/", handleEditPrompt)
|
http.HandleFunc(makePath("/prompts/edit/"), AuthMiddleware(handleEditPrompt))
|
||||||
http.HandleFunc("/prompts/edit-verbose/", handleEditVerbosePrompt)
|
http.HandleFunc(makePath("/prompts/edit-verbose/"), AuthMiddleware(handleEditVerbosePrompt))
|
||||||
http.HandleFunc("/prompts/delete/", handleDeletePrompt)
|
http.HandleFunc(makePath("/prompts/delete/"), AuthMiddleware(handleDeletePrompt))
|
||||||
http.HandleFunc("/prompts/restore/", handleRestorePrompt)
|
http.HandleFunc(makePath("/prompts/restore/"), AuthMiddleware(handleRestorePrompt))
|
||||||
http.HandleFunc("/prompts/restore-verbose/", handleRestoreVerbosePrompt)
|
http.HandleFunc(makePath("/prompts/restore-verbose/"), AuthMiddleware(handleRestoreVerbosePrompt))
|
||||||
http.HandleFunc("/prompts/save-lang", handleSaveLang)
|
http.HandleFunc(makePath("/prompts/save-lang"), AuthMiddleware(handleSaveLang))
|
||||||
|
|
||||||
// Веб-страница для выполнения запросов
|
// Веб-страница для выполнения запросов
|
||||||
http.HandleFunc("/run", handleExecutePage)
|
http.HandleFunc(makePath("/run"), AuthMiddleware(CSRFMiddleware(handleExecutePage)))
|
||||||
|
|
||||||
// API для выполнения запросов
|
// API для выполнения запросов
|
||||||
http.HandleFunc("/api/execute", handleExecute)
|
http.HandleFunc(makePath("/api/execute"), AuthMiddleware(CSRFMiddleware(handleExecute)))
|
||||||
// API для сохранения результатов и истории
|
// API для сохранения результатов и истории
|
||||||
http.HandleFunc("/api/save-result", handleSaveResult)
|
http.HandleFunc(makePath("/api/save-result"), AuthMiddleware(CSRFMiddleware(handleSaveResult)))
|
||||||
http.HandleFunc("/api/add-to-history", handleAddToHistory)
|
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;
|
opacity: 0.9;
|
||||||
font-size: 1.1em;
|
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 {
|
.content {
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ var ExecutePageTemplate = template.Must(template.New("execute").Parse(`<!DOCTYPE
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{{.Title}} - Linux Command GPT</title>
|
<title>{{.Title}} - {{.AppName}}</title>
|
||||||
<style>
|
<style>
|
||||||
{{template "execute_css" .}}
|
{{template "execute_css" .}}
|
||||||
</style>
|
</style>
|
||||||
@@ -17,16 +17,19 @@ var ExecutePageTemplate = template.Must(template.New("execute").Parse(`<!DOCTYPE
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>{{.Header}}</h1>
|
<h1>{{.Header}}</h1>
|
||||||
<p>Выполнение запросов к Linux Command GPT через веб-интерфейс</p>
|
<p>Выполнение запросов к {{.AppName}} через веб-интерфейс</p>
|
||||||
|
<p class="config-info">({{.ProviderType}} • {{.Model}} • {{.Host}})</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="nav-buttons">
|
<div class="nav-buttons">
|
||||||
<a href="/" class="nav-btn">🏠 Главная</a>
|
<a href="{{.BasePath}}/" class="nav-btn">🏠 Главная</a>
|
||||||
<a href="/history" class="nav-btn">📝 История</a>
|
<a href="{{.BasePath}}/history" class="nav-btn">📝 История</a>
|
||||||
<a href="/prompts" class="nav-btn">⚙️ Промпты</a>
|
<a href="{{.BasePath}}/prompts" class="nav-btn">⚙️ Промпты</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="POST" id="executeForm">
|
<form method="POST" id="executeForm">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||||
|
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="system_id">🤖 Системный промпт:</label>
|
<label for="system_id">🤖 Системный промпт:</label>
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ var ExecutePageScriptsTemplate = template.Must(template.New("execute_scripts").P
|
|||||||
function saveResult() {
|
function saveResult() {
|
||||||
const resultDataField = document.getElementById('resultData');
|
const resultDataField = document.getElementById('resultData');
|
||||||
const prompt = document.getElementById('prompt').value;
|
const prompt = document.getElementById('prompt').value;
|
||||||
|
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
|
||||||
|
|
||||||
if (!resultDataField.value || !prompt.trim()) {
|
if (!resultDataField.value || !prompt.trim()) {
|
||||||
alert('Нет данных для сохранения');
|
alert('Нет данных для сохранения');
|
||||||
@@ -104,10 +105,11 @@ var ExecutePageScriptsTemplate = template.Must(template.New("execute_scripts").P
|
|||||||
model: resultData.model || 'Unknown'
|
model: resultData.model || 'Unknown'
|
||||||
};
|
};
|
||||||
|
|
||||||
fetch('/api/save-result', {
|
fetch('{{.BasePath}}/api/save-result', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': csrfToken,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(requestData)
|
body: JSON.stringify(requestData)
|
||||||
})
|
})
|
||||||
@@ -134,6 +136,7 @@ var ExecutePageScriptsTemplate = template.Must(template.New("execute_scripts").P
|
|||||||
const resultDataField = document.getElementById('resultData');
|
const resultDataField = document.getElementById('resultData');
|
||||||
const prompt = document.getElementById('prompt').value;
|
const prompt = document.getElementById('prompt').value;
|
||||||
const systemId = document.getElementById('system_id').value;
|
const systemId = document.getElementById('system_id').value;
|
||||||
|
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
|
||||||
|
|
||||||
if (!resultDataField.value || !prompt.trim()) {
|
if (!resultDataField.value || !prompt.trim()) {
|
||||||
alert('Нет данных для сохранения в историю');
|
alert('Нет данных для сохранения в историю');
|
||||||
@@ -152,10 +155,11 @@ var ExecutePageScriptsTemplate = template.Must(template.New("execute_scripts").P
|
|||||||
system: systemName
|
system: systemName
|
||||||
};
|
};
|
||||||
|
|
||||||
fetch('/api/add-to-history', {
|
fetch('{{.BasePath}}/api/add-to-history', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': csrfToken,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(requestData)
|
body: JSON.stringify(requestData)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -155,13 +155,13 @@ const HistoryPageTemplate = `
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>📝 История запросов</h1>
|
<h1>📝 История запросов</h1>
|
||||||
<p>Управление историей запросов Linux Command GPT</p>
|
<p>Управление историей запросов {{.AppName}}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="nav-buttons">
|
<div class="nav-buttons">
|
||||||
<a href="/" class="nav-btn">🏠 Главная</a>
|
<a href="{{.BasePath}}/" class="nav-btn">🏠 Главная</a>
|
||||||
<a href="/run" class="nav-btn">🚀 Выполнение</a>
|
<a href="{{.BasePath}}/run" class="nav-btn">🚀 Выполнение</a>
|
||||||
<a href="/prompts" class="nav-btn">⚙️ Промпты</a>
|
<a href="{{.BasePath}}/prompts" class="nav-btn">⚙️ Промпты</a>
|
||||||
<button class="nav-btn clear-btn" onclick="clearHistory()">🗑️ Очистить всю историю</button>
|
<button class="nav-btn clear-btn" onclick="clearHistory()">🗑️ Очистить всю историю</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -197,12 +197,12 @@ const HistoryPageTemplate = `
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
function viewHistoryEntry(index) {
|
function viewHistoryEntry(index) {
|
||||||
window.location.href = '/history/view/' + index;
|
window.location.href = '{{.BasePath}}/history/view/' + index;
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteHistoryEntry(index) {
|
function deleteHistoryEntry(index) {
|
||||||
if (confirm('Вы уверены, что хотите удалить запись #' + index + '?')) {
|
if (confirm('Вы уверены, что хотите удалить запись #' + index + '?')) {
|
||||||
fetch('/history/delete/' + index, {
|
fetch('{{.BasePath}}/history/delete/' + index, {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
@@ -221,7 +221,7 @@ const HistoryPageTemplate = `
|
|||||||
|
|
||||||
function clearHistory() {
|
function clearHistory() {
|
||||||
if (confirm('Вы уверены, что хотите очистить всю историю?\\n\\nЭто действие нельзя отменить.')) {
|
if (confirm('Вы уверены, что хотите очистить всю историю?\\n\\nЭто действие нельзя отменить.')) {
|
||||||
fetch('/history/clear', {
|
fetch('{{.BasePath}}/history/clear', {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ const HistoryViewTemplate = `
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>📝 Запись #{{.Index}}</h1>
|
<h1>📝 Запись #{{.Index}}</h1>
|
||||||
<a href="/history" class="back-btn">← Назад к истории</a>
|
<a href="{{.BasePath}}/history" class="back-btn">← Назад к истории</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="history-meta">
|
<div class="history-meta">
|
||||||
@@ -249,7 +249,7 @@ const HistoryViewTemplate = `
|
|||||||
{{.ExplanationHTML}}
|
{{.ExplanationHTML}}
|
||||||
|
|
||||||
<div class="actions">
|
<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>
|
<button class="action-btn delete-btn" onclick="deleteHistoryEntry({{.Index}})">🗑️ Удалить запись</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -258,12 +258,12 @@ const HistoryViewTemplate = `
|
|||||||
<script>
|
<script>
|
||||||
function deleteHistoryEntry(index) {
|
function deleteHistoryEntry(index) {
|
||||||
if (confirm('Вы уверены, что хотите удалить запись #' + index + '?')) {
|
if (confirm('Вы уверены, что хотите удалить запись #' + index + '?')) {
|
||||||
fetch('/history/delete/' + index, {
|
fetch('{{.BasePath}}/history/delete/' + index, {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
window.location.href = '/history';
|
window.location.href = '{{.BasePath}}/history';
|
||||||
} else {
|
} else {
|
||||||
alert('Ошибка при удалении записи');
|
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="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>⚙️ Системные промпты</h1>
|
<h1>⚙️ Системные промпты</h1>
|
||||||
<p>Управление системными промптами Linux Command GPT</p>
|
<p>Управление системными промптами {{.AppName}}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="nav-buttons">
|
<div class="nav-buttons">
|
||||||
<a href="/" class="nav-btn">🏠 Главная</a>
|
<a href="{{.BasePath}}/" class="nav-btn">🏠 Главная</a>
|
||||||
<a href="/run" class="nav-btn">🚀 Выполнение</a>
|
<a href="{{.BasePath}}/run" class="nav-btn">🚀 Выполнение</a>
|
||||||
<a href="/history" class="nav-btn">📝 История</a>
|
<a href="{{.BasePath}}/history" class="nav-btn">📝 История</a>
|
||||||
<button class="nav-btn add-btn" onclick="showAddForm()">➕ Добавить промпт</button>
|
<button class="nav-btn add-btn" onclick="showAddForm()">➕ Добавить промпт</button>
|
||||||
<div class="lang-switcher">
|
<div class="lang-switcher">
|
||||||
<button class="lang-btn {{if eq .Lang "ru"}}active{{end}}" onclick="switchLang('ru')">🇷🇺 RU</button>
|
<button class="lang-btn {{if eq .Lang "ru"}}active{{end}}" onclick="switchLang('ru')">🇷🇺 RU</button>
|
||||||
@@ -391,7 +391,7 @@ const PromptsPageTemplate = `
|
|||||||
|
|
||||||
function saveCurrentPrompts(lang) {
|
function saveCurrentPrompts(lang) {
|
||||||
// Отправляем запрос для сохранения текущих промптов с новым языком
|
// Отправляем запрос для сохранения текущих промптов с новым языком
|
||||||
fetch('/prompts/save-lang', {
|
fetch('{{.BasePath}}/prompts/save-lang', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -417,7 +417,7 @@ const PromptsPageTemplate = `
|
|||||||
|
|
||||||
function deletePrompt(id) {
|
function deletePrompt(id) {
|
||||||
if (confirm('Вы уверены, что хотите удалить промпт #' + id + '?')) {
|
if (confirm('Вы уверены, что хотите удалить промпт #' + id + '?')) {
|
||||||
fetch('/prompts/delete/' + id, {
|
fetch('{{.BasePath}}/prompts/delete/' + id, {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
@@ -467,10 +467,10 @@ const PromptsPageTemplate = `
|
|||||||
|
|
||||||
let url, method;
|
let url, method;
|
||||||
if (isVerbosePrompt) {
|
if (isVerbosePrompt) {
|
||||||
url = '/prompts/edit-verbose/' + id;
|
url = '{{.BasePath}}/prompts/edit-verbose/' + id;
|
||||||
method = 'PUT';
|
method = 'PUT';
|
||||||
} else {
|
} else {
|
||||||
url = id ? '/prompts/edit/' + id : '/prompts/add';
|
url = id ? '{{.BasePath}}/prompts/edit/' + id : '{{.BasePath}}/prompts/add';
|
||||||
method = id ? 'PUT' : 'POST';
|
method = id ? 'PUT' : 'POST';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,7 +501,7 @@ const PromptsPageTemplate = `
|
|||||||
// Функция восстановления системного промпта
|
// Функция восстановления системного промпта
|
||||||
function restorePrompt(id) {
|
function restorePrompt(id) {
|
||||||
if (confirm('Восстановить промпт к значению по умолчанию?')) {
|
if (confirm('Восстановить промпт к значению по умолчанию?')) {
|
||||||
fetch('/prompts/restore/' + id, {
|
fetch('{{.BasePath}}/prompts/restore/' + id, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -526,7 +526,7 @@ const PromptsPageTemplate = `
|
|||||||
// Функция восстановления verbose промпта
|
// Функция восстановления verbose промпта
|
||||||
function restoreVerbosePrompt(mode) {
|
function restoreVerbosePrompt(mode) {
|
||||||
if (confirm('Восстановить промпт к значению по умолчанию?')) {
|
if (confirm('Восстановить промпт к значению по умолчанию?')) {
|
||||||
fetch('/prompts/restore-verbose/' + mode, {
|
fetch('{{.BasePath}}/prompts/restore-verbose/' + mode, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const ResultsPageTemplate = `
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>LCG Results - Linux Command GPT</title>
|
<title>{{.AppAbbreviation}} Результаты - {{.AppName}}</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
@@ -182,15 +182,15 @@ const ResultsPageTemplate = `
|
|||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>🚀 LCG Results</h1>
|
<h1>🚀 {{.AppAbbreviation}} - {{.AppName}}</h1>
|
||||||
<p>Просмотр сохраненных результатов Linux Command GPT</p>
|
<p>Просмотр сохраненных результатов {{.AppName}}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="nav-buttons">
|
<div class="nav-buttons">
|
||||||
<button class="nav-btn" onclick="location.reload()">🔄 Обновить</button>
|
<button class="nav-btn" onclick="location.reload()">🔄 Обновить</button>
|
||||||
<a href="/run" class="nav-btn">🚀 Выполнение</a>
|
<a href="{{.BasePath}}/run" class="nav-btn">🚀 Выполнение</a>
|
||||||
<a href="/history" class="nav-btn">📝 История</a>
|
<a href="{{.BasePath}}/history" class="nav-btn">📝 История</a>
|
||||||
<a href="/prompts" class="nav-btn">⚙️ Промпты</a>
|
<a href="{{.BasePath}}/prompts" class="nav-btn">⚙️ Промпты</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Поиск -->
|
<!-- Поиск -->
|
||||||
@@ -218,7 +218,7 @@ const ResultsPageTemplate = `
|
|||||||
<div class="file-actions">
|
<div class="file-actions">
|
||||||
<button class="delete-btn" onclick="deleteFile('{{.Name}}')" title="Удалить файл">🗑️</button>
|
<button class="delete-btn" onclick="deleteFile('{{.Name}}')" title="Удалить файл">🗑️</button>
|
||||||
</div>
|
</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-name">{{.Name}}</div>
|
||||||
<div class="file-info">
|
<div class="file-info">
|
||||||
📅 {{.ModTime}} | 📏 {{.Size}}
|
📅 {{.ModTime}} | 📏 {{.Size}}
|
||||||
@@ -240,7 +240,7 @@ const ResultsPageTemplate = `
|
|||||||
<script>
|
<script>
|
||||||
function deleteFile(filename) {
|
function deleteFile(filename) {
|
||||||
if (confirm('Вы уверены, что хотите удалить файл "' + filename + '"?\\n\\nЭто действие нельзя отменить.')) {
|
if (confirm('Вы уверены, что хотите удалить файл "' + filename + '"?\\n\\nЭто действие нельзя отменить.')) {
|
||||||
fetch('/delete/' + encodeURIComponent(filename), {
|
fetch('{{.BasePath}}/delete/' + encodeURIComponent(filename), {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
})
|
})
|
||||||
.then(response => {
|
.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