This commit is contained in:
66 changed files with 19815 additions and 0 deletions

44
back/.gitignore vendored Normal file
View File

@@ -0,0 +1,44 @@
# Исключить все YAML файлы содержащие "knock" в имени
*knock*.yaml
*knock*.yml
# Исключить бинарные файлы
port-knocker
port-knocker-*
*.exe
# Исключить зашифрованные конфиги
*.encrypted
# Go artifacts
*.so
*.dylib
*.test
*.out
go.work
# IDE и редакторы
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Логи и временные файлы
*.log
*.tmp
*.temp
# Персональные конфиги и ключи
key.txt
*.key
*secret*

95
back/Makefile Normal file
View File

@@ -0,0 +1,95 @@
.PHONY: build build-linux build-windows clean test help
# Переменные
BINARY_NAME=port-knocker
VERSION=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
BUILD_TIME=$(shell date -u '+%Y-%m-%d_%H:%M:%S')
LDFLAGS=-ldflags "-X main.Version=${VERSION} -X main.BuildTime=${BUILD_TIME} -s -w"
# Цвета для вывода
GREEN=\033[0;32m
NC=\033[0m # No Color
help: ## Показать справку
@echo "$(GREEN)Port Knocker - Утилита для port knocking$(NC)"
@echo ""
@echo "Доступные команды:"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " $(GREEN)%-15s$(NC) %s\n", $$1, $$2}'
build: ## Собрать для текущей платформы
@echo "$(GREEN)Сборка для текущей платформы...$(NC)"
go build ${LDFLAGS} -o ${BINARY_NAME} .
build-linux: ## Собрать для Linux (amd64)
@echo "$(GREEN)Сборка для Linux (amd64)...$(NC)"
GOOS=linux GOARCH=amd64 go build ${LDFLAGS} -o ${BINARY_NAME}-linux-amd64 .
build-windows: ## Собрать для Windows (amd64)
@echo "$(GREEN)Сборка для Windows (amd64)...$(NC)"
GOOS=windows GOARCH=amd64 go build ${LDFLAGS} -o ${BINARY_NAME}-windows-amd64.exe .
build-all: build-linux build-windows ## Собрать для всех платформ
test: ## Запустить тесты
@echo "$(GREEN)Запуск тестов...$(NC)"
go test -v ./...
clean: ## Очистить собранные файлы
@echo "$(GREEN)Очистка...$(NC)"
rm -f ${BINARY_NAME}*
rm -f *.exe
install: build ## Установить в систему
@echo "$(GREEN)Установка...$(NC)"
sudo cp ${BINARY_NAME} /usr/local/bin/
uninstall: ## Удалить из системы
@echo "$(GREEN)Удаление...$(NC)"
sudo rm -f /usr/local/bin/${BINARY_NAME}
deps: ## Установить зависимости
@echo "$(GREEN)Установка зависимостей...$(NC)"
go mod tidy
go mod download
example-encrypt: ## Пример шифрования конфигурации
@echo "$(GREEN)Шифрование примера конфигурации...$(NC)"
./${BINARY_NAME} encrypt -c examples/config.yaml -o examples/config.encrypted -k examples/key.txt
example-decrypt: ## Пример расшифровки зашифрованной конфигурации
@echo "$(GREEN)Расшифровка зашифрованной конфигурации...$(NC)"
./${BINARY_NAME} decrypt -c examples/config.encrypted -o examples/config.decrypted.yaml -k examples/key.txt
example-encrypt-alt: ## Пример шифрования с опцией -i
@echo "$(GREEN)Шифрование с опцией -i...$(NC)"
./${BINARY_NAME} encrypt -i examples/config.yaml -o examples/config.encrypted -k examples/key.txt
example-run: ## Пример запуска с обычной конфигурацией
@echo "$(GREEN)Запуск с обычной конфигурацией...$(NC)"
./${BINARY_NAME} -c examples/config.yaml -v
example-inline: ## Пример запуска с инлайн целями
@echo "$(GREEN)Запуск с инлайн целями...$(NC)"
./${BINARY_NAME} -t "tcp:192.168.1.1:22;tcp:192.168.1.1:80;udp:192.168.1.1:53" -d 500ms -v
example-inline-simple: ## Простой пример с одной целью
@echo "$(GREEN)Простой пример с одной целью...$(NC)"
./${BINARY_NAME} -t "tcp:127.0.0.1:22" -v
release-tag: ## Создать git tag для release (например: make release-tag VERSION=v1.0.0)
@if [ -z "$(VERSION)" ]; then \
echo "Использование: make release-tag VERSION=v1.0.0"; \
exit 1; \
fi
@echo "$(GREEN)Создание тега $(VERSION)...$(NC)"
git tag -a $(VERSION) -m "Release $(VERSION)"
git push origin $(VERSION)
check-git: ## Проверить git статус перед коммитом
@echo "$(GREEN)Проверка git статуса...$(NC)"
git status
@echo ""
@echo "$(GREEN)Файлы в .gitignore:$(NC)"
@echo "- *knock*.yaml (конфигурации с 'knock' в имени)"
@echo "- *.encrypted (зашифрованные файлы)"
@echo "- Бинарные файлы и ключи"

331
back/README.md Normal file
View File

@@ -0,0 +1,331 @@
# Port Knocker
Утилита для отправки port knocking пакетов на удаленные серверы с поддержкой шифрования конфигурации.
**Версия**: 1.0.10
## Возможности
- ✅ Отправка TCP и UDP пакетов
- ✅ Настраиваемые последовательности портов
- ✅ Зашифрованные конфигурационные файлы
- ✅ Автоматическое определение зашифрованных файлов
- ✅ Ключи шифрования из файла или системной переменной
- ✅ Кроссплатформенная сборка (Linux, Windows, macOS)
- ✅ Совместимость со старыми версиями ОС (Ubuntu 18.04+)
- ✅ Подробный вывод для отладки
-**Расшифровка зашифрованных конфигов в открытый YAML (команда decrypt)**
-**Пасхалка для любознательных пользователей** 🎯
## Установка
### Сборка из исходников
```bash
# Клонировать репозиторий
git clone <repository-url>
cd port-knocker
# Установить зависимости
make deps
# Собрать для текущей платформы
make build
# Или собрать для всех платформ
make build-all
```
### Установка в систему
```bash
make install
```
## Использование
### Основная команда
```bash
# С файлом конфигурации
port-knocker -c config.yaml [-k key.txt] [-v]
# С инлайн целями
port-knocker -t "tcp:host:port;udp:host:port" [-d delay] [-v]
```
### Параметры
- `-c, --config` - Путь к файлу конфигурации
- `-t, --targets` - Инлайн цели в формате `[proto]:[host]:[port];[proto]:[host]:[port]`
- `-d, --delay` - Задержка между пакетами (по умолчанию 1s)
- `-k, --key` - Путь к файлу ключа шифрования
- `-v, --verbose` - Подробный вывод
- `-w, --wait-connection` - Ждать установления соединения
**Примечание**: Нужно указать либо `-c` (файл), либо `-t` (инлайн цели), но не оба одновременно.
### Шифрование конфигурации
```bash
# Используя глобальную опцию --config
port-knocker encrypt -c config.yaml -o config.encrypted -k key.txt
# Или используя опцию -i
port-knocker encrypt -i config.yaml -o config.encrypted -k key.txt
```
### **Расшифровка зашифрованного конфига**
```bash
# Используя глобальную опцию --config
port-knocker decrypt -c config.encrypted -o config.decrypted.yaml -k key.txt
# Или используя опцию -i
port-knocker decrypt -i config.encrypted -o config.decrypted.yaml -k key.txt
```
- `-c/--config` или `-i/--input` — путь к файлу (если не указан -i, используется --config)
- `-o/--output` — путь к выходному файлу
- `-k/--key` — путь к ключу (или используйте переменную окружения GO_KNOCKER_SERVE_PASS)
**Важно**: Ключ любой длины автоматически хешируется SHA256 до 32 байт для AES-256.
## Конфигурация
Конфигурационный файл должен быть в формате YAML:
```yaml
targets:
- host: "192.168.1.100"
ports: [1000, 2000, 3000]
protocol: "tcp"
delay: "1s"
- host: "10.0.0.50"
ports: [5000, 6000, 7000, 8000]
protocol: "udp"
delay: "500ms"
```
### Параметры цели
- `host` - IP-адрес или доменное имя цели
- `ports` - Массив портов для knocking
- `protocol` - Протокол: `tcp` или `udp`
- `delay` - Задержка между пакетами (например: `1s`, `500ms`, `2m`)
## Шифрование
### Создание ключа
Ключ может быть любой длины (автоматически хешируется до 32 байт):
```bash
# Создать ключ в файле (любая длина)
echo "my-secret-password" > key.txt
# Или установить системную переменную
export GO_KNOCKER_SERVE_PASS="my-secret-password"
# Можно использовать длинные пароли
echo "this-is-a-very-long-password-that-will-be-hashed-to-32-bytes" > key.txt
```
### Шифрование конфигурации 1
```bash
# Шифрование с ключом из файла
port-knocker encrypt -c config.yaml -o config.encrypted -k key.txt
# Шифрование с ключом из системной переменной
export GO_KNOCKER_SERVE_PASS="my-secret-password"
port-knocker encrypt -c config.yaml -o config.encrypted
```
### **Расшифровка зашифрованной конфигурации**
```bash
# Расшифровка с ключом из файла
port-knocker decrypt -c config.encrypted -o config.decrypted.yaml -k key.txt
# Расшифровка с ключом из системной переменной
export GO_KNOCKER_SERVE_PASS="my-secret-password"
port-knocker decrypt -c config.encrypted -o config.decrypted.yaml
```
### Использование зашифрованной конфигурации
```bash
# С ключом из файла
port-knocker -c config.encrypted -k key.txt -v
# С ключом из системной переменной
export GO_KNOCKER_SERVE_PASS="my-secret-key-32-bytes-long!!"
port-knocker -c config.encrypted -v
```
## Примеры
### Пример 1: Быстрое использование с инлайн целями
```bash
# Простая последовательность TCP портов
port-knocker -t "tcp:192.168.1.100:1000;tcp:192.168.1.100:2000;tcp:192.168.1.100:3000" -v
# Смешанные протоколы с настройкой задержки
port-knocker -t "tcp:server.com:22;udp:server.com:53;tcp:server.com:80" -d 500ms -v
# Одиночный порт
port-knocker -t "tcp:192.168.1.1:22" -v
# С ожиданием соединения
port-knocker -t "tcp:192.168.1.100:443" -w -v
```
### Пример 2: Конфигурационный файл
```bash
# Создать конфигурацию
cat > config.yaml << EOF
targets:
- host: "192.168.1.100"
ports: [1000, 2000, 3000]
protocol: "tcp"
delay: "1s"
EOF
# Запустить
port-knocker -c config.yaml -v
```
### Пример 3: Зашифрованная конфигурация
```bash
# Создать ключ
echo "my-secret-password" > key.txt
# Зашифровать конфигурацию
port-knocker encrypt -c config.yaml -o config.encrypted -k key.txt
# Использовать зашифрованную конфигурацию
port-knocker -c config.encrypted -k key.txt -v
# Расшифровать обратно для редактирования
port-knocker decrypt -c config.encrypted -o config.decrypted.yaml -k key.txt
```
### Пример 4: Множественные цели
```bash
cat > config.yaml << EOF
targets:
- host: "server1.example.com"
ports: [22, 80, 443]
protocol: "tcp"
delay: "2s"
- host: "server2.example.com"
ports: [5000, 6000, 7000, 8000]
protocol: "udp"
delay: "500ms"
EOF
port-knocker -c config.yaml -v
```
## 🎯 Пасхалка
Попробуйте найти скрытую функцию! Запустите:
```bash
port-knocker -t "tcp:8.8.8.8:8888" -v
```
И посмотрите, что произойдет! 🚀
## Совместимость
### Поддерживаемые системы
**Linux:**
- Ubuntu 18.04+ (GLIBC 2.27+)
- CentOS 7+ (GLIBC 2.17+)
- Debian 9+ (GLIBC 2.24+)
- RHEL 7+ (GLIBC 2.17+)
**Windows:**
- Windows 7+
- Windows Server 2012+
**macOS:**
- macOS 10.14+ (Mojave)
- macOS 11+ (Big Sur)
- macOS 12+ (Monterey)
### Сборка для старых систем
Для максимальной совместимости бинарники собираются на Ubuntu 18.04 с Go 1.20.
## Безопасность
- Ключи шифрования должны быть достаточно длинными и случайными
- Зашифрованные файлы имеют права доступа 600
- Системная переменная `GO_KNOCKER_SERVE_PASS` должна быть защищена
- Рекомендуется использовать файлы ключей вместо системных переменных в продакшене
## Сборка
### Для Linux
```bash
make build-linux
```
### Для Windows
```bash
make build-windows
```
### Для всех платформ
```bash
make build-all
```
## Разработка
### Запуск тестов
```bash
make test
```
### Очистка
```bash
make clean
```
### Справка
```bash
make help
```
## 📚 Дополнительная документация
Для более подробной информации о создании релизов и других аспектах проекта:
- **[Документация](docs/)** - Подробные инструкции и руководства
- **[Ручное создание релизов](docs/manual-release.md)** - Пошаговая инструкция
- **[Чек-лист релизов](docs/release-checklist.md)** - Быстрый чек-лист
- **[Скрипт быстрого релиза](docs/scripts/quick-release.sh)** - Автоматизация
## Лицензия
MIT License

338
back/bash_completion.sh Normal file
View File

@@ -0,0 +1,338 @@
# bash completion V2 for port-knocker -*- shell-script -*-
__port-knocker_debug()
{
if [[ -n ${BASH_COMP_DEBUG_FILE-} ]]; then
echo "$*" >> "${BASH_COMP_DEBUG_FILE}"
fi
}
# Macs have bash3 for which the bash-completion package doesn't include
# _init_completion. This is a minimal version of that function.
__port-knocker_init_completion()
{
COMPREPLY=()
_get_comp_words_by_ref "$@" cur prev words cword
}
# This function calls the port-knocker program to obtain the completion
# results and the directive. It fills the 'out' and 'directive' vars.
__port-knocker_get_completion_results() {
local requestComp lastParam lastChar args
# Prepare the command to request completions for the program.
# Calling ${words[0]} instead of directly port-knocker allows handling aliases
args=("${words[@]:1}")
requestComp="${words[0]} __complete ${args[*]}"
lastParam=${words[$((${#words[@]}-1))]}
lastChar=${lastParam:$((${#lastParam}-1)):1}
__port-knocker_debug "lastParam ${lastParam}, lastChar ${lastChar}"
if [[ -z ${cur} && ${lastChar} != = ]]; then
# If the last parameter is complete (there is a space following it)
# We add an extra empty parameter so we can indicate this to the go method.
__port-knocker_debug "Adding extra empty parameter"
requestComp="${requestComp} ''"
fi
# When completing a flag with an = (e.g., port-knocker -n=<TAB>)
# bash focuses on the part after the =, so we need to remove
# the flag part from $cur
if [[ ${cur} == -*=* ]]; then
cur="${cur#*=}"
fi
__port-knocker_debug "Calling ${requestComp}"
# Use eval to handle any environment variables and such
out=$(eval "${requestComp}" 2>/dev/null)
# Extract the directive integer at the very end of the output following a colon (:)
directive=${out##*:}
# Remove the directive
out=${out%:*}
if [[ ${directive} == "${out}" ]]; then
# There is not directive specified
directive=0
fi
__port-knocker_debug "The completion directive is: ${directive}"
__port-knocker_debug "The completions are: ${out}"
}
__port-knocker_process_completion_results() {
local shellCompDirectiveError=1
local shellCompDirectiveNoSpace=2
local shellCompDirectiveNoFileComp=4
local shellCompDirectiveFilterFileExt=8
local shellCompDirectiveFilterDirs=16
local shellCompDirectiveKeepOrder=32
if (((directive & shellCompDirectiveError) != 0)); then
# Error code. No completion.
__port-knocker_debug "Received error from custom completion go code"
return
else
if (((directive & shellCompDirectiveNoSpace) != 0)); then
if [[ $(type -t compopt) == builtin ]]; then
__port-knocker_debug "Activating no space"
compopt -o nospace
else
__port-knocker_debug "No space directive not supported in this version of bash"
fi
fi
if (((directive & shellCompDirectiveKeepOrder) != 0)); then
if [[ $(type -t compopt) == builtin ]]; then
# no sort isn't supported for bash less than < 4.4
if [[ ${BASH_VERSINFO[0]} -lt 4 || ( ${BASH_VERSINFO[0]} -eq 4 && ${BASH_VERSINFO[1]} -lt 4 ) ]]; then
__port-knocker_debug "No sort directive not supported in this version of bash"
else
__port-knocker_debug "Activating keep order"
compopt -o nosort
fi
else
__port-knocker_debug "No sort directive not supported in this version of bash"
fi
fi
if (((directive & shellCompDirectiveNoFileComp) != 0)); then
if [[ $(type -t compopt) == builtin ]]; then
__port-knocker_debug "Activating no file completion"
compopt +o default
else
__port-knocker_debug "No file completion directive not supported in this version of bash"
fi
fi
fi
# Separate activeHelp from normal completions
local completions=()
local activeHelp=()
__port-knocker_extract_activeHelp
if (((directive & shellCompDirectiveFilterFileExt) != 0)); then
# File extension filtering
local fullFilter filter filteringCmd
# Do not use quotes around the $completions variable or else newline
# characters will be kept.
for filter in ${completions[*]}; do
fullFilter+="$filter|"
done
filteringCmd="_filedir $fullFilter"
__port-knocker_debug "File filtering command: $filteringCmd"
$filteringCmd
elif (((directive & shellCompDirectiveFilterDirs) != 0)); then
# File completion for directories only
local subdir
subdir=${completions[0]}
if [[ -n $subdir ]]; then
__port-knocker_debug "Listing directories in $subdir"
pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return
else
__port-knocker_debug "Listing directories in ."
_filedir -d
fi
else
__port-knocker_handle_completion_types
fi
__port-knocker_handle_special_char "$cur" :
__port-knocker_handle_special_char "$cur" =
# Print the activeHelp statements before we finish
if ((${#activeHelp[*]} != 0)); then
printf "\n";
printf "%s\n" "${activeHelp[@]}"
printf "\n"
# The prompt format is only available from bash 4.4.
# We test if it is available before using it.
if (x=${PS1@P}) 2> /dev/null; then
printf "%s" "${PS1@P}${COMP_LINE[@]}"
else
# Can't print the prompt. Just print the
# text the user had typed, it is workable enough.
printf "%s" "${COMP_LINE[@]}"
fi
fi
}
# Separate activeHelp lines from real completions.
# Fills the $activeHelp and $completions arrays.
__port-knocker_extract_activeHelp() {
local activeHelpMarker="_activeHelp_ "
local endIndex=${#activeHelpMarker}
while IFS='' read -r comp; do
if [[ ${comp:0:endIndex} == $activeHelpMarker ]]; then
comp=${comp:endIndex}
__port-knocker_debug "ActiveHelp found: $comp"
if [[ -n $comp ]]; then
activeHelp+=("$comp")
fi
else
# Not an activeHelp line but a normal completion
completions+=("$comp")
fi
done <<<"${out}"
}
__port-knocker_handle_completion_types() {
__port-knocker_debug "__port-knocker_handle_completion_types: COMP_TYPE is $COMP_TYPE"
case $COMP_TYPE in
37|42)
# Type: menu-complete/menu-complete-backward and insert-completions
# If the user requested inserting one completion at a time, or all
# completions at once on the command-line we must remove the descriptions.
# https://github.com/spf13/cobra/issues/1508
local tab=$'\t' comp
while IFS='' read -r comp; do
[[ -z $comp ]] && continue
# Strip any description
comp=${comp%%$tab*}
# Only consider the completions that match
if [[ $comp == "$cur"* ]]; then
COMPREPLY+=("$comp")
fi
done < <(printf "%s\n" "${completions[@]}")
;;
*)
# Type: complete (normal completion)
__port-knocker_handle_standard_completion_case
;;
esac
}
__port-knocker_handle_standard_completion_case() {
local tab=$'\t' comp
# Short circuit to optimize if we don't have descriptions
if [[ "${completions[*]}" != *$tab* ]]; then
IFS=$'\n' read -ra COMPREPLY -d '' < <(compgen -W "${completions[*]}" -- "$cur")
return 0
fi
local longest=0
local compline
# Look for the longest completion so that we can format things nicely
while IFS='' read -r compline; do
[[ -z $compline ]] && continue
# Strip any description before checking the length
comp=${compline%%$tab*}
# Only consider the completions that match
[[ $comp == "$cur"* ]] || continue
COMPREPLY+=("$compline")
if ((${#comp}>longest)); then
longest=${#comp}
fi
done < <(printf "%s\n" "${completions[@]}")
# If there is a single completion left, remove the description text
if ((${#COMPREPLY[*]} == 1)); then
__port-knocker_debug "COMPREPLY[0]: ${COMPREPLY[0]}"
comp="${COMPREPLY[0]%%$tab*}"
__port-knocker_debug "Removed description from single completion, which is now: ${comp}"
COMPREPLY[0]=$comp
else # Format the descriptions
__port-knocker_format_comp_descriptions $longest
fi
}
__port-knocker_handle_special_char()
{
local comp="$1"
local char=$2
if [[ "$comp" == *${char}* && "$COMP_WORDBREAKS" == *${char}* ]]; then
local word=${comp%"${comp##*${char}}"}
local idx=${#COMPREPLY[*]}
while ((--idx >= 0)); do
COMPREPLY[idx]=${COMPREPLY[idx]#"$word"}
done
fi
}
__port-knocker_format_comp_descriptions()
{
local tab=$'\t'
local comp desc maxdesclength
local longest=$1
local i ci
for ci in ${!COMPREPLY[*]}; do
comp=${COMPREPLY[ci]}
# Properly format the description string which follows a tab character if there is one
if [[ "$comp" == *$tab* ]]; then
__port-knocker_debug "Original comp: $comp"
desc=${comp#*$tab}
comp=${comp%%$tab*}
# $COLUMNS stores the current shell width.
# Remove an extra 4 because we add 2 spaces and 2 parentheses.
maxdesclength=$(( COLUMNS - longest - 4 ))
# Make sure we can fit a description of at least 8 characters
# if we are to align the descriptions.
if ((maxdesclength > 8)); then
# Add the proper number of spaces to align the descriptions
for ((i = ${#comp} ; i < longest ; i++)); do
comp+=" "
done
else
# Don't pad the descriptions so we can fit more text after the completion
maxdesclength=$(( COLUMNS - ${#comp} - 4 ))
fi
# If there is enough space for any description text,
# truncate the descriptions that are too long for the shell width
if ((maxdesclength > 0)); then
if ((${#desc} > maxdesclength)); then
desc=${desc:0:$(( maxdesclength - 1 ))}
desc+="…"
fi
comp+=" ($desc)"
fi
COMPREPLY[ci]=$comp
__port-knocker_debug "Final comp: $comp"
fi
done
}
__start_port-knocker()
{
local cur prev words cword split
COMPREPLY=()
# Call _init_completion from the bash-completion package
# to prepare the arguments properly
if declare -F _init_completion >/dev/null 2>&1; then
_init_completion -n =: || return
else
__port-knocker_init_completion -n =: || return
fi
__port-knocker_debug
__port-knocker_debug "========= starting completion logic =========="
__port-knocker_debug "cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}, cword is $cword"
# The user could have moved the cursor backwards on the command-line.
# We need to trigger completion from the $cword location, so we need
# to truncate the command-line ($words) up to the $cword location.
words=("${words[@]:0:$cword+1}")
__port-knocker_debug "Truncated words[*]: ${words[*]},"
local out directive
__port-knocker_get_completion_results
__port-knocker_process_completion_results
}
if [[ $(type -t compopt) = "builtin" ]]; then
complete -o default -F __start_port-knocker port-knocker
else
complete -o default -o nospace -F __start_port-knocker port-knocker
fi
# ex: ts=4 sw=4 et filetype=sh

258
back/cmd/crypto_routes.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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"
}

View File

@@ -0,0 +1,22 @@
path: /home/su/projects/articles/embed-gui-article/back/examples/config.yaml
targets:
- host: "192.168.1.100"
ports: [1000, 2000, 3000]
protocol: "tcp"
delay: "1s"
wait_connection: false
gateway: ""
- host: "10.0.0.50"
ports: [5000, 6000, 7000, 8000]
protocol: "udp"
delay: "500ms"
wait_connection: false
gateway: "192.168.1.1"
- host: "example.com"
ports: [22, 80, 443]
protocol: "tcp"
delay: "2s"
wait_connection: false
gateway: "10.0.0.1:8080"

22
back/examples/config.yaml Normal file
View File

@@ -0,0 +1,22 @@
path: /home/su/projects/articles/embed-gui-article/back/examples/config.yaml
targets:
- host: "192.168.1.100"
ports: [1000, 2000, 3000]
protocol: "tcp"
delay: "1s"
wait_connection: false
gateway: ""
- host: "10.0.0.50"
ports: [5000, 6000, 7000, 8000]
protocol: "udp"
delay: "500ms"
wait_connection: false
gateway: "192.168.1.1"
- host: "example.com"
ports: [22, 80, 443]
protocol: "tcp"
delay: "2s"
wait_connection: true
gateway: "10.0.0.1:8080"

41
back/go.mod Normal file
View File

@@ -0,0 +1,41 @@
module port-knocker
go 1.21
require (
github.com/gin-contrib/cors v1.7.2
github.com/gin-gonic/gin v1.10.0
github.com/spf13/cobra v1.8.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
)

107
back/go.sum Normal file
View File

@@ -0,0 +1,107 @@
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw=
github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

561
back/internal/jokes.md Normal file
View File

@@ -0,0 +1,561 @@
**********
На собеседовании в школе для особо одарённых детей шестилетнего Вовочку попросили рассказать, чем автобус отличается от троллейбуса.
Вовочка ничего скрывать от тёти не стал и честно ей сообщил, что автобус работает на двигателе внутреннего сгорания, а троллейбус - на электродвигателе переменного тока.
Оказалось - ничего подобного!
Просто троллейбус с рогами, а автобус - без.
И нечего тут морочить тёте голову!
**********
Гламурная москвичка приезжает погостить в деревню к бабушке.
- Бабуль, куда у вас тут ночью сходить можно?
- В ведро.
**********
Только перечитывая в 40 лет книгу "Д'Артаньян и три мушкетёра" ты наконец-то начинаешь понимать, что единственный положительный герой в этой книге - кардинал Ришелье...
**********
Вместо того, чтобы у себя в Алжире, Марокко и Тунисе строить жизнь, как во Франции, люди приезжают во Францию, чтобы там создать себе такую жизнь, как в Алжире, Марокко и Тунисе. Это странно. Ещё более странно заставлять при этом и французов жить, как в Алжире, Марокко и Тунисе.
**********
Твоя религия ничего МНЕ не запрещает. Она запрещает ТЕБЕ. Уясни это.
**********
Антон Силуанов предложил россиянам не думать о ключевой ставке ЦБ.
Также россиянам не стоит думать о:
- процентах по кредитам;
- курсе рубля;
- тарифах ЖКХ;
- ценах в магазинах.
В то же время россиянам стоит думать о:
- госдолге США;
- крахе доллара;
- величии державы;
- необходимости потерпеть.
**********
Василий Иванович с Петькой сидят, скучают. Василий Иванович:
- Петька, сгоняй на хутор к старику-самогонщику, сообрази чего-нибудь!
- Это можно.
Через час Петька возвращается. Василий Иванович:
- Ну как?
- Да нет у него ни фига.
- Эх, молодежь, всему вас учить надо. Пойдем вместе.
Приходят к деду. Василий Иванович:
- Здорово, отец!
- Здорово, сынки!
- Мы вот к тебе от имени Советской власти. Вот я гляжу, хата у тебя старая.
- Ох, какая старая!
- Петька, запиши: новую хату ему от Советской власти! И жена у тебя вроде старая...
- Ой, какая старая!!
- Петька, запиши: новую жену ему от Советской власти!
- Сынки! Родненькие! Да сколько ж я вас ждал!
Ну, сели за стол, выпили, с собой взяли. Уже в дверях Василий Иванович как бы невзначай:
- Слышь, дед! А может у тебя и /хрен/ старый?
- Уж каккой старый!!!
- Петька, запиши: /хрен/ ему от Советской власти!
**********
Таможенник поднимается на корабль для досмотра.
- Наркотики есть?
Хозяин корабля отвечает:
- Есть. Вот, пожалуйста (достает чемодан). Вот героин, вот кокаин. Все аккуратно упаковано,вот шприц готовый.
Таможенник, вытаращив глаза:
- А может, и оружие есть?
Хозяин (достает другой чемодан):
- Вот Макаров, вот Калашник ,вот патроны к ним. Все как надо.
Таможенник с усмешкой:
- Наверное, и валюта есть?
Хозяин достает третий чемодан:
- Вот миллион долларов, пожалуйста.
Таможенник, ничего не понимая:
- И это все ваше?
Хозяин:
- Нет, это ваше. Мое в трюме.
**********
Летит самолет. Пилот по громкой связи:
- Уважаемые дамы и господа, вас приветствует командир корабля. Прослушайте информацию о нашем полете. Наш полет проходит на высоте 10 тысяч метров со скоростью 900 километров в час, температура за бортом...БЛИН...А-А-А-А!.. ЭТО ЧТО ТАКОЕ?.. НУ, КРЫНДЕЦ...
В салоне гробовая тишина.
Через минуту опять по радио:
- Прошу извинения у уважаемых пассажиров. Просто это наша стюардесса опрокинула на меня горячий кофе. Видели ли бы вы теперь мои белые брюки спереди...
Мужик в первом ряду:
- Твои брюки, это все херня! Видел бы ты мои брюки сзади...
**********
Новый учитель, придя в класс, обнаружил, что одного мальчика дразнят Мойше-дурачок. На перемене он спросил ребят, почему они его так обзывают.
- Да он и вправду дурачок, господин учитель. Если дать ему большую монету в пять шекелей и маленькую в десять, он выберет пять, потому что думает, что она больше. Вот, смотрите...
Парень достает две монеты и предлагает Мойше выбрать. Тот, как всегда,
выбирает пять. Учитель с удивлением спращивает:
- Почему же ты выбрал монету в пять шекелей, а не в десять?
- Посмотрите, она же больше, господин учитель!
После уроков учитель подошел к Мойше.
- Неужели ты не понимаешь, что пять шекелей больше только по размерам,
но на десять шекелей можно купить больше?
- Конечно понимаю, господин учитель.
- Так почему же ты выбираешь пять?
- Потому что, если я выберу десять, они перестанут давать мне деньги!
**********
Как разные народы переносят низкие температуры:
+10 C: Американцев трясет. Русские сажают огурцы в огородах.
+1.6 C: У итальянцев не заводятся машины. Русские ездят с опущенными стеклами.
0 C: В Америке замерзает вода. В России вода загустевает.
- 17.9 C: В Нью-Йорке домовладельцы включают отопление. Русские последний раз в сезоне выезжают на пикники.
- 42 C: В Европе не функционирует транспорт. Русские едят мороженое на улице.
- 73 C: Финский спецназ эвакуирует Санта-Клауса из Лапландии. Русские надевают ушанки.
- 114 C: Замерзает этиловый спирт. У русских плохое настроение.
- 273 C: Абсолютный ноль, остананавливается атомарное движение. Русские ругаются: "Холодно, мля!"
- 295 C: У католиков в аду замерзают черти. Российская сборная по футболу становится чемпионом мира.
**********
На дискотеке в Германии русский в майке с надписью: "У турков три проблемы".
К нему тут же подходит турок и спрашивает:
- Ты чего? Проблем ищешь? Ты наехать хочешь?
- Это ваша первая проблема. Агрессивность. Вы всегда пытаетесь создавать проблемы на пустом месте.
Когда дискотека заканчивается, то русского уже подкарауливает группа
турков.
- Сейчас ты ответишь за свои слова! - говорят они.
- Это ваша вторая проблема. Вы не можете решать свои проблемы сами и сразу собираете своих по любому поводу.
- Да как ты смеешь с нами так говорить?!! - турки повыхватывали ножи...
- Это ваша третья проблема, - продолжает русский. - Вы всегда приходите с ножами на перестрелку.
**********
ДЕЛОВОЕ ПРЕДЛОЖЕНИЕ
(пер. с англ, автор мне неизвестен)
Джонни очень хотел одну девушку в своем офисе, но она принадлежала другому... Как-то раз ему стало так невмоготу, что он подошел к ней и сказал: "Я
дам тебе 1000 долларов, если ты мне отдашься", но девушка ответила "НЕТ".
Джонни сказал: "Да я быстро - я брошу деньги на пол, ты нагнешься подобрать, а как поднимешь - я уже закончу". Девушка задумалась на секунду, и ответила, что должна проконсультироваться с бойфрендом.
Она позвонила и рассказала тому все. Бойфренд ответил: "Проси 2000, и поднимай деньги очень быстро, так чтоб он даже не успел спустить штаны".
Девушка согласилась, и дала свое согласие Джонни.
Прошло полчаса, бойфренд ждет, а девушка все не звонит... Наконец спустя 45 минут бойфренд позвонил сам и спросил, что случилось. Девушка ответила: "Этот подонок расплатился монетами."
МОРАЛЬ: Всегда рассматривайте деловое предложение досконально, до того, как вы его примете и вас поимеют!
**********
Начало учебного года в американской школе. Классная руководительница знакомит класс:
- Дети, у нас новенький Шакиро Сузуки из Японии, знакомьтесь. А сейчас начинаем урок и посмотрим, как хорошо вы знаете американскую историю.
Кто сказал "Свобода или смерть"?
В классе мертвая тишина. Сузуки вскидывает руку:
- Патрик Генри, 1775 год, Филадельфия.
- Очень хорошо. А чьи слова: "Государство это народ, и как таковое никогда не должно умереть"?
Опять рука Сузуки:
- Абрахам Линкольн, 1863 год, Вашингтон.
Учительница строго смотрит на класс:
- Стыдно, дети! Сузуки японец, а знает американскую историю лучше всех!
В этот момент тихий голос с задней парты:
- Задолбали сраные япошки!
Учительница резко оборачивается:
- Кто сказал???!!!
Сузуки вскакивает и оттарабанивает:
- Генерал МакАртур, остров Гвадалканал, 1942 год.
Возмущенный вопль:
- Сузуки дерьмо!!!
И ни секунды задержки:
- Валентино Росси на мотогонках ГранПри-Бразилия в Рио де Жанейро, 2002
год! –выпаливает японец!
Класс в истерике, училка в обмороке, распахивается дверь и появляется
разъяренный директор школы:
- Вашу мать! Что здесь за бардак???!!!
Не успевший сесть Сузуки:
- Президент Ельцин, заседание парламента России, 1993 год!
**********
Муж:
- Какого тёща приезжает?
Жена:
- Числа или хрена?
**********
Лозунг "Задушим коррупцию" был признан экстремистским как призывающий к
насильственному свержению существующего строя.
**********
Урок "Основы православной культуры". Учительница:
- И помните, дети! Те, кто будет учиться на "4" и "5", попадут в рай. А
те, кто будет учиться на "2" и "3", - в ад!
Вовочка с задней парты:
- Мариванна, а что, закончить школу живым нельзя?
**********
Штаб Ку Клукс Клана:
- Скажите, как вступить в вашу организацию?
- Это просто. Нужно замочить 6 негров и одного кота.
- А кота за что?
- Поздравляю, вы приняты
**********
Увидев на холодильнике всего два магнитика - из Магадана и Воркуты, воры покормили кота и вымыли посуду.
**********
Боевик ИГИЛ остановил автомобиль христианской пары.
Боевик ИГИЛ: «Ты мусульманин?»
Христианин: «Да, я мусульманин».
Боевик ИГИЛ: «Если ты мусульманин, перескажи суру из Корана».
Христианин рассказал стихотворение из Библии.
Боевик ИГИЛ: «Хорошо, можешь ехать».
Через несколько минут жена, едва переведя дух, говорит мужу: «Не могу поверить, как ты пошел на такой риск. Почему ты сказал, что мы мусульмане? Если бы он узнал, что ты врёшь, он убил бы нас обоих!»
«Зря волновалась. Если бы они знали Коран, они бы никогда не убивали людей!» ответил ей муж.
**********
Сборная России взяла 4 золота на международной олимпиаде по физике в Цюрихе. Деньги и белые BMW никто не предложил. Даже не заметили.
**********
Когда в стране коррупции нет — микролитражки мчат по хайвеям.
Когда коррупция — Бентли тащатся по бездорожью.
Всё просто, брат.
**********
Интересно, а если провести обыск у всего руководства ФСБ - можно будет обратно понизить пенсионный возраст?
**********
Центр организации дорожного движения Москвы пришел к выводу, что личный автомобиль гражданину не нужен. 93% времени он стоит на приколе, а лишь 7% используется, заявил руководитель Департамента транспорта Максим Ликсутов.
Остроумные люди посоветовали Максиму Ликсутову отрезать пенис, которым он пользуется меньше 10 минут в день.
**********
По поводу слов Кадырова, что его достало, что во всем обвиняют кавказцев... Знакомый татарин сказал: До тех пор пока ты ЧЕЛОВЕК, никого в России твоя национальность особо не интересует. Как только стал СКОТИНОЙ всем сразу интересно, чья это скотина гадит?
**********
Зачем пересаживать чиновников на отечественные автомобили, если они имеют право на бесплатный проезд в общественном транспорте?
**********
Почему те, кто хочет носить хиджаб не живут там, где его ношение приветствуется?
**********
В кафе заходит человек с собакой и заключает с посетителями пари,что его пес сейчас будет разговаривать. Но собака молчит. Человек оплачивает пари и уходит под общий хохот.
- Из-за тебя я проиграл уйму денег! - говорит хозяин собаке. - Почему ты не заговорил?
- Чудак! - отвечает пес. - Ты только представь, сколько денег мы загребем завтра.
**********
Выходит утром гаишник на дорогу, голова после вчерашнего раскалывается.
Смотрит - джип несется. Ну он остановил его с целью сбора средств на опохмел. Смотрит, а там бомж сидит. Документы проверил - правда, бомжа машина. Ну мент его спрашивает:
- Ты же бомж. Ты где такую крутую тачку взял?
- А мне пьяные новые русские предложили, если я их рассмешу - джип мой. Ну я их и рассмешил.
- А как?
- Да я одному лысому на голову нагадил, у него сразу волосы выросли, вот умора была.
Мент шапку снимает, там лысина. Он и говорит:
- А ты мне так можешь?
- Могу.
Бомж гадит менту на лысину, a из кустов раздается хохот и крик:
- Не, ну ваще, да я ему еще и хату подарю.
**********
Вовочка приходит в аптеку:
- Дайте мне упаковку презервативов!
- Во-первых, это не для детей, - отвечает аптекарь, - а во-вторых, пусть придет папа и возьмет нужный размер.
- Во-первых, это не для детей, а от детей, а во-вторых, это не для папы, а мама едет на курорт, и какие там размеры будут, она еще не знает...
Кенийский бегун Абель Мутай был всего в нескольких футах от финиша, но перепутал с вывесками и остановился, думая, что завершил гонку. Испанский бегун, Иван Фернандес, стоял за ним и, понимая, что происходит, начал кричать на кенийца, чтобы он продолжил бег. Мутай не знал испанского и не понял. Понимая, что происходит, Фернандес толкнул Мутая к победе. Журналист спросил Ивана: «Зачем ты это сделал?» Иван ответил: «Моя мечта заключается в том, чтобы когда-нибудь у нас была такая общественная жизнь, где мы толкаемся и помогаем друг другу побеждать». Журналистка настаивала: «Но почему ты дал победить Кении?» Иван ответил:«Я не дал ему победить, он собирался победить. Гонка была его». Журналист настаивал, и снова спросил: «А ведь можно было победить!» Иван посмотрел на него и ответил: «А в чем заслуга моей победы? Какая честь будет в этой медали? Что бы моя мама об этом подумала? Ценности передаются из поколения в поколение. Каким ценностям мы учим наших детей?»
**********
Попали в Ад американец, индус и русский. Встретил их Черт и говорит:
- Всем, кто сюда попадает, даю шанс перейти в Рай.
И достает здоровенный кнут (побольше, чем у Харрисона Форда в "Последнем
крестовом походе"):
- Кто выдержит три удара не закричав - отпускаю! Можете защищаться, чем хотите.
Первым вышел американец.
- Чем хочешь защищаться?
Американец взял здоровый гранитный камень:
- Я готов!
Черт размахнулся в первый раз и... камень вдребезги. Второй раз - и американец заорал как бешенный...
- Следующий, - говорит Черт.
Выходит индус.
- Чем будешь защищаться?
- Ничем! - отвечает индус, - Я 80 лет занимался йогой, и в медитации тело не чувствует боли!
- Ладно.
Первый удар. Индус: - Ошшш...
Второй удар. Индус: - Ошшш...
Третий удар. Индус: - Ошшш...
- Ух е# твою... Еще никто не выдерживал трех ударов. - говорит Черт. - Ну
что ж, ты свободен, можешь спокойно идти в Рай.
- Нет, - говорит индус, - хочу остаться и посмотреть. Во всех анекдотах русские выигрывают. Хочу увидеть, как у него на этот раз получится.
- Ладно, останься. Ну, чем думаешь защищаться? - обращается Черт к русскому.
- Чем защищаться - индусом, конечно...
**********
Отвечать надо быстро, не раздумывая и не тратя понапрасну время.
А главное - не мошенничать!
1. Вы участвуете в соревнованиях и обогнали бегуна, занимающего вторую
позицию. Какую позицию вы теперь занимаете?
Ответ: Если вы ответили, что вы теперь первый - то вы абсолютно не
правы.
Вы обогнали второго бегуна и заняли его место, так что вы теперь на
второй позиции.
Попробуйте не ошибиться во втором вопросе.
2. Вы обогнали последнего бегуна, на какой позиции вы теперь находитесь?
Ответ: Если вы ответили на предпоследнем - вы опять абсолютно не правы.
Подумайте. Как можно обогнать бегуна, идущего последним? Если вы бежите
за ним, значит он не последний. Ответ - это невозможно. Получается, что
использование мозга ваша не самая сильная сторона.
Как бы то ни было - вот еще один вопрос. Ничего не пишите и не
используйте калькулятор, и помните - вы должны отвечать быстро.
Возьмите 1000. Прибавьте 40. Прибавьте еще тысячу. Прибавьте 30.
Еще 1000.
Плюс 20. Плюс 1000. И плюс 10. Что получилось?
Ответ 5000? Опять неверно. Правильный ответ 4100. Попробуйте пересчитать
на калькуляторе.
Сегодня точно не ваш день. Но, может быть, получится с последним вопросом.
У отца Мэри есть пять дочерей: 1. Чача 2. Чече 3. Чичи 4 Чочо.
Вопрос: Как зовут пятую дочь? Думайте быстро. Ответ чуть ниже.
Ответ: Чучу? НЕТ! Конечно, ее зовут Мэри. Прочтите еще раз вопрос.
ВЫВОД: Вы самое слабое звено - прощайте.
**********
Мужик просыпается с утра с жуткого бодуна, открывает глаза, голова болит, оглядывается по сторонам: фуууу, дома... встает с кровати, ощупывает себя - е-мое, в пижаме... в жизни пижаму не одевал. Смотрит - на туалетном столике стакан воды, таблетка аспирина и записка от жены:
"Милый, завтрак на столе, все прибрала, твоя навеки - жена". Мужик в совершенном непонимании, выпивает таблетку и идет в ванную... по пути обнаруживает, что квартира не то что чистая, просто вылизана до блеска, сын сидит у себя в комнате, делает уроки...
- Сынок, а что вчера было?
- Ты пришел пьяный, как обычно под утро. Облевал всю прихожую, нагадил мимо унитаза, побил в кухне всю посуду, поставил матери фингал под глазом.
- Ну и, что случилось с мамой, с квартирой???
- Ааа, ты про это, просто когда тебя мама стала укладывать спать и начала стягивать с тебя штаны, ты заорал "уйди, сука - Я ЖЕНАТЫЙ!!!"
**********
Штатский Джонс был назначен в армейский учебный центр, где он должен был просвещать рекрутов по поводу различных правительственных обязательств перед ними, особенно о Страховании Жизни Военослужащих (СЖВ). Вскоре после этого лейтенант центра заметил, что Джонс имеет почти 100%-ю продажу страховок СЖВ, чего раньше никогда не бывало. Лейтенант сел в конце заполненной рекрутами комнаты и стал слушать торговую подачу Джонса. Джонс объяснил новым рекрутам основы СЖВ, а затем сказал:
"Если у вас есть СЖВ и вы пошли в бой и погибли, - правительство обязано выплатить вашим наследникам 200 000$. Если у вас нет СЖВ и вы пошли в бой и погибли, - правительство обязано выплатить вашим наследникам максимум всего лишь 6000$". "А теперь", сказал он в заключение, "как вы думаете, кого они пошлют в бой первыми?"
**********
- Ватсон, а что это вы курите? Дайте угадаю - табак "Королева Вирджиния"
с листочками вишни, из юбилейного выпуска в бархатной упаковке?
- Поразительно, Холмс! Как это вы угадали?
- Ей-богу, Ватсон! Ну не миссис Хадсон же свистнула из моей комнаты
последнюю пачку!
**********
У последней остановки метро ждет автобуса инженер, который допоздна
делал халтуру на работе. Полдвенадцатого ночи. Автобуса нет. Он весь
задубел... И тут возле него останавливается шикарный Лексус, опускается
окно и девушка типа “порномодель” говорит: "Садитесь, я вас подвезу". Он
отнекивается, мол денег нету...Она: "Да какие деньги! Вы ж на бирюлевский
автобус тут стоите... А как они ходят?! Садитесь, я так вас подвезу, а то
замерзнете..."
Он сел назад. Поехали. Тепло. Класс. И тут она спрашивает:
- Ничего, если мы за подружкой моей заедем? Я с ней раньше
договаривалась. Но это по пути... Пара минут...
Он говорит:
- Конечно... Хозяин-барин. Какие вопросы...
Заехали. Выходит девушка такого же калибра, как и первая. Плюхается на
сиденье и говорит:
- Мань, я похавать не успела. Давай причалим к магазинчику хавки купим...
Причалили... Та зашла... Выходит. У нее 2 бутылки французского шампанского
по штуке баксов, сувенирное (на полкило) ведерочко черной икры,
французские батоны, еще что-то в фирменных коробочках...
Едут... Высаживают мужика... И тут та, что со жратвой говорит:
- Мань, а что мы тут в машине крошить будем?
А та, что за рулем - мужику:
- Вы не против, если мы на пять минут к вам зайдем, перекусим и дальше
поедем?
Он извиняется, что мол, холостяцкий беспорядок, они: “Ничего... Мы
ненадолго...”
Поднялись к нему. Выпили эти две бутылки. Закусили... И /делали взрослые дела/ втроем до утра.
А через какое-то время эти телки прохаживаются в Доме кино по какой-то
тусовке. И одна говорит:
- Как все это меня достало! Эти престарелые плейбои, этот Михалков со
своими проститутками, этот Гусман старый дедун, эти все заслуженные
П***** России... Блин, смотреть уже на них не могу.
А вторая:
- Слушай, давай плюнем на это все и поедем к Коле в Бирюлево!
Первая:
- Да-а... К Коле в Бирюлево... Думаешь, он нас вспомнит?
ПАМЯТНИК ЛАБРАДОРУ МОНТИ *** В городе Квиснсленде (Австралия) жил лабрадор по кличке Монти. Хозяин был глубоко пожилым человеком. Он всегда брал Монти в походы по местным магазинам и обучил его носить в зубах свою корзину с продуктами. Однажды владелец Монти был не в силах пойти по магазинам. Он послал Монти со списком покупок и деньгами в корзине. Монти обошел все магазины, в которые он заходил вместе с хозяином. Продавцы читали записку и клали в корзинку необходимое. С тех пор Монти каждый день бегал по магазинам с корзинкой в зубах. Монти получил такую известность, что когда случилось неизбежное и он умер, местная община решила возвести ему памятник в виде бронзовой статуи с корзинкой, полной продуктов. Теперь, бронзовый Монти в натуральную величину сидит при входе в торговый центр, куда он бегал за продуктами для своего хозяина. Памятник установили 15 июня 1996 года.
**********
Как попасть в рай (притча)
По длинной, дикой, утомительной дороге шел человек с собакой.
Шел он себе шел, устал, собака тоже устала. Вдруг перед ним - оазис!
Прекрасные ворота, за оградой - музыка, цветы, журчание ручья,
словом, отдых.
- Что это такое? - спросил путешественник у привратника.
- Это рай, ты уже умер, и теперь можешь войти и отдохнуть
по-настоящему.
- А есть там вода?
- Сколько угодно: чистые фонтаны, прохладные бассейны...
- А поесть дадут?
- Все, что захочешь.
- Но со мной собака.
- Сожалею, сэр, с собаками нельзя. Ее придется оставить здесь.
И путешественник пошел мимо.. Через некоторое время дорога привела его
на ферму. У ворот тоже сидел привратник.
- Я хочу пить, - попросил путешественник.
- Заходи, во дворе есть колодец.
- А моя собака?
- Возле колодца увидишь поилку.
- А поесть?
- Могу угостить тебя ужином.
- А собаке?
- Найдется косточка.
- А что это за место?
- Это рай.
- Как так? Привратник у дворца неподалеку сказал мне, что рай - там.
- Врет он все. Там ад.
- Как же вы, в раю, это терпите?
- Это нам очень полезно. До рая доходят только те, кто не бросает
своих друзей.
**********
Мужик едет на встречу, опаздывает, нервничает, не может найти место
припарковаться. Поднимает лицо к небу и говорит:
— Господи, помоги мне найти место для парковки. Я тогда брошу пить и
буду каждое воскресенье ходить в церковь!
Вдруг чудесным образом появляется свободное местечко. Мужик снова
обращается к небу:
А, всё, не надо. Нашёл!
**********
Журналисты спрашивают у фермера:
- Скажите, как у вас прошел год.
- Не поверите, замечательно. Урожай зерна хороший - без хлеба не
останусь, картошка удалась - опять таки буду не голодный, а еще свинья
опоросилась...
- Вы не хотели бы поблагодарить за это президента?
- Да с чего ж? Пахал сам, сеял сам, растил и собирал опять таки сам - в
чем тут его заслуга.
- Как так? (жестко) А вы подумайте!
- А, ну ежли подумать, то насчет свиньи не отрицаю, тут всяко могло
быть...
**********
Сомалийский иммигрант прибыл в Берлин. Он останавливает первого человека, которого он видит и говорит: "Благодарю вас, господин. Германия позволила мне жить в этой стране, дала мне жилье, денег на еду, бесплатное медицинское обслуживание, бесплатное образование и никаких налогов!" Прохожий отвечает: "Вы ошибаетесь, я афганец." Человек идет дальше и встречает другого прохожего: "Спасибо за то, что такая красивая страна Германия! и т.д.". Человек говорит: "Я не немец, я иракец!" Вновь прибывший идет дальше, к следующему человеку, пожимает ему руку и говорит: "Спасибо за прекрасную Германию!" "Этот человек поднимает руку и говорит: "Я из Пакистана, я не из Германии!" Он, наконец, видит - идет милая дама. Спрашивает: "Вы немка?" Она говорит: "Нет, я из Индии!" Озадаченный, он спрашивает ее: "А где же немцы?" Индуска проверяет часы и отвечает: "Так они сейчас работают!"
**********
Cидит Мухаммед на корточках в Берлине и плюет на землю через дырку в зубах. Вдруг появляется фея и говорит:
— Я социалистическая социальная либеральная фея! Я прилетела, чтобы исполнить три желания!
— Посмотри, какая у меня дырка во рту! Я хочу, чтобы мне вылечили и вставили все зубы!
Не успел Мухаммед произнести эти слова, как тотчас вышел закон о бесплатном лечении и протезировании зубов для социальных иностранцев, и его рот засиял белоснежной голливудской улыбкой.
— Я очень скучаю по своим четырем женам и пятнадцати детишкам, а также по родителям, братьям и сестрам, родителям-братьям-сестрам моих жен! Я хочу, чтобы мы все жили на роскошной вилле, и чтобы денег всегда много было!
Не успел Мухаммед договорить, как оказался в прекрасной вилле! На столе — текст закона о воссоединении семей для социальных иностранцев, а также банковские распечатки со сведениями о поступивших пособиях. Дом полностью меблирован и оснащен электроприборами в соответствии с законом о помощи в приобретении мебели и бытовой техники для социальных иностранцев.
Счастливый Мухаммед просто не знает, чего бы ему еще попросить, ведь одно желание еще осталось. И он попросил:
— Хочу стать настоящим немцем. Не только по гражданству. Хочу быть голубоглазым блондином, и чтоб меня звали Фриц Шульц!
Не успел он закончить фразу, как все исчезло, и он обнаружил себя вновь сидящим на корточках и плюющим на землю сквозь дырку в зубах.
— Что случилось? — спросил он у феи.
— Как не стыдно, господин Шульц, клянчить у государства! Вы должны заботиться о себе сами! Идите и ищите работу!
**********
А давайте больным детям на лечение брать из бюджета, а депутатам зарплату собирать на первом канале!
**********
Идёт Будда с учениками по дороге. Видит: яма, в ней вол, крестьянин пытается его вытянуть, но сил не хватает. Будда кивнул ученикам, они быстро помогли вытянуть животное. Идут дальше, снова яма, в ней вол, на краю сидит крестьянин и горько плачет. Будда прошёл мимо и как бы не заметил. Ученики его спрашивают:
- Учитель, почему ты не захотел помочь этому крестьянину?
- Помочь плакать?
**********
Воздушный шар сбился с курса, и воздухоплаватель срочно опустился с ним вниз. Увидев внизу человека, он спросил:
- Извините, где я нахожусь?
- Вы находитесь на воздушном шаре, в 15м над землей. Ваши координаты - 5°28'17" N и 100°40'19" E.
- Похоже, вы математик, - вздохнул воздухоплаватель.
- Да, я математик, - согласился прохожий. - Как вы догадались?
- Ваш ответ, по-видимому, точный и полный, но для меня совершенно бесполезный. Я по-прежнему не знаю, где я нахожусь, и что мне делать. Вы мне нисколько не помогли, только напрасно отняли время.
- А вы, похоже, из управленцев, - заметил математик.
- Я действительно топ-менеджер серьезной компании, - воспрял воздухоплаватель. - Но как вы догадались? Вы видели меня по телевизору?
- Зачем? - удивился математик. - Судите сами: вы не понимаете ни где вы находитесь, ни что вам следует делать, в этом вы полагаетесь на нижестоящих. Спрашивая совета у эксперта, вы ни на секунду не задумываетесь, способны ли вы понять его ответ, и когда оказывается, что это - не так, вы возмущаетесь вместо того, чтобы переспросить. Вы находитесь ровно в том же положении, что и до моего ответа, но теперь почему-то обвиняете в этом меня. Наконец, вы находитесь выше других только благодаря дутому пузырю, и если с ним что-то случится - падение станет для вас фатальным.
**********
Забавно, что когда Сбербанк празднует свой юбилей, то он считает свою историю с 1841 года, а когда ему задают вопросы про вклады 1991 года, то оказывается, что это совершенно другой банк.
**********
А давайте что-нибудь споем в поддержку артистов, попавших в сложную финансовую ситуацию?
**********
Горит здание Сбербанка.
Звонок в пожарную охрану:
- Срочно приезжайте!!! Пожар в здании Сбербанка!!!
- Одну минуту, я переключу вас на специалиста
играет бодрая музыка, затем слышатся радостные фразы: "Если вы хотите узнать о наших новых услугах - нажмите "1". Если хотите заключить договор на монтаж противопожарного оборудования - нажмите "2". Внимание! Пожарная охрана представляет вам совершенно новый способ тушения пожаров! Хотите узнать больше? Нажмите "3". Не услышали свой вариант? Оставайтесь на линии. Приготовьте кадастровый номер вашего объекта, а также паспортные данные его владельца. Ваш звонок очень важен для нас - оставайтесь на линии.
Хотите попробовать потушить пожар самостоятельно? Воспользуйтесь услугой "Продвинутый пожарный"! Чтобы узнать, как подключить - нажмите "5""
........
- Оператор пожарной охраны Сергей, чем я могу вам помочь?
- У нас пожар! Горит три помещения!
- Скажите, как я могу к вам обращаться?
- Вы идиот? У нас здание горит! Не надо ко мне обращаться, срочно выезжайте тушить!
- Подскажите кадастровый номер здания и ФИО владельца
- Да не знаю я никакого кадастрового номера, я назвал вам адрес! Этого недостаточно, чтобы выехать на тушение пожара?!
- Оставайтесь на линии, я переведу вас на специалиста по поддержке
Играет бодрая музыка,"Если вы хотите заказать монтаж противопожарной сигнализации - произнесите: "Монтаж сигнализации", Если вы хотите подключиться к услуге "Круглосуточный пожарный расчет" - произнесите: "Подключиться". Вы не выбрали подходящий вариант. Ваш звонок будет переведен на оператора".
- Здравствуйте, меня зовут Александр, чем могу помочь?!
- У нас здание горит! Сделайте что-нибудь!
- Подскажите, как я могу к вам обращаться?
- Б...!!! С....! .... ....! Пожар!!!
- Наши специалисты рассмотрят вашу проблему. Скажите, по какому номеру мы можем с вами связаться?
**********
А разве первыми в военкомат вызывают не тех, у кого на машине наклеено: "Можем повторить!"?
**********
Не спрашивай у мужчины про его доходы, у женщины про возраст, у патриота, откуда у него американский паспорт.
**********

642
back/internal/knocker.go Normal file
View 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()
}

568
back/jokes.md Normal file
View File

@@ -0,0 +1,568 @@
**********
На собеседовании в школе для особо одарённых детей шестилетнего Вовочку попросили рассказать, чем автобус отличается от троллейбуса.
Вовочка ничего скрывать от тёти не стал и честно ей сообщил, что автобус работает на двигателе внутреннего сгорания, а троллейбус - на электродвигателе переменного тока.
Оказалось - ничего подобного!
Просто троллейбус с рогами, а автобус - без.
И нечего тут морочить тёте голову!
**********
Гламурная москвичка приезжает погостить в деревню к бабушке.
- Бабуль, куда у вас тут ночью сходить можно?
- В ведро.
**********
Только перечитывая в 40 лет книгу "Д'Артаньян и три мушкетёра" ты наконец-то начинаешь понимать, что единственный положительный герой в этой книге - кардинал Ришелье...
**********
Вместо того, чтобы у себя в Алжире, Марокко и Тунисе строить жизнь, как во Франции, люди приезжают во Францию, чтобы там создать себе такую жизнь, как в Алжире, Марокко и Тунисе. Это странно. Ещё более странно заставлять при этом и французов жить, как в Алжире, Марокко и Тунисе.
**********
Твоя религия ничего МНЕ не запрещает. Она запрещает ТЕБЕ. Уясни это.
**********
Антон Силуанов предложил россиянам не думать о ключевой ставке ЦБ.
Также россиянам не стоит думать о:
- процентах по кредитам;
- курсе рубля;
- тарифах ЖКХ;
- ценах в магазинах.
В то же время россиянам стоит думать о:
- госдолге США;
- крахе доллара;
- величии державы;
- необходимости потерпеть.
**********
Василий Иванович с Петькой сидят, скучают. Василий Иванович:
- Петька, сгоняй на хутор к старику-самогонщику, сообрази чего-нибудь!
- Это можно.
Через час Петька возвращается. Василий Иванович:
- Ну как?
- Да нет у него ни фига.
- Эх, молодежь, всему вас учить надо. Пойдем вместе.
Приходят к деду. Василий Иванович:
- Здорово, отец!
- Здорово, сынки!
- Мы вот к тебе от имени Советской власти. Вот я гляжу, хата у тебя старая.
- Ох, какая старая!
- Петька, запиши: новую хату ему от Советской власти! И жена у тебя вроде старая...
- Ой, какая старая!!
- Петька, запиши: новую жену ему от Советской власти!
- Сынки! Родненькие! Да сколько ж я вас ждал!
Ну, сели за стол, выпили, с собой взяли. Уже в дверях Василий Иванович как бы невзначай:
- Слышь, дед! А может у тебя и /хрен/ старый?
- Уж каккой старый!!!
- Петька, запиши: /хрен/ ему от Советской власти!
**********
Таможенник поднимается на корабль для досмотра.
- Наркотики есть?
Хозяин корабля отвечает:
- Есть. Вот, пожалуйста (достает чемодан). Вот героин, вот кокаин. Все аккуратно упаковано,вот шприц готовый.
Таможенник, вытаращив глаза:
- А может, и оружие есть?
Хозяин (достает другой чемодан):
- Вот Макаров, вот Калашник ,вот патроны к ним. Все как надо.
Таможенник с усмешкой:
- Наверное, и валюта есть?
Хозяин достает третий чемодан:
- Вот миллион долларов, пожалуйста.
Таможенник, ничего не понимая:
- И это все ваше?
Хозяин:
- Нет, это ваше. Мое в трюме.
**********
Летит самолет. Пилот по громкой связи:
- Уважаемые дамы и господа, вас приветствует командир корабля. Прослушайте информацию о нашем полете. Наш полет проходит на высоте 10 тысяч метров со скоростью 900 километров в час, температура за бортом...БЛИН...А-А-А-А!.. ЭТО ЧТО ТАКОЕ?.. НУ, КРЫНДЕЦ...
В салоне гробовая тишина.
Через минуту опять по радио:
- Прошу извинения у уважаемых пассажиров. Просто это наша стюардесса опрокинула на меня горячий кофе. Видели ли бы вы теперь мои белые брюки спереди...
Мужик в первом ряду:
- Твои брюки, это все херня! Видел бы ты мои брюки сзади...
**********
Новый учитель, придя в класс, обнаружил, что одного мальчика дразнят Мойше-дурачок. На перемене он спросил ребят, почему они его так обзывают.
- Да он и вправду дурачок, господин учитель. Если дать ему большую монету в пять шекелей и маленькую в десять, он выберет пять, потому что думает, что она больше. Вот, смотрите...
Парень достает две монеты и предлагает Мойше выбрать. Тот, как всегда,
выбирает пять. Учитель с удивлением спращивает:
- Почему же ты выбрал монету в пять шекелей, а не в десять?
- Посмотрите, она же больше, господин учитель!
После уроков учитель подошел к Мойше.
- Неужели ты не понимаешь, что пять шекелей больше только по размерам,
но на десять шекелей можно купить больше?
- Конечно понимаю, господин учитель.
- Так почему же ты выбираешь пять?
- Потому что, если я выберу десять, они перестанут давать мне деньги!
Четвёртая десятка
**********
Как разные народы переносят низкие температуры:
+10 C: Американцев трясет. Русские сажают огурцы в огородах.
+1.6 C: У итальянцев не заводятся машины. Русские ездят с опущенными стеклами.
0 C: В Америке замерзает вода. В России вода загустевает.
- 17.9 C: В Нью-Йорке домовладельцы включают отопление. Русские последний раз в сезоне выезжают на пикники.
- 42 C: В Европе не функционирует транспорт. Русские едят мороженое на улице.
- 73 C: Финский спецназ эвакуирует Санта-Клауса из Лапландии. Русские надевают ушанки.
- 114 C: Замерзает этиловый спирт. У русских плохое настроение.
- 273 C: Абсолютный ноль, остананавливается атомарное движение. Русские ругаются: "Холодно, мля!"
- 295 C: У католиков в аду замерзают черти. Российская сборная по футболу становится чемпионом мира.
**********
На дискотеке в Германии русский в майке с надписью: "У турков три проблемы".
К нему тут же подходит турок и спрашивает:
- Ты чего? Проблем ищешь? Ты наехать хочешь?
- Это ваша первая проблема. Агрессивность. Вы всегда пытаетесь создавать проблемы на пустом месте.
Когда дискотека заканчивается, то русского уже подкарауливает группа
турков.
- Сейчас ты ответишь за свои слова! - говорят они.
- Это ваша вторая проблема. Вы не можете решать свои проблемы сами и сразу собираете своих по любому поводу.
- Да как ты смеешь с нами так говорить?!! - турки повыхватывали ножи...
- Это ваша третья проблема, - продолжает русский. - Вы всегда приходите с ножами на перестрелку.
**********
ДЕЛОВОЕ ПРЕДЛОЖЕНИЕ
(пер. с англ, автор мне неизвестен)
Джонни очень хотел одну девушку в своем офисе, но она принадлежала другому... Как-то раз ему стало так невмоготу, что он подошел к ней и сказал: "Я
дам тебе 1000 долларов, если ты мне отдашься", но девушка ответила "НЕТ".
Джонни сказал: "Да я быстро - я брошу деньги на пол, ты нагнешься подобрать, а как поднимешь - я уже закончу". Девушка задумалась на секунду, и ответила, что должна проконсультироваться с бойфрендом.
Она позвонила и рассказала тому все. Бойфренд ответил: "Проси 2000, и поднимай деньги очень быстро, так чтоб он даже не успел спустить штаны".
Девушка согласилась, и дала свое согласие Джонни.
Прошло полчаса, бойфренд ждет, а девушка все не звонит... Наконец спустя 45 минут бойфренд позвонил сам и спросил, что случилось. Девушка ответила: "Этот подонок расплатился монетами."
МОРАЛЬ: Всегда рассматривайте деловое предложение досконально, до того, как вы его примете и вас поимеют!
**********
Начало учебного года в американской школе. Классная руководительница знакомит класс:
- Дети, у нас новенький Шакиро Сузуки из Японии, знакомьтесь. А сейчас начинаем урок и посмотрим, как хорошо вы знаете американскую историю.
Кто сказал "Свобода или смерть"?
В классе мертвая тишина. Сузуки вскидывает руку:
- Патрик Генри, 1775 год, Филадельфия.
- Очень хорошо. А чьи слова: "Государство это народ, и как таковое никогда не должно умереть"?
Опять рука Сузуки:
- Абрахам Линкольн, 1863 год, Вашингтон.
Учительница строго смотрит на класс:
- Стыдно, дети! Сузуки японец, а знает американскую историю лучше всех!
В этот момент тихий голос с задней парты:
- Задолбали сраные япошки!
Учительница резко оборачивается:
- Кто сказал???!!!
Сузуки вскакивает и оттарабанивает:
- Генерал МакАртур, остров Гвадалканал, 1942 год.
Возмущенный вопль:
- Сузуки дерьмо!!!
И ни секунды задержки:
- Валентино Росси на мотогонках ГранПри-Бразилия в Рио де Жанейро, 2002
год! –выпаливает японец!
Класс в истерике, училка в обмороке, распахивается дверь и появляется
разъяренный директор школы:
- Вашу мать! Что здесь за бардак???!!!
Не успевший сесть Сузуки:
- Президент Ельцин, заседание парламента России, 1993 год!
**********
Муж:
- Какого тёща приезжает?
Жена:
- Числа или хрена?
**********
Лозунг "Задушим коррупцию" был признан экстремистским как призывающий к
насильственному свержению существующего строя.
**********
Урок "Основы православной культуры". Учительница:
- И помните, дети! Те, кто будет учиться на "4" и "5", попадут в рай. А
те, кто будет учиться на "2" и "3", - в ад!
Вовочка с задней парты:
- Мариванна, а что, закончить школу живым нельзя?
**********
Штаб Ку Клукс Клана:
- Скажите, как вступить в вашу организацию?
- Это просто. Нужно замочить 6 негров и одного кота.
- А кота за что?
- Поздравляю, вы приняты
**********
Увидев на холодильнике всего два магнитика - из Магадана и Воркуты, воры покормили кота и вымыли посуду.
**********
Боевик ИГИЛ остановил автомобиль христианской пары.
Боевик ИГИЛ: «Ты мусульманин?»
Христианин: «Да, я мусульманин».
Боевик ИГИЛ: «Если ты мусульманин, перескажи суру из Корана».
Христианин рассказал стихотворение из Библии.
Боевик ИГИЛ: «Хорошо, можешь ехать».
Через несколько минут жена, едва переведя дух, говорит мужу: «Не могу поверить, как ты пошел на такой риск. Почему ты сказал, что мы мусульмане? Если бы он узнал, что ты врёшь, он убил бы нас обоих!»
«Зря волновалась. Если бы они знали Коран, они бы никогда не убивали людей!» ответил ей муж.
Третья десятка
**********
Сборная России взяла 4 золота на международной олимпиаде по физике в Цюрихе. Деньги и белые BMW никто не предложил. Даже не заметили.
**********
Когда в стране коррупции нет — микролитражки мчат по хайвеям.
Когда коррупция — Бентли тащатся по бездорожью.
Всё просто, брат.
**********
Интересно, а если провести обыск у всего руководства ФСБ - можно будет обратно понизить пенсионный возраст?
**********
Центр организации дорожного движения Москвы пришел к выводу, что личный автомобиль гражданину не нужен. 93% времени он стоит на приколе, а лишь 7% используется, заявил руководитель Департамента транспорта Максим Ликсутов.
Остроумные люди посоветовали Максиму Ликсутову отрезать пенис, которым он пользуется меньше 10 минут в день.
**********
По поводу слов Кадырова, что его достало, что во всем обвиняют кавказцев... Знакомый татарин сказал: До тех пор пока ты ЧЕЛОВЕК, никого в России твоя национальность особо не интересует. Как только стал СКОТИНОЙ всем сразу интересно, чья это скотина гадит?
**********
Зачем пересаживать чиновников на отечественные автомобили, если они имеют право на бесплатный проезд в общественном транспорте?
**********
Почему те, кто хочет носить хиджаб не живут там, где его ношение приветствуется?
**********
В кафе заходит человек с собакой и заключает с посетителями пари,что его пес сейчас будет разговаривать. Но собака молчит. Человек оплачивает пари и уходит под общий хохот.
- Из-за тебя я проиграл уйму денег! - говорит хозяин собаке. - Почему ты не заговорил?
- Чудак! - отвечает пес. - Ты только представь, сколько денег мы загребем завтра.
**********
Выходит утром гаишник на дорогу, голова после вчерашнего раскалывается.
Смотрит - джип несется. Ну он остановил его с целью сбора средств на опохмел. Смотрит, а там бомж сидит. Документы проверил - правда, бомжа машина. Ну мент его спрашивает:
- Ты же бомж. Ты где такую крутую тачку взял?
- А мне пьяные новые русские предложили, если я их рассмешу - джип мой. Ну я их и рассмешил.
- А как?
- Да я одному лысому на голову нагадил, у него сразу волосы выросли, вот умора была.
Мент шапку снимает, там лысина. Он и говорит:
- А ты мне так можешь?
- Могу.
Бомж гадит менту на лысину, a из кустов раздается хохот и крик:
- Не, ну ваще, да я ему еще и хату подарю.
**********
Вовочка приходит в аптеку:
- Дайте мне упаковку презервативов!
- Во-первых, это не для детей, - отвечает аптекарь, - а во-вторых, пусть придет папа и возьмет нужный размер.
- Во-первых, это не для детей, а от детей, а во-вторых, это не для папы, а мама едет на курорт, и какие там размеры будут, она еще не знает...
Кенийский бегун Абель Мутай был всего в нескольких футах от финиша, но перепутал с вывесками и остановился, думая, что завершил гонку. Испанский бегун, Иван Фернандес, стоял за ним и, понимая, что происходит, начал кричать на кенийца, чтобы он продолжил бег. Мутай не знал испанского и не понял. Понимая, что происходит, Фернандес толкнул Мутая к победе. Журналист спросил Ивана: «Зачем ты это сделал?» Иван ответил: «Моя мечта заключается в том, чтобы когда-нибудь у нас была такая общественная жизнь, где мы толкаемся и помогаем друг другу побеждать». Журналистка настаивала: «Но почему ты дал победить Кении?» Иван ответил:«Я не дал ему победить, он собирался победить. Гонка была его». Журналист настаивал, и снова спросил: «А ведь можно было победить!» Иван посмотрел на него и ответил: «А в чем заслуга моей победы? Какая честь будет в этой медали? Что бы моя мама об этом подумала? Ценности передаются из поколения в поколение. Каким ценностям мы учим наших детей?»
Вторая десятка
**********
Попали в Ад американец, индус и русский. Встретил их Черт и говорит:
- Всем, кто сюда попадает, даю шанс перейти в Рай.
И достает здоровенный кнут (побольше, чем у Харрисона Форда в "Последнем
крестовом походе"):
- Кто выдержит три удара не закричав - отпускаю! Можете защищаться, чем хотите.
Первым вышел американец.
- Чем хочешь защищаться?
Американец взял здоровый гранитный камень:
- Я готов!
Черт размахнулся в первый раз и... камень вдребезги. Второй раз - и американец заорал как бешенный...
- Следующий, - говорит Черт.
Выходит индус.
- Чем будешь защищаться?
- Ничем! - отвечает индус, - Я 80 лет занимался йогой, и в медитации тело не чувствует боли!
- Ладно.
Первый удар. Индус: - Ошшш...
Второй удар. Индус: - Ошшш...
Третий удар. Индус: - Ошшш...
- Ух е# твою... Еще никто не выдерживал трех ударов. - говорит Черт. - Ну
что ж, ты свободен, можешь спокойно идти в Рай.
- Нет, - говорит индус, - хочу остаться и посмотреть. Во всех анекдотах русские выигрывают. Хочу увидеть, как у него на этот раз получится.
- Ладно, останься. Ну, чем думаешь защищаться? - обращается Черт к русскому.
- Чем защищаться - индусом, конечно...
**********
Отвечать надо быстро, не раздумывая и не тратя понапрасну время.
А главное - не мошенничать!
1. Вы участвуете в соревнованиях и обогнали бегуна, занимающего вторую
позицию. Какую позицию вы теперь занимаете?
Ответ: Если вы ответили, что вы теперь первый - то вы абсолютно не
правы.
Вы обогнали второго бегуна и заняли его место, так что вы теперь на
второй позиции.
Попробуйте не ошибиться во втором вопросе.
2. Вы обогнали последнего бегуна, на какой позиции вы теперь находитесь?
Ответ: Если вы ответили на предпоследнем - вы опять абсолютно не правы.
Подумайте. Как можно обогнать бегуна, идущего последним? Если вы бежите
за ним, значит он не последний. Ответ - это невозможно. Получается, что
использование мозга ваша не самая сильная сторона.
Как бы то ни было - вот еще один вопрос. Ничего не пишите и не
используйте калькулятор, и помните - вы должны отвечать быстро.
Возьмите 1000. Прибавьте 40. Прибавьте еще тысячу. Прибавьте 30.
Еще 1000.
Плюс 20. Плюс 1000. И плюс 10. Что получилось?
Ответ 5000? Опять неверно. Правильный ответ 4100. Попробуйте пересчитать
на калькуляторе.
Сегодня точно не ваш день. Но, может быть, получится с последним вопросом.
У отца Мэри есть пять дочерей: 1. Чача 2. Чече 3. Чичи 4 Чочо.
Вопрос: Как зовут пятую дочь? Думайте быстро. Ответ чуть ниже.
Ответ: Чучу? НЕТ! Конечно, ее зовут Мэри. Прочтите еще раз вопрос.
ВЫВОД: Вы самое слабое звено - прощайте.
**********
Мужик просыпается с утра с жуткого бодуна, открывает глаза, голова болит, оглядывается по сторонам: фуууу, дома... встает с кровати, ощупывает себя - е-мое, в пижаме... в жизни пижаму не одевал. Смотрит - на туалетном столике стакан воды, таблетка аспирина и записка от жены:
"Милый, завтрак на столе, все прибрала, твоя навеки - жена". Мужик в совершенном непонимании, выпивает таблетку и идет в ванную... по пути обнаруживает, что квартира не то что чистая, просто вылизана до блеска, сын сидит у себя в комнате, делает уроки...
- Сынок, а что вчера было?
- Ты пришел пьяный, как обычно под утро. Облевал всю прихожую, нагадил мимо унитаза, побил в кухне всю посуду, поставил матери фингал под глазом.
- Ну и, что случилось с мамой, с квартирой???
- Ааа, ты про это, просто когда тебя мама стала укладывать спать и начала стягивать с тебя штаны, ты заорал "уйди, сука - Я ЖЕНАТЫЙ!!!"
**********
Штатский Джонс был назначен в армейский учебный центр, где он должен был просвещать рекрутов по поводу различных правительственных обязательств перед ними, особенно о Страховании Жизни Военослужащих (СЖВ). Вскоре после этого лейтенант центра заметил, что Джонс имеет почти 100%-ю продажу страховок СЖВ, чего раньше никогда не бывало. Лейтенант сел в конце заполненной рекрутами комнаты и стал слушать торговую подачу Джонса. Джонс объяснил новым рекрутам основы СЖВ, а затем сказал:
"Если у вас есть СЖВ и вы пошли в бой и погибли, - правительство обязано выплатить вашим наследникам 200 000$. Если у вас нет СЖВ и вы пошли в бой и погибли, - правительство обязано выплатить вашим наследникам максимум всего лишь 6000$". "А теперь", сказал он в заключение, "как вы думаете, кого они пошлют в бой первыми?"
**********
- Ватсон, а что это вы курите? Дайте угадаю - табак "Королева Вирджиния"
с листочками вишни, из юбилейного выпуска в бархатной упаковке?
- Поразительно, Холмс! Как это вы угадали?
- Ей-богу, Ватсон! Ну не миссис Хадсон же свистнула из моей комнаты
последнюю пачку!
**********
У последней остановки метро ждет автобуса инженер, который допоздна
делал халтуру на работе. Полдвенадцатого ночи. Автобуса нет. Он весь
задубел... И тут возле него останавливается шикарный Лексус, опускается
окно и девушка типа “порномодель” говорит: "Садитесь, я вас подвезу". Он
отнекивается, мол денег нету...Она: "Да какие деньги! Вы ж на бирюлевский
автобус тут стоите... А как они ходят?! Садитесь, я так вас подвезу, а то
замерзнете..."
Он сел назад. Поехали. Тепло. Класс. И тут она спрашивает:
- Ничего, если мы за подружкой моей заедем? Я с ней раньше
договаривалась. Но это по пути... Пара минут...
Он говорит:
- Конечно... Хозяин-барин. Какие вопросы...
Заехали. Выходит девушка такого же калибра, как и первая. Плюхается на
сиденье и говорит:
- Мань, я похавать не успела. Давай причалим к магазинчику хавки купим...
Причалили... Та зашла... Выходит. У нее 2 бутылки французского шампанского
по штуке баксов, сувенирное (на полкило) ведерочко черной икры,
французские батоны, еще что-то в фирменных коробочках...
Едут... Высаживают мужика... И тут та, что со жратвой говорит:
- Мань, а что мы тут в машине крошить будем?
А та, что за рулем - мужику:
- Вы не против, если мы на пять минут к вам зайдем, перекусим и дальше
поедем?
Он извиняется, что мол, холостяцкий беспорядок, они: “Ничего... Мы
ненадолго...”
Поднялись к нему. Выпили эти две бутылки. Закусили... И /делали взрослые дела/ втроем до утра.
А через какое-то время эти телки прохаживаются в Доме кино по какой-то
тусовке. И одна говорит:
- Как все это меня достало! Эти престарелые плейбои, этот Михалков со
своими проститутками, этот Гусман старый дедун, эти все заслуженные
П***** России... Блин, смотреть уже на них не могу.
А вторая:
- Слушай, давай плюнем на это все и поедем к Коле в Бирюлево!
Первая:
- Да-а... К Коле в Бирюлево... Думаешь, он нас вспомнит?
ПАМЯТНИК ЛАБРАДОРУ МОНТИ *** В городе Квиснсленде (Австралия) жил лабрадор по кличке Монти. Хозяин был глубоко пожилым человеком. Он всегда брал Монти в походы по местным магазинам и обучил его носить в зубах свою корзину с продуктами. Однажды владелец Монти был не в силах пойти по магазинам. Он послал Монти со списком покупок и деньгами в корзине. Монти обошел все магазины, в которые он заходил вместе с хозяином. Продавцы читали записку и клали в корзинку необходимое. С тех пор Монти каждый день бегал по магазинам с корзинкой в зубах. Монти получил такую известность, что когда случилось неизбежное и он умер, местная община решила возвести ему памятник в виде бронзовой статуи с корзинкой, полной продуктов. Теперь, бронзовый Монти в натуральную величину сидит при входе в торговый центр, куда он бегал за продуктами для своего хозяина. Памятник установили 15 июня 1996 года.
**********
Как попасть в рай (притча)
По длинной, дикой, утомительной дороге шел человек с собакой.
Шел он себе шел, устал, собака тоже устала. Вдруг перед ним - оазис!
Прекрасные ворота, за оградой - музыка, цветы, журчание ручья,
словом, отдых.
- Что это такое? - спросил путешественник у привратника.
- Это рай, ты уже умер, и теперь можешь войти и отдохнуть
по-настоящему.
- А есть там вода?
- Сколько угодно: чистые фонтаны, прохладные бассейны...
- А поесть дадут?
- Все, что захочешь.
- Но со мной собака.
- Сожалею, сэр, с собаками нельзя. Ее придется оставить здесь.
И путешественник пошел мимо.. Через некоторое время дорога привела его
на ферму. У ворот тоже сидел привратник.
- Я хочу пить, - попросил путешественник.
- Заходи, во дворе есть колодец.
- А моя собака?
- Возле колодца увидишь поилку.
- А поесть?
- Могу угостить тебя ужином.
- А собаке?
- Найдется косточка.
- А что это за место?
- Это рай.
- Как так? Привратник у дворца неподалеку сказал мне, что рай - там.
- Врет он все. Там ад.
- Как же вы, в раю, это терпите?
- Это нам очень полезно. До рая доходят только те, кто не бросает
своих друзей.
**********
Мужик едет на встречу, опаздывает, нервничает, не может найти место
припарковаться. Поднимает лицо к небу и говорит:
— Господи, помоги мне найти место для парковки. Я тогда брошу пить и
буду каждое воскресенье ходить в церковь!
Вдруг чудесным образом появляется свободное местечко. Мужик снова
обращается к небу:
А, всё, не надо. Нашёл!
**********
Журналисты спрашивают у фермера:
- Скажите, как у вас прошел год.
- Не поверите, замечательно. Урожай зерна хороший - без хлеба не
останусь, картошка удалась - опять таки буду не голодный, а еще свинья
опоросилась...
- Вы не хотели бы поблагодарить за это президента?
- Да с чего ж? Пахал сам, сеял сам, растил и собирал опять таки сам - в
чем тут его заслуга.
- Как так? (жестко) А вы подумайте!
- А, ну ежли подумать, то насчет свиньи не отрицаю, тут всяко могло
быть...
**********
Сомалийский иммигрант прибыл в Берлин. Он останавливает первого человека, которого он видит и говорит: "Благодарю вас, господин. Германия позволила мне жить в этой стране, дала мне жилье, денег на еду, бесплатное медицинское обслуживание, бесплатное образование и никаких налогов!" Прохожий отвечает: "Вы ошибаетесь, я афганец." Человек идет дальше и встречает другого прохожего: "Спасибо за то, что такая красивая страна Германия! и т.д.". Человек говорит: "Я не немец, я иракец!" Вновь прибывший идет дальше, к следующему человеку, пожимает ему руку и говорит: "Спасибо за прекрасную Германию!" "Этот человек поднимает руку и говорит: "Я из Пакистана, я не из Германии!" Он, наконец, видит - идет милая дама. Спрашивает: "Вы немка?" Она говорит: "Нет, я из Индии!" Озадаченный, он спрашивает ее: "А где же немцы?" Индуска проверяет часы и отвечает: "Так они сейчас работают!"
Первая десятка
**********
Как сказал старый казак: если украинцев пошлют воевать с русскими, надо стать на границе спина к спине и стрелять в тех, кто послал.
**********
Cидит Мухаммед на корточках в Берлине и плюет на землю через дырку в зубах. Вдруг появляется фея и говорит:
— Я социалистическая социальная либеральная фея! Я прилетела, чтобы исполнить три желания!
— Посмотри, какая у меня дырка во рту! Я хочу, чтобы мне вылечили и вставили все зубы!
Не успел Мухаммед произнести эти слова, как тотчас вышел закон о бесплатном лечении и протезировании зубов для социальных иностранцев, и его рот засиял белоснежной голливудской улыбкой.
— Я очень скучаю по своим четырем женам и пятнадцати детишкам, а также по родителям, братьям и сестрам, родителям-братьям-сестрам моих жен! Я хочу, чтобы мы все жили на роскошной вилле, и чтобы денег всегда много было!
Не успел Мухаммед договорить, как оказался в прекрасной вилле! На столе — текст закона о воссоединении семей для социальных иностранцев, а также банковские распечатки со сведениями о поступивших пособиях. Дом полностью меблирован и оснащен электроприборами в соответствии с законом о помощи в приобретении мебели и бытовой техники для социальных иностранцев.
Счастливый Мухаммед просто не знает, чего бы ему еще попросить, ведь одно желание еще осталось. И он попросил:
— Хочу стать настоящим немцем. Не только по гражданству. Хочу быть голубоглазым блондином, и чтоб меня звали Фриц Шульц!
Не успел он закончить фразу, как все исчезло, и он обнаружил себя вновь сидящим на корточках и плюющим на землю сквозь дырку в зубах.
— Что случилось? — спросил он у феи.
— Как не стыдно, господин Шульц, клянчить у государства! Вы должны заботиться о себе сами! Идите и ищите работу!
**********
А давайте больным детям на лечение брать из бюджета, а депутатам зарплату собирать на первом канале!
**********
Идёт Будда с учениками по дороге. Видит: яма, в ней вол, крестьянин пытается его вытянуть, но сил не хватает. Будда кивнул ученикам, они быстро помогли вытянуть животное. Идут дальше, снова яма, в ней вол, на краю сидит крестьянин и горько плачет. Будда прошёл мимо и как бы не заметил. Ученики его спрашивают:
- Учитель, почему ты не захотел помочь этому крестьянину?
- Помочь плакать?
**********
Воздушный шар сбился с курса, и воздухоплаватель срочно опустился с ним вниз. Увидев внизу человека, он спросил:
- Извините, где я нахожусь?
- Вы находитесь на воздушном шаре, в 15м над землей. Ваши координаты - 5°28'17" N и 100°40'19" E.
- Похоже, вы математик, - вздохнул воздухоплаватель.
- Да, я математик, - согласился прохожий. - Как вы догадались?
- Ваш ответ, по-видимому, точный и полный, но для меня совершенно бесполезный. Я по-прежнему не знаю, где я нахожусь, и что мне делать. Вы мне нисколько не помогли, только напрасно отняли время.
- А вы, похоже, из управленцев, - заметил математик.
- Я действительно топ-менеджер серьезной компании, - воспрял воздухоплаватель. - Но как вы догадались? Вы видели меня по телевизору?
- Зачем? - удивился математик. - Судите сами: вы не понимаете ни где вы находитесь, ни что вам следует делать, в этом вы полагаетесь на нижестоящих. Спрашивая совета у эксперта, вы ни на секунду не задумываетесь, способны ли вы понять его ответ, и когда оказывается, что это - не так, вы возмущаетесь вместо того, чтобы переспросить. Вы находитесь ровно в том же положении, что и до моего ответа, но теперь почему-то обвиняете в этом меня. Наконец, вы находитесь выше других только благодаря дутому пузырю, и если с ним что-то случится - падение станет для вас фатальным.
**********
Забавно, что когда Сбербанк празднует свой юбилей, то он считает свою историю с 1841 года, а когда ему задают вопросы про вклады 1991 года, то оказывается, что это совершенно другой банк.
**********
А давайте что-нибудь споем в поддержку артистов, попавших в сложную финансовую ситуацию?
**********
Горит здание Сбербанка.
Звонок в пожарную охрану:
- Срочно приезжайте!!! Пожар в здании Сбербанка!!!
- Одну минуту, я переключу вас на специалиста
играет бодрая музыка, затем слышатся радостные фразы: "Если вы хотите узнать о наших новых услугах - нажмите "1". Если хотите заключить договор на монтаж противопожарного оборудования - нажмите "2". Внимание! Пожарная охрана представляет вам совершенно новый способ тушения пожаров! Хотите узнать больше? Нажмите "3". Не услышали свой вариант? Оставайтесь на линии. Приготовьте кадастровый номер вашего объекта, а также паспортные данные его владельца. Ваш звонок очень важен для нас - оставайтесь на линии.
Хотите попробовать потушить пожар самостоятельно? Воспользуйтесь услугой "Продвинутый пожарный"! Чтобы узнать, как подключить - нажмите "5""
........
- Оператор пожарной охраны Сергей, чем я могу вам помочь?
- У нас пожар! Горит три помещения!
- Скажите, как я могу к вам обращаться?
- Вы идиот? У нас здание горит! Не надо ко мне обращаться, срочно выезжайте тушить!
- Подскажите кадастровый номер здания и ФИО владельца
- Да не знаю я никакого кадастрового номера, я назвал вам адрес! Этого недостаточно, чтобы выехать на тушение пожара?!
- Оставайтесь на линии, я переведу вас на специалиста по поддержке
Играет бодрая музыка,"Если вы хотите заказать монтаж противопожарной сигнализации - произнесите: "Монтаж сигнализации", Если вы хотите подключиться к услуге "Круглосуточный пожарный расчет" - произнесите: "Подключиться". Вы не выбрали подходящий вариант. Ваш звонок будет переведен на оператора".
- Здравствуйте, меня зовут Александр, чем могу помочь?!
- У нас здание горит! Сделайте что-нибудь!
- Подскажите, как я могу к вам обращаться?
- Б...!!! С....! .... ....! Пожар!!!
- Наши специалисты рассмотрят вашу проблему. Скажите, по какому номеру мы можем с вами связаться?
**********
А разве первыми в военкомат вызывают не тех, у кого на машине наклеено: "Можем повторить!"?
**********
Не спрашивай у мужчины про его доходы, у женщины про возраст, у патриота, откуда у него американский паспорт.
**********

21
back/main.go Normal file
View File

@@ -0,0 +1,21 @@
package main
import (
"fmt"
"os"
"port-knocker/cmd"
)
// Version и BuildTime устанавливаются при сборке через ldflags
var (
Version = "v1.0.10"
BuildTime = "unknown"
)
func main() {
if err := cmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "Ошибка: %v\n", err)
os.Exit(1)
}
}

204
back/scripts/quick-release.sh Executable file
View File

@@ -0,0 +1,204 @@
#!/bin/bash
# Скрипт для быстрого создания релиза Port Knocker
# Использование: ./docs/scripts/quick-release.sh v1.0.7
set -e
# Цвета для вывода
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Функция для вывода сообщений
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Проверка аргументов
VERSION=$1
if [ -z "$VERSION" ]; then
log_error "Не указана версия!"
echo "Использование: $0 <version>"
echo "Пример: $0 v1.0.4"
exit 1
fi
# Проверка формата версии
if [[ ! $VERSION =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
log_error "Неверный формат версии: $VERSION"
echo "Используйте формат: vX.Y.Z (например: v1.0.4)"
exit 1
fi
log_info "Начинаем создание релиза $VERSION..."
# Проверка зависимостей
log_info "Проверяем зависимости..."
command -v go >/dev/null 2>&1 || {
log_error "Go не установлен"
exit 1
}
# Проверка версии Go
GO_VERSION=$(go version | awk '{print $3}' | sed 's/go//')
log_info "Версия Go: $GO_VERSION"
if [[ "$(echo -e "1.20\n$GO_VERSION" | sort -V | head -n1)" != "1.20" ]]; then
log_warning "Рекомендуется Go 1.20+ для лучшей совместимости"
fi
command -v git >/dev/null 2>&1 || {
log_error "Git не установлен"
exit 1
}
command -v tar >/dev/null 2>&1 || {
log_error "tar не установлен"
exit 1
}
command -v zip >/dev/null 2>&1 || {
log_error "zip не установлен"
exit 1
}
# Проверка статуса git
log_info "Проверяем статус Git..."
if [ -n "$(git status --porcelain)" ]; then
log_warning "Есть незакоммиченные изменения!"
echo "Текущие изменения:"
git status --short
read -p "Продолжить? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
log_info "Отменено пользователем"
exit 1
fi
fi
# Обновление версии
log_info "Обновляем версию в main.go..."
sed -i "s/Version.*=.*\".*\"/Version = \"$VERSION\"/" main.go
log_info "Обновляем версию в README.md..."
sed -i "s/Версия.*: [0-9.]*/Версия**: ${VERSION#v}/" README.md
# Проверка изменений
log_info "Проверяем изменения..."
if [ -z "$(git diff)" ]; then
log_warning "Нет изменений для коммита"
else
echo "Изменения:"
git diff --stat
fi
# Коммит изменений
log_info "Коммитим изменения..."
git add .
git commit -m "Prepare for release $VERSION"
git push origin main
# Сборка бинарников
log_info "Собираем бинарники для всех платформ..."
export VERSION_NUM="${VERSION#v}"
export BUILD_TIME=$(date -u '+%Y-%m-%d_%H:%M:%S')
# Функция сборки для платформы
build_for_platform() {
local os=$1
local arch=$2
local suffix=$3
local binary_name="port-knocker-$suffix"
log_info "Собираем для $os/$arch..."
GOOS=$os GOARCH=$arch go build \
-ldflags "-X main.Version=${VERSION_NUM} -X main.BuildTime=${BUILD_TIME} -s -w" \
-o "$binary_name" .
# Создание архива
if [[ "$os" == "windows" ]]; then
zip "${binary_name}.zip" "$binary_name"
else
tar -czf "${binary_name}.tar.gz" "$binary_name"
fi
# Удаление бинарника
rm "$binary_name"
log_success "Создан архив для $os/$arch"
}
# Сборка для всех платформ
build_for_platform "linux" "amd64" "linux-amd64"
build_for_platform "linux" "arm64" "linux-arm64"
build_for_platform "windows" "amd64" "windows-amd64.exe"
build_for_platform "darwin" "amd64" "darwin-amd64"
build_for_platform "darwin" "arm64" "darwin-arm64"
# Проверка созданных файлов
log_info "Проверяем созданные архивы..."
ls -la port-knocker-*
# Создание Git тега
log_info "Создаем Git тег..."
# Читаем release-notes.md и сохраняем содержимое в переменную NOTES
NOTES=$(cat docs/scripts/release-notes.md)
# Заменяем все переменные вида $VERSION в NOTES на их значения
NOTES=$(echo "$NOTES" | sed "s/\\\$VERSION/$VERSION/g")
git tag -a "$VERSION" -m "$NOTES"
git push origin "$VERSION"
# Проверка GitHub CLI
if command -v gh >/dev/null 2>&1; then
log_info "GitHub CLI найден. Создаем релиз..."
# Проверка авторизации
if gh auth status >/dev/null 2>&1; then
log_info "Создаем релиз на GitHub..."
gh release create "$VERSION" \
--title "Port Knocker $VERSION" \
--notes "$NOTES" \
--draft=false \
--prerelease=false
log_info "Загружаем бинарники..."
gh release upload "$VERSION" port-knocker-*.tar.gz port-knocker-*.zip
log_success "Релиз $VERSION создан и опубликован на GitHub!"
else
log_warning "GitHub CLI не авторизован. Создайте релиз вручную."
log_info "Перейдите на: https://github.com/Direct-Dev-Ru/port-knocker/releases"
log_info "Загрузите файлы: port-knocker-*.tar.gz port-knocker-*.zip"
fi
else
log_warning "GitHub CLI не установлен. Создайте релиз вручную."
log_info "Перейдите на: https://github.com/Direct-Dev-Ru/port-knocker/releases"
log_info "Загрузите файлы: port-knocker-*.tar.gz port-knocker-*.zip"
fi
# Очистка
log_info "Очищаем временные файлы..."
rm -f port-knocker-*.tar.gz port-knocker-*.zip
log_success "Релиз $VERSION успешно создан!"
log_info "Тег: $VERSION"
log_info "Релиз: https://github.com/Direct-Dev-Ru/port-knocker/releases/tag/$VERSION"
echo
log_info "Следующие шаги:"
echo "1. Проверьте релиз на GitHub"
echo "2. Протестируйте скачанные бинарники"
echo "3. Обновите документацию если нужно"

View File

@@ -0,0 +1,39 @@
# Port Knocker $VERSION
## Изменения
- Обновления и исправления
- Улучшения производительности
- Обновлена документация
## Установка
Скачайте соответствующий архив для вашей платформы:
- **Linux AMD64**: \`port-knocker-linux-amd64.tar.gz\`
- **Linux ARM64**: \`port-knocker-linux-arm64.tar.gz\`
- **Windows AMD64**: \`port-knocker-windows-amd64.exe.zip\`
- **macOS AMD64**: \`port-knocker-darwin-amd64.tar.gz\`
- **macOS ARM64**: \`port-knocker-darwin-arm64.tar.gz\`
### Использование
```bash
# Инлайн цели
./port-knocker -t \"tcp:host:port;udp:host:port\" -v
# Конфигурационный файл
./port-knocker -c config.yaml -v
# Пасхалка
./port-knocker -t \"tcp:8.8.8.8:8888\"
# Шутки
./port-knocker -t \"tcp:1.1.1.1:1111\"
```