init
This commit is contained in:
258
back/cmd/crypto_routes.go
Normal file
258
back/cmd/crypto_routes.go
Normal file
@@ -0,0 +1,258 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// setupCryptoRoutes настраивает роуты для шифрования/дешифрования
|
||||
func setupCryptoRoutes(api *gin.RouterGroup, passHash [32]byte) {
|
||||
// Encrypt: вход YAML (и опционально path). Если path указан: можно не передавать yaml,
|
||||
// тогда читаем из файла и пишем шифровку в этот же путь
|
||||
api.POST("/encrypt", func(c *gin.Context) {
|
||||
var req struct {
|
||||
Yaml string `json:"yaml"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": fmt.Sprintf("bad json: %v", err)})
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.Yaml) == "" {
|
||||
c.JSON(400, gin.H{"error": "yaml is required"})
|
||||
return
|
||||
}
|
||||
|
||||
// Извлекаем path из YAML для автоматической записи файла
|
||||
var yamlPath string
|
||||
if yamlStr := strings.TrimSpace(req.Yaml); yamlStr != "" {
|
||||
var config map[string]interface{}
|
||||
if err := yaml.Unmarshal([]byte(yamlStr), &config); err == nil {
|
||||
if pathVal, ok := config["path"].(string); ok {
|
||||
yamlPath = strings.TrimSpace(pathVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Используем path из запроса или из YAML
|
||||
targetPath := strings.TrimSpace(req.Path)
|
||||
if targetPath == "" && yamlPath != "" {
|
||||
targetPath = yamlPath
|
||||
}
|
||||
|
||||
encrypted, err := encryptBytes([]byte(req.Yaml), passHash[:])
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
out := "ENCRYPTED:" + encrypted
|
||||
|
||||
if targetPath != "" {
|
||||
if !isFileIOEnabled() {
|
||||
c.JSON(403, gin.H{"error": "file I/O is disabled; set GO_KNOCKER_ENABLE_FILE_IO=1 or remove GO_KNOCKER_ENABLE_FILE_IO=0"})
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(targetPath, []byte(out), 0600); err != nil {
|
||||
c.JSON(500, gin.H{"error": fmt.Sprintf("write failed: %v", err)})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"status": "ok", "encrypted": out, "path": targetPath})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"encrypted": out})
|
||||
})
|
||||
|
||||
// Decrypt: вход ENCRYPTED:... (и опционально path). Если path указан: можно не передавать encrypted,
|
||||
// тогда читаем из файла и пишем расшифровку в этот же путь
|
||||
api.POST("/decrypt", func(c *gin.Context) {
|
||||
var req struct {
|
||||
Encrypted string `json:"encrypted"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": fmt.Sprintf("bad json: %v", err)})
|
||||
return
|
||||
}
|
||||
var enc string
|
||||
if strings.TrimSpace(req.Path) != "" && strings.TrimSpace(req.Encrypted) == "" {
|
||||
if !isFileIOEnabled() {
|
||||
c.JSON(403, gin.H{"error": "file I/O is disabled; set GO_KNOCKER_ENABLE_FILE_IO=1 or remove GO_KNOCKER_ENABLE_FILE_IO=0"})
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(req.Path)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": fmt.Sprintf("read failed: %v", err)})
|
||||
return
|
||||
}
|
||||
enc = string(data)
|
||||
} else {
|
||||
enc = req.Encrypted
|
||||
}
|
||||
if !strings.HasPrefix(enc, "ENCRYPTED:") {
|
||||
c.JSON(400, gin.H{"error": "input must start with ENCRYPTED:"})
|
||||
return
|
||||
}
|
||||
plain, err := decryptString(enc[10:], passHash[:])
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Извлекаем path из расшифрованного YAML
|
||||
var yamlPath string
|
||||
if plainStr := string(plain); strings.TrimSpace(plainStr) != "" {
|
||||
var config map[string]interface{}
|
||||
if err := yaml.Unmarshal(plain, &config); err == nil {
|
||||
if pathVal, ok := config["path"].(string); ok {
|
||||
yamlPath = strings.TrimSpace(pathVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Используем path из запроса или из YAML
|
||||
targetPath := strings.TrimSpace(req.Path)
|
||||
if targetPath == "" && yamlPath != "" {
|
||||
targetPath = yamlPath
|
||||
}
|
||||
|
||||
if targetPath != "" {
|
||||
if !isFileIOEnabled() {
|
||||
c.JSON(403, gin.H{"error": "file I/O is disabled; set GO_KNOCKER_ENABLE_FILE_IO=1 or remove GO_KNOCKER_ENABLE_FILE_IO=0"})
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(targetPath, plain, 0600); err != nil {
|
||||
c.JSON(500, gin.H{"error": fmt.Sprintf("write failed: %v", err)})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"status": "ok", "yaml": string(plain), "path": targetPath})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"yaml": string(plain)})
|
||||
})
|
||||
|
||||
// Encrypt file on server filesystem
|
||||
api.POST("/encrypt-file", func(c *gin.Context) {
|
||||
if !isFileIOEnabled() {
|
||||
c.JSON(403, gin.H{"error": "file I/O is disabled; set GO_KNOCKER_ENABLE_FILE_IO=1 or remove GO_KNOCKER_ENABLE_FILE_IO=0"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
if err := c.BindJSON(&req); err != nil || strings.TrimSpace(req.Path) == "" {
|
||||
c.JSON(400, gin.H{"error": "invalid path"})
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(req.Path)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": fmt.Sprintf("read failed: %v", err)})
|
||||
return
|
||||
}
|
||||
encrypted, err := encryptBytes(data, passHash[:])
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
out := "ENCRYPTED:" + encrypted
|
||||
if err := os.WriteFile(req.Path, []byte(out), 0600); err != nil {
|
||||
c.JSON(500, gin.H{"error": fmt.Sprintf("write failed: %v", err)})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"status": "ok", "encrypted": out})
|
||||
})
|
||||
|
||||
// Decrypt file on server filesystem
|
||||
api.POST("/decrypt-file", func(c *gin.Context) {
|
||||
if !isFileIOEnabled() {
|
||||
c.JSON(403, gin.H{"error": "file I/O is disabled; set GO_KNOCKER_ENABLE_FILE_IO=1 or remove GO_KNOCKER_ENABLE_FILE_IO=0"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
if err := c.BindJSON(&req); err != nil || strings.TrimSpace(req.Path) == "" {
|
||||
c.JSON(400, gin.H{"error": "invalid path"})
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(req.Path)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": fmt.Sprintf("read failed: %v", err)})
|
||||
return
|
||||
}
|
||||
enc := string(data)
|
||||
if !strings.HasPrefix(enc, "ENCRYPTED:") {
|
||||
c.JSON(400, gin.H{"error": "file must start with ENCRYPTED:"})
|
||||
return
|
||||
}
|
||||
plain, err := decryptString(enc[10:], passHash[:])
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(req.Path, plain, 0600); err != nil {
|
||||
c.JSON(500, gin.H{"error": fmt.Sprintf("write failed: %v", err)})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"status": "ok", "yaml": string(plain)})
|
||||
})
|
||||
}
|
||||
|
||||
// encryptBytes шифрует байты с помощью AES-GCM
|
||||
func encryptBytes(data []byte, key []byte) (string, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ciphertext := gcm.Seal(nonce, nonce, data, nil)
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
// decryptString дешифрует строку с помощью AES-GCM
|
||||
func decryptString(encrypted string, key []byte) ([]byte, error) {
|
||||
data, err := base64.StdEncoding.DecodeString(encrypted)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(data) < nonceSize {
|
||||
return nil, fmt.Errorf("ciphertext too short")
|
||||
}
|
||||
|
||||
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
|
||||
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
130
back/cmd/decrypt.go
Normal file
130
back/cmd/decrypt.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/sha256"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var decryptCmd = &cobra.Command{
|
||||
Use: "decrypt",
|
||||
Short: "Расшифровать зашифрованный конфиг в открытый YAML",
|
||||
Long: `Расшифровывает зашифрованный конфигурационный файл (ENCRYPTED:...) в обычный YAML-файл`,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Для команды decrypt config не обязателен если есть -i
|
||||
return nil
|
||||
},
|
||||
RunE: runDecrypt,
|
||||
}
|
||||
|
||||
var (
|
||||
decryptInputFile string
|
||||
decryptOutputFile string
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(decryptCmd)
|
||||
decryptCmd.Flags().StringVarP(&decryptInputFile, "input", "i", "", "Входной зашифрованный файл (если не указан, используется --config)")
|
||||
decryptCmd.Flags().StringVarP(&decryptOutputFile, "output", "o", "", "Выходной YAML-файл")
|
||||
decryptCmd.MarkFlagRequired("output")
|
||||
}
|
||||
|
||||
func runDecrypt(cmd *cobra.Command, args []string) error {
|
||||
// Определяем входной файл: либо из -i, либо из глобального --config
|
||||
input := decryptInputFile
|
||||
if input == "" {
|
||||
input = configFile
|
||||
if input == "" {
|
||||
return fmt.Errorf("необходимо указать входной файл через -i или --config")
|
||||
}
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(input)
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось прочитать входной файл %s: %w", input, err)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(string(data), "ENCRYPTED:") {
|
||||
return fmt.Errorf("файл %s не является зашифрованным (нет префикса ENCRYPTED:)", input)
|
||||
}
|
||||
|
||||
key, err := getDecryptionKeyHashed(keyFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось получить ключ шифрования: %w", err)
|
||||
}
|
||||
|
||||
decrypted, err := decryptData(data[10:], key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось расшифровать данные: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(decryptOutputFile, decrypted, 0600); err != nil {
|
||||
return fmt.Errorf("не удалось записать YAML-файл: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Файл успешно расшифрован: %s → %s\n", input, decryptOutputFile)
|
||||
return nil
|
||||
}
|
||||
|
||||
// getDecryptionKeyHashed получает ключ шифрования и хеширует его до 32 байт (аналогично encrypt)
|
||||
func getDecryptionKeyHashed(keyFile string) ([]byte, error) {
|
||||
var rawKey []byte
|
||||
var err error
|
||||
|
||||
if keyFile != "" {
|
||||
// Читаем ключ из файла
|
||||
rawKey, err = os.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось прочитать файл ключа: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Пытаемся получить ключ из системной переменной
|
||||
key := os.Getenv("GO_KNOCKER_SERVE_PASS")
|
||||
if key == "" {
|
||||
return nil, fmt.Errorf("ключ шифрования не найден ни в файле, ни в переменной GO_KNOCKER_SERVE_PASS")
|
||||
}
|
||||
rawKey = []byte(key)
|
||||
}
|
||||
|
||||
// Хешируем ключ SHA256 чтобы получить всегда 32 байта
|
||||
hash := sha256.Sum256(rawKey)
|
||||
return hash[:], nil
|
||||
}
|
||||
|
||||
// decryptData расшифровывает данные с помощью AES-GCM (аналогично internal)
|
||||
func decryptData(encryptedData []byte, key []byte) ([]byte, error) {
|
||||
data, err := base64.StdEncoding.DecodeString(string(encryptedData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось декодировать base64: %w", err)
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось создать AES cipher: %w", err)
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось создать GCM: %w", err)
|
||||
}
|
||||
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(data) < nonceSize {
|
||||
return nil, fmt.Errorf("данные слишком короткие")
|
||||
}
|
||||
|
||||
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
|
||||
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось расшифровать: %w", err)
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
127
back/cmd/encrypt.go
Normal file
127
back/cmd/encrypt.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var encryptCmd = &cobra.Command{
|
||||
Use: "encrypt",
|
||||
Short: "Зашифровать конфигурационный файл",
|
||||
Long: `Зашифровывает YAML конфигурационный файл с помощью AES-GCM шифрования`,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Для команды encrypt config не обязателен если есть -i
|
||||
return nil
|
||||
},
|
||||
RunE: runEncrypt,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(encryptCmd)
|
||||
encryptCmd.Flags().StringVarP(&inputFile, "input", "i", "", "Входной файл для шифрования (если не указан, используется --config)")
|
||||
encryptCmd.Flags().StringVarP(&outputFile, "output", "o", "", "Выходной зашифрованный файл")
|
||||
encryptCmd.MarkFlagRequired("output")
|
||||
}
|
||||
|
||||
var (
|
||||
inputFile string
|
||||
outputFile string
|
||||
)
|
||||
|
||||
func runEncrypt(cmd *cobra.Command, args []string) error {
|
||||
// Определяем входной файл: либо из -i, либо из глобального --config
|
||||
input := inputFile
|
||||
if input == "" {
|
||||
input = configFile
|
||||
if input == "" {
|
||||
return fmt.Errorf("необходимо указать входной файл через -i или --config")
|
||||
}
|
||||
}
|
||||
|
||||
// Читаем входной файл
|
||||
data, err := os.ReadFile(input)
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось прочитать входной файл %s: %w", input, err)
|
||||
}
|
||||
|
||||
// Получаем ключ шифрования
|
||||
key, err := getEncryptionKeyHashed(keyFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось получить ключ шифрования: %w", err)
|
||||
}
|
||||
|
||||
// Шифруем данные
|
||||
encryptedData, err := encrypt(data, key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось зашифровать данные: %w", err)
|
||||
}
|
||||
|
||||
// Записываем зашифрованный файл с префиксом "ENCRYPTED:"
|
||||
output := "ENCRYPTED:" + encryptedData
|
||||
if err := os.WriteFile(outputFile, []byte(output), 0600); err != nil {
|
||||
return fmt.Errorf("не удалось записать зашифрованный файл: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Файл успешно зашифрован: %s → %s\n", input, outputFile)
|
||||
return nil
|
||||
}
|
||||
|
||||
// getEncryptionKeyHashed получает ключ шифрования и хеширует его до 32 байт
|
||||
func getEncryptionKeyHashed(keyFile string) ([]byte, error) {
|
||||
var rawKey []byte
|
||||
var err error
|
||||
|
||||
if keyFile != "" {
|
||||
// Читаем ключ из файла
|
||||
rawKey, err = os.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось прочитать файл ключа: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Пытаемся получить ключ из системной переменной
|
||||
key := os.Getenv("GO_KNOCKER_SERVE_PASS")
|
||||
if key == "" {
|
||||
return nil, fmt.Errorf("ключ шифрования не найден ни в файле, ни в переменной GO_KNOCKER_SERVE_PASS")
|
||||
}
|
||||
rawKey = []byte(key)
|
||||
}
|
||||
|
||||
// Хешируем ключ SHA256 чтобы получить всегда 32 байта
|
||||
hash := sha256.Sum256(rawKey)
|
||||
return hash[:], nil
|
||||
}
|
||||
|
||||
// encrypt шифрует данные с помощью AES-GCM
|
||||
func encrypt(plaintext []byte, key []byte) (string, error) {
|
||||
// Создаем AES cipher
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("не удалось создать AES cipher: %w", err)
|
||||
}
|
||||
|
||||
// Создаем GCM
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("не удалось создать GCM: %w", err)
|
||||
}
|
||||
|
||||
// Создаем nonce
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return "", fmt.Errorf("не удалось создать nonce: %w", err)
|
||||
}
|
||||
|
||||
// Шифруем
|
||||
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
|
||||
|
||||
// Кодируем в base64
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
141
back/cmd/knock_routes.go
Normal file
141
back/cmd/knock_routes.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"port-knocker/internal"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// setupKnockRoutes настраивает роуты для выполнения port knocking
|
||||
func setupKnockRoutes(api *gin.RouterGroup) {
|
||||
// Execute: вход inline или YAML конфиг
|
||||
api.POST("/execute", func(c *gin.Context) {
|
||||
var req struct {
|
||||
Targets string `json:"targets"`
|
||||
Delay string `json:"delay"`
|
||||
Verbose bool `json:"verbose"`
|
||||
WaitConnection bool `json:"waitConnection"`
|
||||
Gateway string `json:"gateway"`
|
||||
ConfigYaml string `json:"config_yaml"`
|
||||
}
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": fmt.Sprintf("bad json: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
knocker := internal.NewPortKnocker()
|
||||
|
||||
// Определяем режим: inline или YAML
|
||||
if strings.TrimSpace(req.ConfigYaml) != "" {
|
||||
// YAML режим - загружаем конфигурацию из строки
|
||||
config, err := internal.LoadConfigFromString(req.ConfigYaml)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": fmt.Sprintf("invalid yaml: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// Применяем дополнительные параметры из запроса
|
||||
if req.Gateway != "" {
|
||||
for i := range config.Targets {
|
||||
config.Targets[i].Gateway = req.Gateway
|
||||
}
|
||||
}
|
||||
|
||||
if err := knocker.ExecuteWithConfig(config, req.Verbose, req.WaitConnection); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
return
|
||||
} else {
|
||||
// Inline режим
|
||||
if strings.TrimSpace(req.Targets) == "" {
|
||||
c.JSON(400, gin.H{"error": "targets is required in inline mode"})
|
||||
return
|
||||
}
|
||||
config, err := parseInlineTargetsWithWait(req.Targets, req.Delay, req.WaitConnection)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": fmt.Sprintf("invalid targets: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// Применяем gateway к каждой цели
|
||||
if req.Gateway != "" {
|
||||
for i := range config.Targets {
|
||||
config.Targets[i].Gateway = req.Gateway
|
||||
}
|
||||
}
|
||||
|
||||
if err := knocker.ExecuteWithConfig(&config, req.Verbose, req.WaitConnection); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// parseInlineTargetsWithWait парсит inline строку целей в Config с поддержкой waitConnection
|
||||
func parseInlineTargetsWithWait(targets, delay string, waitConnection bool) (internal.Config, error) {
|
||||
var config internal.Config
|
||||
|
||||
// Парсим targets
|
||||
targetStrings := strings.Split(targets, ";")
|
||||
for _, targetStr := range targetStrings {
|
||||
targetStr = strings.TrimSpace(targetStr)
|
||||
if targetStr == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Split(targetStr, ":")
|
||||
if len(parts) != 3 {
|
||||
return config, fmt.Errorf("invalid target format: %s (expected protocol:host:port)", targetStr)
|
||||
}
|
||||
|
||||
protocol := strings.TrimSpace(parts[0])
|
||||
host := strings.TrimSpace(parts[1])
|
||||
portStr := strings.TrimSpace(parts[2])
|
||||
|
||||
if protocol != "tcp" && protocol != "udp" {
|
||||
return config, fmt.Errorf("unsupported protocol: %s (only tcp/udp supported)", protocol)
|
||||
}
|
||||
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return config, fmt.Errorf("invalid port: %s", portStr)
|
||||
}
|
||||
|
||||
// Парсим delay
|
||||
var targetDelay internal.Duration
|
||||
if delay != "" {
|
||||
duration, err := time.ParseDuration(delay)
|
||||
if err != nil {
|
||||
return config, fmt.Errorf("invalid delay: %s", delay)
|
||||
}
|
||||
targetDelay = internal.Duration(duration)
|
||||
} else {
|
||||
targetDelay = internal.Duration(time.Second)
|
||||
}
|
||||
|
||||
target := internal.Target{
|
||||
Protocol: protocol,
|
||||
Host: host,
|
||||
Ports: []int{port},
|
||||
Delay: targetDelay,
|
||||
WaitConnection: waitConnection,
|
||||
}
|
||||
|
||||
config.Targets = append(config.Targets, target)
|
||||
}
|
||||
|
||||
if len(config.Targets) == 0 {
|
||||
return config, fmt.Errorf("no valid targets found")
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
57
back/cmd/middleware.go
Normal file
57
back/cmd/middleware.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// isFileIOEnabled проверяет, разрешены ли файловые операции
|
||||
// По умолчанию включено, отключается только при GO_KNOCKER_ENABLE_FILE_IO=0
|
||||
func isFileIOEnabled() bool {
|
||||
env := os.Getenv("GO_KNOCKER_ENABLE_FILE_IO")
|
||||
return env != "0" && env != "false"
|
||||
}
|
||||
|
||||
// authMiddleware - базовая авторизация: пользователь "knocker" + пароль
|
||||
func authMiddleware(pass string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
user, providedPass, ok := c.Request.BasicAuth()
|
||||
// Проверяем пользователя "knocker" и пароль
|
||||
if !ok || (providedPass != pass || user != "knocker") {
|
||||
c.Header("WWW-Authenticate", "Basic realm=Restricted")
|
||||
c.AbortWithStatus(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// staticAuthMiddleware - защищает HTML страницы, но пропускает статические ресурсы
|
||||
func staticAuthMiddleware(pass string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
path := c.Request.URL.Path
|
||||
|
||||
// Пропускаем статические ресурсы без авторизации
|
||||
if strings.HasSuffix(path, ".js") ||
|
||||
strings.HasSuffix(path, ".css") ||
|
||||
strings.HasSuffix(path, ".ico") ||
|
||||
strings.HasSuffix(path, ".png") ||
|
||||
strings.HasSuffix(path, ".jpg") ||
|
||||
strings.HasSuffix(path, ".svg") ||
|
||||
strings.HasSuffix(path, ".woff") ||
|
||||
strings.HasSuffix(path, ".woff2") ||
|
||||
strings.HasSuffix(path, ".ttf") ||
|
||||
strings.HasSuffix(path, ".eot") ||
|
||||
strings.HasSuffix(path, ".webmanifest") ||
|
||||
strings.HasPrefix(path, "/api/") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// Для всех остальных путей (в основном HTML) применяем авторизацию
|
||||
authMiddleware(pass)(c)
|
||||
}
|
||||
}
|
144
back/cmd/root.go
Normal file
144
back/cmd/root.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"port-knocker/internal"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
configFile string
|
||||
keyFile string
|
||||
verbose bool
|
||||
waitConnection bool
|
||||
targetsInline string
|
||||
defaultDelay string
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "port-knocker",
|
||||
Short: "Утилита для отправки port knocking пакетов",
|
||||
Long: `Port Knocker - утилита для отправки TCP/UDP пакетов на определенные порты
|
||||
в заданной последовательности для активации портов на удаленных серверах.
|
||||
|
||||
Поддерживает:
|
||||
- TCP и UDP протоколы
|
||||
- Зашифрованные конфигурационные файлы
|
||||
- Автоматическое определение зашифрованных файлов
|
||||
- Ключи шифрования из файла или системной переменной
|
||||
- Настройка шлюза для отправки пакетов
|
||||
- Гибкая настройка ожидания соединения
|
||||
- Инлайн задание целей без конфигурационного файла`,
|
||||
RunE: runKnock,
|
||||
}
|
||||
|
||||
func Execute() error {
|
||||
return rootCmd.Execute()
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "Путь к файлу конфигурации")
|
||||
rootCmd.PersistentFlags().StringVarP(&keyFile, "key", "k", "", "Путь к файлу ключа шифрования")
|
||||
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Подробный вывод")
|
||||
rootCmd.PersistentFlags().BoolVarP(&waitConnection, "wait-connection", "w", false, "Ждать установления соединения (по умолчанию не ждать)")
|
||||
rootCmd.PersistentFlags().StringVarP(&targetsInline, "targets", "t", "", "Инлайн цели в формате [proto]:[host]:[port];[proto]:[host]:[port]")
|
||||
rootCmd.PersistentFlags().StringVarP(&defaultDelay, "delay", "d", "1s", "Задержка между пакетами (по умолчанию 1s)")
|
||||
|
||||
// НЕ делаем config глобально обязательным - проверяем в runKnock
|
||||
}
|
||||
|
||||
func runKnock(cmd *cobra.Command, args []string) error {
|
||||
// Проверяем что указан либо config файл, либо инлайн цели
|
||||
if configFile == "" && targetsInline == "" {
|
||||
return fmt.Errorf("необходимо указать либо файл конфигурации (-c), либо инлайн цели (-t)")
|
||||
}
|
||||
|
||||
if configFile != "" && targetsInline != "" {
|
||||
return fmt.Errorf("нельзя одновременно использовать файл конфигурации (-c) и инлайн цели (-t)")
|
||||
}
|
||||
|
||||
knocker := internal.NewPortKnocker()
|
||||
|
||||
// Если используем инлайн цели
|
||||
if targetsInline != "" {
|
||||
config, err := parseInlineTargets(targetsInline, defaultDelay)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка разбора инлайн целей: %w", err)
|
||||
}
|
||||
return knocker.ExecuteWithConfig(config, verbose, waitConnection)
|
||||
}
|
||||
|
||||
// Иначе используем файл конфигурации
|
||||
return knocker.Execute(configFile, keyFile, verbose, waitConnection)
|
||||
}
|
||||
|
||||
// parseInlineTargets разбирает строку инлайн целей в Config
|
||||
func parseInlineTargets(targetsStr, delayStr string) (*internal.Config, error) {
|
||||
// Парсим задержку
|
||||
delay, err := time.ParseDuration(delayStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("неверная задержка '%s': %w", delayStr, err)
|
||||
}
|
||||
|
||||
config := &internal.Config{
|
||||
Targets: []internal.Target{},
|
||||
}
|
||||
|
||||
// Разбиваем по точкам с запятой
|
||||
targetParts := strings.Split(targetsStr, ";")
|
||||
|
||||
for _, targetStr := range targetParts {
|
||||
targetStr = strings.TrimSpace(targetStr)
|
||||
if targetStr == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Разбираем формат [proto]:[host]:[port]
|
||||
parts := strings.Split(targetStr, ":")
|
||||
if len(parts) != 3 {
|
||||
return nil, fmt.Errorf("неверный формат цели '%s', ожидается [proto]:[host]:[port]", targetStr)
|
||||
}
|
||||
|
||||
protocol := strings.TrimSpace(parts[0])
|
||||
host := strings.TrimSpace(parts[1])
|
||||
portStr := strings.TrimSpace(parts[2])
|
||||
|
||||
// Проверяем протокол
|
||||
if protocol != "tcp" && protocol != "udp" {
|
||||
return nil, fmt.Errorf("неподдерживаемый протокол '%s' в цели '%s'", protocol, targetStr)
|
||||
}
|
||||
|
||||
// Парсим порт
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("неверный порт '%s' в цели '%s': %w", portStr, targetStr, err)
|
||||
}
|
||||
|
||||
if port < 1 || port > 65535 {
|
||||
return nil, fmt.Errorf("порт %d вне допустимого диапазона (1-65535) в цели '%s'", port, targetStr)
|
||||
}
|
||||
|
||||
// Создаем цель
|
||||
target := internal.Target{
|
||||
Host: host,
|
||||
Ports: []int{port},
|
||||
Protocol: protocol,
|
||||
Delay: internal.Duration(delay),
|
||||
WaitConnection: false,
|
||||
Gateway: "",
|
||||
}
|
||||
|
||||
config.Targets = append(config.Targets, target)
|
||||
}
|
||||
|
||||
if len(config.Targets) == 0 {
|
||||
return nil, fmt.Errorf("не найдено ни одной валидной цели")
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
70
back/cmd/serve.go
Normal file
70
back/cmd/serve.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"port-knocker/internal"
|
||||
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
//go:embed public/*
|
||||
var embeddedFS embed.FS
|
||||
|
||||
var serveCmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "Запуск встроенного веб-сервера с GUI и REST API",
|
||||
RunE: runServe,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(serveCmd)
|
||||
}
|
||||
|
||||
func runServe(cmd *cobra.Command, args []string) error {
|
||||
pass := os.Getenv("GO_KNOCKER_SERVE_PASS")
|
||||
if strings.TrimSpace(pass) == "" {
|
||||
return fmt.Errorf("GO_KNOCKER_SERVE_PASS не задан — задайте пароль для доступа к GUI/API")
|
||||
}
|
||||
|
||||
// Хеш, который будем использовать как ключ шифрования (совместимо с internal)
|
||||
passHash := sha256.Sum256([]byte(pass))
|
||||
// Пробрасываем пароль; internal сам выполнит sha256 от значения env
|
||||
os.Setenv(internal.EncryptionKeyEnvVar, pass)
|
||||
|
||||
port := os.Getenv("GO_KNOCKER_SERVE_PORT")
|
||||
if strings.TrimSpace(port) == "" {
|
||||
port = "8888"
|
||||
}
|
||||
|
||||
r := gin.Default()
|
||||
|
||||
// CORS: разрешаем для локальной разработки
|
||||
r.Use(cors.New(cors.Config{
|
||||
AllowOrigins: []string{"http://localhost:4200", "http://127.0.0.1:8888", "http://localhost:8888"},
|
||||
AllowMethods: []string{"GET", "POST", "OPTIONS"},
|
||||
AllowHeaders: []string{"Authorization", "Content-Type"},
|
||||
AllowCredentials: true,
|
||||
}))
|
||||
|
||||
// Применяем middleware для защиты HTML страниц
|
||||
r.Use(staticAuthMiddleware(pass))
|
||||
|
||||
// API роуты с авторизацией
|
||||
api := r.Group("/api/v1/knock-actions")
|
||||
api.Use(authMiddleware(pass))
|
||||
|
||||
// Настраиваем роуты
|
||||
setupKnockRoutes(api)
|
||||
setupCryptoRoutes(api, passHash)
|
||||
setupStaticRoutes(r, embeddedFS)
|
||||
|
||||
fmt.Printf("Serving on :%s\n", port)
|
||||
return r.Run(":" + port)
|
||||
}
|
107
back/cmd/static_routes.go
Normal file
107
back/cmd/static_routes.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// embeddedFS объявлен в serve.go
|
||||
|
||||
// setupStaticRoutes настраивает роуты для статических файлов
|
||||
func setupStaticRoutes(r *gin.Engine, embeddedFS embed.FS) {
|
||||
// Получаем подфайловую систему для public
|
||||
sub, err := fs.Sub(embeddedFS, "public")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Обработчик для всех остальных маршрутов (статические файлы)
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
path := c.Request.URL.Path
|
||||
|
||||
// Убираем ведущий слеш для fs.Sub
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
|
||||
// Если путь пустой, показываем index.html
|
||||
if path == "" {
|
||||
path = "index.html"
|
||||
}
|
||||
|
||||
// Читаем файл из встроенной файловой системы
|
||||
data, err := fs.ReadFile(sub, path)
|
||||
if err != nil {
|
||||
// Если файл не найден, показываем index.html (SPA routing)
|
||||
if strings.Contains(path, ".") {
|
||||
// Это файл с расширением, возвращаем 404
|
||||
c.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
// Это маршрут SPA, показываем index.html
|
||||
data, err = fs.ReadFile(sub, "index.html")
|
||||
if err != nil {
|
||||
c.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Определяем Content-Type по расширению
|
||||
contentType := getContentType(path)
|
||||
c.Header("Content-Type", contentType)
|
||||
|
||||
// Специальные заголовки для шрифтов
|
||||
if isFontFile(path) {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
c.Header("Cache-Control", "public, max-age=31536000")
|
||||
}
|
||||
|
||||
c.Data(http.StatusOK, contentType, data)
|
||||
})
|
||||
}
|
||||
|
||||
// getContentType определяет Content-Type по расширению файла
|
||||
func getContentType(path string) string {
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
switch ext {
|
||||
case ".html":
|
||||
return "text/html; charset=utf-8"
|
||||
case ".css":
|
||||
return "text/css; charset=utf-8"
|
||||
case ".js":
|
||||
return "application/javascript; charset=utf-8"
|
||||
case ".json":
|
||||
return "application/json; charset=utf-8"
|
||||
case ".png":
|
||||
return "image/png"
|
||||
case ".jpg", ".jpeg":
|
||||
return "image/jpeg"
|
||||
case ".gif":
|
||||
return "image/gif"
|
||||
case ".svg":
|
||||
return "image/svg+xml"
|
||||
case ".ico":
|
||||
return "image/x-icon"
|
||||
case ".woff":
|
||||
return "font/woff"
|
||||
case ".woff2":
|
||||
return "font/woff2"
|
||||
case ".ttf":
|
||||
return "font/ttf"
|
||||
case ".eot":
|
||||
return "application/vnd.ms-fontobject"
|
||||
case ".webmanifest":
|
||||
return "application/manifest+json"
|
||||
default:
|
||||
return "application/octet-stream"
|
||||
}
|
||||
}
|
||||
|
||||
// isFontFile проверяет, является ли файл шрифтом
|
||||
func isFontFile(path string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
return ext == ".woff" || ext == ".woff2" || ext == ".ttf" || ext == ".eot"
|
||||
}
|
Reference in New Issue
Block a user