init
This commit is contained in:
642
back/internal/knocker.go
Normal file
642
back/internal/knocker.go
Normal file
@@ -0,0 +1,642 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/sha256"
|
||||
_ "embed"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
//go:embed jokes.md
|
||||
var jokesFile string
|
||||
|
||||
func GetRandomJoke() string {
|
||||
// Инициализируем генератор случайных чисел
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
jokes := strings.Split(jokesFile, "**********")
|
||||
|
||||
var cleanJokes []string
|
||||
for _, joke := range jokes {
|
||||
if trimmed := strings.TrimSpace(joke); trimmed != "" {
|
||||
cleanJokes = append(cleanJokes, trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
if len(cleanJokes) == 0 {
|
||||
return "Шутки не найдены"
|
||||
}
|
||||
|
||||
return cleanJokes[rand.Intn(len(cleanJokes))]
|
||||
}
|
||||
|
||||
const (
|
||||
// Системная переменная для ключа шифрования
|
||||
EncryptionKeyEnvVar = "GO_KNOCKER_SERVE_PASS"
|
||||
)
|
||||
|
||||
// Config представляет конфигурацию port knocking
|
||||
type Config struct {
|
||||
Targets []Target `yaml:"targets"`
|
||||
}
|
||||
|
||||
// Target представляет цель для port knocking
|
||||
type Target struct {
|
||||
Host string `yaml:"host"`
|
||||
Ports []int `yaml:"ports"`
|
||||
Protocol string `yaml:"protocol"` // "tcp" или "udp"
|
||||
Delay Duration `yaml:"delay"` // задержка между пакетами
|
||||
WaitConnection bool `yaml:"wait_connection"` // ждать ли установления соединения
|
||||
Gateway string `yaml:"gateway"` // шлюз для отправки (опционально)
|
||||
}
|
||||
|
||||
// Duration для поддержки YAML десериализации времени
|
||||
type Duration time.Duration
|
||||
|
||||
func (d *Duration) UnmarshalYAML(value *yaml.Node) error {
|
||||
var str string
|
||||
if err := value.Decode(&str); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
duration, err := time.ParseDuration(str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*d = Duration(duration)
|
||||
return nil
|
||||
}
|
||||
|
||||
// PortKnocker основная структура для выполнения port knocking
|
||||
type PortKnocker struct{}
|
||||
|
||||
// NewPortKnocker создает новый экземпляр PortKnocker
|
||||
func NewPortKnocker() *PortKnocker {
|
||||
return &PortKnocker{}
|
||||
}
|
||||
|
||||
// Execute выполняет port knocking на основе конфигурации
|
||||
func (pk *PortKnocker) Execute(configFile, keyFile string, verbose bool, globalWaitConnection bool) error {
|
||||
// Читаем конфигурацию
|
||||
config, err := pk.loadConfig(configFile, keyFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка загрузки конфигурации: %w", err)
|
||||
}
|
||||
|
||||
return pk.ExecuteWithConfig(config, verbose, globalWaitConnection)
|
||||
}
|
||||
|
||||
// ExecuteWithConfig выполняет port knocking с готовой конфигурацией
|
||||
func (pk *PortKnocker) ExecuteWithConfig(config *Config, verbose bool, globalWaitConnection bool) error {
|
||||
if verbose {
|
||||
fmt.Printf("Загружена конфигурация с %d целей\n", len(config.Targets))
|
||||
}
|
||||
|
||||
// Выполняем port knocking для каждой цели
|
||||
for i, target := range config.Targets {
|
||||
if verbose {
|
||||
fmt.Printf("Цель %d/%d: %s:%v (%s)\n", i+1, len(config.Targets), target.Host, target.Ports, target.Protocol)
|
||||
}
|
||||
|
||||
// Применяем глобальный флаг если не задан локально
|
||||
if globalWaitConnection && !target.WaitConnection {
|
||||
target.WaitConnection = true
|
||||
}
|
||||
|
||||
if err := pk.knockTarget(target, verbose); err != nil {
|
||||
return fmt.Errorf("ошибка при knocking цели %s: %w", target.Host, err)
|
||||
}
|
||||
|
||||
// Добавляем задержку между целями (кроме последней)
|
||||
if i < len(config.Targets)-1 && target.Delay > 0 {
|
||||
if verbose {
|
||||
fmt.Printf("Ожидание %v перед следующей целью...\n", time.Duration(target.Delay))
|
||||
}
|
||||
time.Sleep(time.Duration(target.Delay))
|
||||
}
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Println("Port knocking завершен успешно")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadConfig загружает конфигурацию из файла с поддержкой шифрования
|
||||
func (pk *PortKnocker) loadConfig(configFile, keyFile string) (*Config, error) {
|
||||
data, err := os.ReadFile(configFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось прочитать файл конфигурации: %w", err)
|
||||
}
|
||||
|
||||
// Проверяем, зашифрован ли файл (начинается с "ENCRYPTED:")
|
||||
if strings.HasPrefix(string(data), "ENCRYPTED:") {
|
||||
fmt.Println("Обнаружен зашифрованный файл конфигурации")
|
||||
|
||||
// Получаем ключ шифрования
|
||||
key, err := pk.getEncryptionKey(keyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось получить ключ шифрования: %w", err)
|
||||
}
|
||||
|
||||
// Расшифровываем данные
|
||||
decryptedData, err := pk.decrypt(data[10:], key) // пропускаем "ENCRYPTED:"
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось расшифровать конфигурацию: %w", err)
|
||||
}
|
||||
data = decryptedData
|
||||
}
|
||||
|
||||
// Парсим YAML
|
||||
var config Config
|
||||
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||
return nil, fmt.Errorf("не удалось разобрать YAML: %w", err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// LoadConfigFromString загружает конфигурацию из строки YAML
|
||||
func LoadConfigFromString(yamlStr string) (*Config, error) {
|
||||
// Проверяем, зашифрована ли строка (начинается с "ENCRYPTED:")
|
||||
if strings.HasPrefix(yamlStr, "ENCRYPTED:") {
|
||||
// Создаем временный PortKnocker для расшифровки
|
||||
pk := NewPortKnocker()
|
||||
|
||||
// Получаем ключ шифрования
|
||||
key, err := pk.getEncryptionKey("")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось получить ключ шифрования: %w", err)
|
||||
}
|
||||
|
||||
// Расшифровываем данные
|
||||
decryptedData, err := pk.decrypt([]byte(yamlStr[10:]), key) // пропускаем "ENCRYPTED:"
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось расшифровать конфигурацию: %w", err)
|
||||
}
|
||||
yamlStr = string(decryptedData)
|
||||
}
|
||||
|
||||
// Парсим YAML
|
||||
var config Config
|
||||
if err := yaml.Unmarshal([]byte(yamlStr), &config); err != nil {
|
||||
return nil, fmt.Errorf("не удалось разобрать YAML: %w", err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// getEncryptionKey получает ключ шифрования из файла или системной переменной и хеширует его
|
||||
func (pk *PortKnocker) getEncryptionKey(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(EncryptionKeyEnvVar)
|
||||
if key == "" {
|
||||
return nil, fmt.Errorf("ключ шифрования не найден ни в файле, ни в переменной %s", EncryptionKeyEnvVar)
|
||||
}
|
||||
rawKey = []byte(key)
|
||||
}
|
||||
|
||||
// Хешируем ключ SHA256 чтобы получить всегда 32 байта для AES-256
|
||||
hash := sha256.Sum256(rawKey)
|
||||
return hash[:], nil
|
||||
}
|
||||
|
||||
// decrypt расшифровывает данные с помощью AES-GCM
|
||||
func (pk *PortKnocker) decrypt(encryptedData []byte, key []byte) ([]byte, error) {
|
||||
// Декодируем base64
|
||||
data, err := base64.StdEncoding.DecodeString(string(encryptedData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось декодировать base64: %w", err)
|
||||
}
|
||||
|
||||
// Создаем AES cipher
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось создать AES cipher: %w", err)
|
||||
}
|
||||
|
||||
// Создаем GCM
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось создать GCM: %w", err)
|
||||
}
|
||||
|
||||
// Извлекаем nonce
|
||||
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
|
||||
}
|
||||
|
||||
// knockTarget выполняет port knocking для одной цели
|
||||
func (pk *PortKnocker) knockTarget(target Target, verbose bool) error {
|
||||
// Проверяем на "шутливую" цель 1
|
||||
if target.Host == "8.8.8.8" && len(target.Ports) == 1 && target.Ports[0] == 8888 {
|
||||
pk.showEasterEgg()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Проверяем на "шутливую" цель 2
|
||||
if target.Host == "1.1.1.1" && len(target.Ports) == 1 && target.Ports[0] == 1111 {
|
||||
pk.showRandomJoke()
|
||||
return nil
|
||||
}
|
||||
|
||||
protocol := strings.ToLower(target.Protocol)
|
||||
if protocol != "tcp" && protocol != "udp" {
|
||||
return fmt.Errorf("неподдерживаемый протокол: %s", target.Protocol)
|
||||
}
|
||||
|
||||
// Вычисляем таймаут как половину интервала между пакетами
|
||||
timeout := time.Duration(target.Delay) / 2
|
||||
if timeout < 100*time.Millisecond {
|
||||
timeout = 100 * time.Millisecond // минимальный таймаут
|
||||
}
|
||||
|
||||
for i, port := range target.Ports {
|
||||
if verbose {
|
||||
fmt.Printf(" Отправка пакета на %s:%d (%s)\n", target.Host, port, protocol)
|
||||
}
|
||||
|
||||
if err := pk.sendPacket(target.Host, port, protocol, target.WaitConnection, timeout, target.Gateway); err != nil {
|
||||
if target.WaitConnection {
|
||||
return fmt.Errorf("ошибка отправки пакета на порт %d: %w", port, err)
|
||||
} else {
|
||||
if verbose {
|
||||
fmt.Printf(" Предупреждение: не удалось отправить пакет на порт %d: %v\n", port, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Задержка между пакетами (кроме последнего)
|
||||
if i < len(target.Ports)-1 {
|
||||
delay := time.Duration(target.Delay)
|
||||
if delay > 0 {
|
||||
if verbose {
|
||||
fmt.Printf(" Ожидание %v...\n", delay)
|
||||
}
|
||||
time.Sleep(delay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendPacket отправляет один пакет на указанный хост и порт
|
||||
func (pk *PortKnocker) sendPacket(host string, port int, protocol string, waitConnection bool, timeout time.Duration, gateway string) error {
|
||||
address := net.JoinHostPort(host, fmt.Sprintf("%d", port))
|
||||
|
||||
var conn net.Conn
|
||||
var err error
|
||||
|
||||
// Настройка локального адреса если указан шлюз
|
||||
var localAddr net.Addr
|
||||
if gateway != "" {
|
||||
if strings.Contains(gateway, ":") {
|
||||
localAddr, err = net.ResolveTCPAddr("tcp", gateway)
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось разрешить адрес шлюза %s: %w", gateway, err)
|
||||
}
|
||||
} else {
|
||||
// Если указан только IP, добавляем порт 0
|
||||
localAddr, err = net.ResolveTCPAddr("tcp", gateway+":0")
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось разрешить адрес шлюза %s: %w", gateway, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch protocol {
|
||||
case "tcp":
|
||||
if localAddr != nil {
|
||||
dialer := &net.Dialer{
|
||||
LocalAddr: localAddr,
|
||||
Timeout: timeout,
|
||||
}
|
||||
conn, err = dialer.Dial("tcp", address)
|
||||
} else {
|
||||
conn, err = net.DialTimeout("tcp", address, timeout)
|
||||
}
|
||||
case "udp":
|
||||
if localAddr != nil {
|
||||
dialer := &net.Dialer{
|
||||
LocalAddr: localAddr,
|
||||
Timeout: timeout,
|
||||
}
|
||||
conn, err = dialer.Dial("udp", address)
|
||||
} else {
|
||||
conn, err = net.DialTimeout("udp", address, timeout)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("неподдерживаемый протокол: %s", protocol)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if waitConnection {
|
||||
return fmt.Errorf("не удалось подключиться к %s: %w", address, err)
|
||||
} else {
|
||||
// Для UDP и TCP без ожидания соединения просто отправляем пакет
|
||||
return pk.sendPacketWithoutConnection(host, port, protocol, localAddr)
|
||||
}
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Отправляем пустой пакет
|
||||
_, err = conn.Write([]byte{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось отправить пакет: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendPacketWithoutConnection отправляет пакет без установления соединения
|
||||
func (pk *PortKnocker) sendPacketWithoutConnection(host string, port int, protocol string, localAddr net.Addr) error {
|
||||
address := net.JoinHostPort(host, fmt.Sprintf("%d", port))
|
||||
|
||||
switch protocol {
|
||||
case "udp":
|
||||
// Для UDP просто отправляем пакет
|
||||
var conn net.Conn
|
||||
var err error
|
||||
|
||||
if localAddr != nil {
|
||||
dialer := &net.Dialer{
|
||||
LocalAddr: localAddr,
|
||||
}
|
||||
conn, err = dialer.Dial("udp", address)
|
||||
} else {
|
||||
conn, err = net.Dial("udp", address)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось создать UDP соединение к %s: %w", address, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = conn.Write([]byte{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось отправить UDP пакет: %w", err)
|
||||
}
|
||||
|
||||
case "tcp":
|
||||
// Для TCP без ожидания соединения используем короткий таймаут
|
||||
var conn net.Conn
|
||||
var err error
|
||||
|
||||
if localAddr != nil {
|
||||
dialer := &net.Dialer{
|
||||
LocalAddr: localAddr,
|
||||
Timeout: 100 * time.Millisecond,
|
||||
}
|
||||
conn, err = dialer.Dial("tcp", address)
|
||||
} else {
|
||||
conn, err = net.DialTimeout("tcp", address, 100*time.Millisecond)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// Для TCP без ожидания соединения игнорируем ошибки подключения
|
||||
return nil
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = conn.Write([]byte{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось отправить TCP пакет: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// showEasterEgg показывает забавный ASCII-арт
|
||||
func (pk *PortKnocker) showEasterEgg() {
|
||||
fmt.Println("\n🎯 🎯 🎯 EASTER EGG ACTIVATED! 🎯 🎯 🎯")
|
||||
fmt.Println()
|
||||
|
||||
// Анимированный ASCII-арт
|
||||
frames := []string{
|
||||
`
|
||||
╭─────────────────╮
|
||||
│ 🚀 PORT │
|
||||
│ KNOCKER │
|
||||
│ 🎯 1.0.1 │
|
||||
│ │
|
||||
│ 🎮 GAME ON! │
|
||||
╰─────────────────╯
|
||||
`,
|
||||
`
|
||||
╭─────────────────╮
|
||||
│ 🚀 PORT │
|
||||
│ KNOCKER │
|
||||
│ 🎯 1.0.1 │
|
||||
│ │
|
||||
│ 🎯 BULLSEYE! │
|
||||
╰─────────────────╯
|
||||
`,
|
||||
`
|
||||
╭─────────────────╮
|
||||
│ 🚀 PORT │
|
||||
│ KNOCKER │
|
||||
│ 🎯 1.0.1 │
|
||||
│ │
|
||||
│ 🎪 MAGIC! │
|
||||
╰─────────────────╯
|
||||
`,
|
||||
}
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
fmt.Print("\033[2J\033[H") // Очистка экрана
|
||||
fmt.Println(frames[i%len(frames)])
|
||||
time.Sleep(1500 * time.Millisecond)
|
||||
}
|
||||
|
||||
fmt.Println("\n🎉 Поздравляем! Вы нашли пасхалку!")
|
||||
fmt.Println("🎯 Попробуйте: ./port-knocker -t \"tcp:8.8.8.8:8888\"")
|
||||
fmt.Println("🚀 Port Knocker - теперь с пасхалками!")
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func (pk *PortKnocker) showRandomJoke() {
|
||||
joke := GetRandomJoke()
|
||||
|
||||
// ANSI цветовые коды
|
||||
const (
|
||||
colorReset = "\033[0m"
|
||||
colorRed = "\033[31m"
|
||||
colorGreen = "\033[32m"
|
||||
colorYellow = "\033[33m"
|
||||
colorBlue = "\033[34m"
|
||||
colorPurple = "\033[35m"
|
||||
colorCyan = "\033[36m"
|
||||
colorWhite = "\033[37m"
|
||||
colorBold = "\033[1m"
|
||||
)
|
||||
|
||||
// Функция для подсчета видимой длины строки (без ANSI кодов) в рунах
|
||||
visibleLength := func(s string) int {
|
||||
// Удаляем ANSI escape последовательности
|
||||
clean := s
|
||||
for strings.Contains(clean, "\033[") {
|
||||
start := strings.Index(clean, "\033[")
|
||||
end := strings.Index(clean[start:], "m")
|
||||
if end == -1 {
|
||||
break
|
||||
}
|
||||
clean = clean[:start] + clean[start+end+1:]
|
||||
}
|
||||
// Возвращаем количество рун, а не байт
|
||||
return len([]rune(clean))
|
||||
}
|
||||
|
||||
// Функция для умного разбиения строки
|
||||
splitLine := func(line string, maxWidth int) []string {
|
||||
runes := []rune(line)
|
||||
if len(runes) <= maxWidth {
|
||||
return []string{line}
|
||||
}
|
||||
|
||||
var result []string
|
||||
remaining := line
|
||||
|
||||
for len([]rune(remaining)) > maxWidth {
|
||||
// Ищем позицию для разрыва в пределах maxWidth
|
||||
breakPos := maxWidth
|
||||
remainingRunes := []rune(remaining)
|
||||
|
||||
for i := maxWidth; i >= 0; i-- {
|
||||
if i < len(remainingRunes) {
|
||||
char := remainingRunes[i]
|
||||
// Разрываем на пробеле, знаке пунктуации или в конце строки
|
||||
if char == ' ' || char == ',' || char == '.' || char == '!' ||
|
||||
char == '?' || char == ':' || char == ';' || char == '-' {
|
||||
breakPos = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Если не нашли подходящего места, разрываем по maxWidth
|
||||
if breakPos == maxWidth {
|
||||
breakPos = maxWidth
|
||||
}
|
||||
|
||||
// Создаем строку из рун
|
||||
breakString := string(remainingRunes[:breakPos])
|
||||
result = append(result, strings.TrimSpace(breakString))
|
||||
remaining = strings.TrimSpace(string(remainingRunes[breakPos:]))
|
||||
}
|
||||
|
||||
if len([]rune(remaining)) > 0 {
|
||||
result = append(result, remaining)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Разбиваем исходную шутку на строки
|
||||
originalLines := strings.Split(joke, "\n")
|
||||
|
||||
// Обрабатываем каждую строку и разбиваем длинные
|
||||
var processedLines []string
|
||||
for _, line := range originalLines {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
splitLines := splitLine(line, 80)
|
||||
processedLines = append(processedLines, splitLines...)
|
||||
}
|
||||
|
||||
// Находим максимальную длину строки для рамки (в рунах)
|
||||
maxLength := 0
|
||||
for _, line := range processedLines {
|
||||
lineLength := len([]rune(line))
|
||||
if lineLength > maxLength {
|
||||
maxLength = lineLength
|
||||
}
|
||||
}
|
||||
|
||||
// Убеждаемся, что maxLength не меньше минимальной ширины для заголовков
|
||||
minWidth := 60 // Минимальная ширина для заголовков
|
||||
if maxLength < minWidth {
|
||||
maxLength = minWidth
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("%s%s╭%s", colorPurple, colorBold, colorReset)
|
||||
fmt.Printf("%s%s", colorYellow, strings.Repeat("─", maxLength+2))
|
||||
fmt.Printf("%s%s╮%s\n", colorPurple, colorBold, colorReset)
|
||||
|
||||
headerText := " Зацени Анектотец! 🤣 "
|
||||
fmt.Printf("%s%s│%s", colorPurple, colorBold, colorReset)
|
||||
fmt.Printf("%s%s%s%s", colorCyan, colorBold, headerText, colorReset)
|
||||
fmt.Printf("%s%s", colorYellow, strings.Repeat(" ", 1+maxLength-visibleLength(headerText)))
|
||||
fmt.Printf("%s%s│%s\n", colorPurple, colorBold, colorReset)
|
||||
|
||||
fmt.Printf("%s%s├%s", colorPurple, colorBold, colorReset)
|
||||
fmt.Printf("%s%s", colorYellow, strings.Repeat("─", maxLength+2))
|
||||
fmt.Printf("%s%s┤%s\n", colorPurple, colorBold, colorReset)
|
||||
|
||||
// Выводим обработанные строки шутки
|
||||
for _, line := range processedLines {
|
||||
fmt.Printf("%s%s│%s", colorPurple, colorBold, colorReset)
|
||||
fmt.Printf("%s%s%s", colorWhite, line, colorReset)
|
||||
fmt.Printf("%s%s", colorYellow, strings.Repeat(" ", 2+maxLength-len([]rune(line))))
|
||||
fmt.Printf("%s%s│%s\n", colorPurple, colorBold, colorReset)
|
||||
}
|
||||
|
||||
fmt.Printf("%s%s├%s", colorPurple, colorBold, colorReset)
|
||||
fmt.Printf("%s%s", colorYellow, strings.Repeat("─", maxLength+2))
|
||||
fmt.Printf("%s%s┤%s\n", colorPurple, colorBold, colorReset)
|
||||
|
||||
// Вычисляем правильную ширину для нижних строк
|
||||
cmdText := "Попробуйте: ./port-knocker -t \"tcp:1.1.1.1:1111\""
|
||||
titleText := "🚀 Port Knocker - теперь с шутками! 🤣"
|
||||
|
||||
fmt.Printf("%s%s│%s", colorPurple, colorBold, colorReset)
|
||||
fmt.Printf("%s%s%s%s", colorGreen, colorBold, cmdText, colorReset)
|
||||
fmt.Printf("%s%s", colorYellow, strings.Repeat(" ", 2+maxLength-visibleLength(cmdText)))
|
||||
fmt.Printf("%s%s│%s\n", colorPurple, colorBold, colorReset)
|
||||
|
||||
fmt.Printf("%s%s│%s", colorPurple, colorBold, colorReset)
|
||||
fmt.Printf("%s%s%s%s", colorBlue, colorBold, titleText, colorReset)
|
||||
fmt.Printf("%s%s", colorYellow, strings.Repeat(" ", maxLength-visibleLength(titleText)))
|
||||
fmt.Printf("%s%s│%s\n", colorPurple, colorBold, colorReset)
|
||||
|
||||
fmt.Printf("%s%s╰%s", colorPurple, colorBold, colorReset)
|
||||
fmt.Printf("%s%s", colorYellow, strings.Repeat("─", maxLength+2))
|
||||
fmt.Printf("%s%s╯%s\n", colorPurple, colorBold, colorReset)
|
||||
fmt.Println()
|
||||
}
|
Reference in New Issue
Block a user