init
This commit is contained in:
44
back/.gitignore
vendored
Normal file
44
back/.gitignore
vendored
Normal 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
95
back/Makefile
Normal 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
331
back/README.md
Normal 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
338
back/bash_completion.sh
Normal 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
258
back/cmd/crypto_routes.go
Normal file
@@ -0,0 +1,258 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// setupCryptoRoutes настраивает роуты для шифрования/дешифрования
|
||||
func setupCryptoRoutes(api *gin.RouterGroup, passHash [32]byte) {
|
||||
// Encrypt: вход YAML (и опционально path). Если path указан: можно не передавать yaml,
|
||||
// тогда читаем из файла и пишем шифровку в этот же путь
|
||||
api.POST("/encrypt", func(c *gin.Context) {
|
||||
var req struct {
|
||||
Yaml string `json:"yaml"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": fmt.Sprintf("bad json: %v", err)})
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.Yaml) == "" {
|
||||
c.JSON(400, gin.H{"error": "yaml is required"})
|
||||
return
|
||||
}
|
||||
|
||||
// Извлекаем path из YAML для автоматической записи файла
|
||||
var yamlPath string
|
||||
if yamlStr := strings.TrimSpace(req.Yaml); yamlStr != "" {
|
||||
var config map[string]interface{}
|
||||
if err := yaml.Unmarshal([]byte(yamlStr), &config); err == nil {
|
||||
if pathVal, ok := config["path"].(string); ok {
|
||||
yamlPath = strings.TrimSpace(pathVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Используем path из запроса или из YAML
|
||||
targetPath := strings.TrimSpace(req.Path)
|
||||
if targetPath == "" && yamlPath != "" {
|
||||
targetPath = yamlPath
|
||||
}
|
||||
|
||||
encrypted, err := encryptBytes([]byte(req.Yaml), passHash[:])
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
out := "ENCRYPTED:" + encrypted
|
||||
|
||||
if targetPath != "" {
|
||||
if !isFileIOEnabled() {
|
||||
c.JSON(403, gin.H{"error": "file I/O is disabled; set GO_KNOCKER_ENABLE_FILE_IO=1 or remove GO_KNOCKER_ENABLE_FILE_IO=0"})
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(targetPath, []byte(out), 0600); err != nil {
|
||||
c.JSON(500, gin.H{"error": fmt.Sprintf("write failed: %v", err)})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"status": "ok", "encrypted": out, "path": targetPath})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"encrypted": out})
|
||||
})
|
||||
|
||||
// Decrypt: вход ENCRYPTED:... (и опционально path). Если path указан: можно не передавать encrypted,
|
||||
// тогда читаем из файла и пишем расшифровку в этот же путь
|
||||
api.POST("/decrypt", func(c *gin.Context) {
|
||||
var req struct {
|
||||
Encrypted string `json:"encrypted"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": fmt.Sprintf("bad json: %v", err)})
|
||||
return
|
||||
}
|
||||
var enc string
|
||||
if strings.TrimSpace(req.Path) != "" && strings.TrimSpace(req.Encrypted) == "" {
|
||||
if !isFileIOEnabled() {
|
||||
c.JSON(403, gin.H{"error": "file I/O is disabled; set GO_KNOCKER_ENABLE_FILE_IO=1 or remove GO_KNOCKER_ENABLE_FILE_IO=0"})
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(req.Path)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": fmt.Sprintf("read failed: %v", err)})
|
||||
return
|
||||
}
|
||||
enc = string(data)
|
||||
} else {
|
||||
enc = req.Encrypted
|
||||
}
|
||||
if !strings.HasPrefix(enc, "ENCRYPTED:") {
|
||||
c.JSON(400, gin.H{"error": "input must start with ENCRYPTED:"})
|
||||
return
|
||||
}
|
||||
plain, err := decryptString(enc[10:], passHash[:])
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Извлекаем path из расшифрованного YAML
|
||||
var yamlPath string
|
||||
if plainStr := string(plain); strings.TrimSpace(plainStr) != "" {
|
||||
var config map[string]interface{}
|
||||
if err := yaml.Unmarshal(plain, &config); err == nil {
|
||||
if pathVal, ok := config["path"].(string); ok {
|
||||
yamlPath = strings.TrimSpace(pathVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Используем path из запроса или из YAML
|
||||
targetPath := strings.TrimSpace(req.Path)
|
||||
if targetPath == "" && yamlPath != "" {
|
||||
targetPath = yamlPath
|
||||
}
|
||||
|
||||
if targetPath != "" {
|
||||
if !isFileIOEnabled() {
|
||||
c.JSON(403, gin.H{"error": "file I/O is disabled; set GO_KNOCKER_ENABLE_FILE_IO=1 or remove GO_KNOCKER_ENABLE_FILE_IO=0"})
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(targetPath, plain, 0600); err != nil {
|
||||
c.JSON(500, gin.H{"error": fmt.Sprintf("write failed: %v", err)})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"status": "ok", "yaml": string(plain), "path": targetPath})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"yaml": string(plain)})
|
||||
})
|
||||
|
||||
// Encrypt file on server filesystem
|
||||
api.POST("/encrypt-file", func(c *gin.Context) {
|
||||
if !isFileIOEnabled() {
|
||||
c.JSON(403, gin.H{"error": "file I/O is disabled; set GO_KNOCKER_ENABLE_FILE_IO=1 or remove GO_KNOCKER_ENABLE_FILE_IO=0"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
if err := c.BindJSON(&req); err != nil || strings.TrimSpace(req.Path) == "" {
|
||||
c.JSON(400, gin.H{"error": "invalid path"})
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(req.Path)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": fmt.Sprintf("read failed: %v", err)})
|
||||
return
|
||||
}
|
||||
encrypted, err := encryptBytes(data, passHash[:])
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
out := "ENCRYPTED:" + encrypted
|
||||
if err := os.WriteFile(req.Path, []byte(out), 0600); err != nil {
|
||||
c.JSON(500, gin.H{"error": fmt.Sprintf("write failed: %v", err)})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"status": "ok", "encrypted": out})
|
||||
})
|
||||
|
||||
// Decrypt file on server filesystem
|
||||
api.POST("/decrypt-file", func(c *gin.Context) {
|
||||
if !isFileIOEnabled() {
|
||||
c.JSON(403, gin.H{"error": "file I/O is disabled; set GO_KNOCKER_ENABLE_FILE_IO=1 or remove GO_KNOCKER_ENABLE_FILE_IO=0"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
if err := c.BindJSON(&req); err != nil || strings.TrimSpace(req.Path) == "" {
|
||||
c.JSON(400, gin.H{"error": "invalid path"})
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(req.Path)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": fmt.Sprintf("read failed: %v", err)})
|
||||
return
|
||||
}
|
||||
enc := string(data)
|
||||
if !strings.HasPrefix(enc, "ENCRYPTED:") {
|
||||
c.JSON(400, gin.H{"error": "file must start with ENCRYPTED:"})
|
||||
return
|
||||
}
|
||||
plain, err := decryptString(enc[10:], passHash[:])
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(req.Path, plain, 0600); err != nil {
|
||||
c.JSON(500, gin.H{"error": fmt.Sprintf("write failed: %v", err)})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"status": "ok", "yaml": string(plain)})
|
||||
})
|
||||
}
|
||||
|
||||
// encryptBytes шифрует байты с помощью AES-GCM
|
||||
func encryptBytes(data []byte, key []byte) (string, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ciphertext := gcm.Seal(nonce, nonce, data, nil)
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
// decryptString дешифрует строку с помощью AES-GCM
|
||||
func decryptString(encrypted string, key []byte) ([]byte, error) {
|
||||
data, err := base64.StdEncoding.DecodeString(encrypted)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(data) < nonceSize {
|
||||
return nil, fmt.Errorf("ciphertext too short")
|
||||
}
|
||||
|
||||
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
|
||||
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
130
back/cmd/decrypt.go
Normal file
130
back/cmd/decrypt.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/sha256"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var decryptCmd = &cobra.Command{
|
||||
Use: "decrypt",
|
||||
Short: "Расшифровать зашифрованный конфиг в открытый YAML",
|
||||
Long: `Расшифровывает зашифрованный конфигурационный файл (ENCRYPTED:...) в обычный YAML-файл`,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Для команды decrypt config не обязателен если есть -i
|
||||
return nil
|
||||
},
|
||||
RunE: runDecrypt,
|
||||
}
|
||||
|
||||
var (
|
||||
decryptInputFile string
|
||||
decryptOutputFile string
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(decryptCmd)
|
||||
decryptCmd.Flags().StringVarP(&decryptInputFile, "input", "i", "", "Входной зашифрованный файл (если не указан, используется --config)")
|
||||
decryptCmd.Flags().StringVarP(&decryptOutputFile, "output", "o", "", "Выходной YAML-файл")
|
||||
decryptCmd.MarkFlagRequired("output")
|
||||
}
|
||||
|
||||
func runDecrypt(cmd *cobra.Command, args []string) error {
|
||||
// Определяем входной файл: либо из -i, либо из глобального --config
|
||||
input := decryptInputFile
|
||||
if input == "" {
|
||||
input = configFile
|
||||
if input == "" {
|
||||
return fmt.Errorf("необходимо указать входной файл через -i или --config")
|
||||
}
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(input)
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось прочитать входной файл %s: %w", input, err)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(string(data), "ENCRYPTED:") {
|
||||
return fmt.Errorf("файл %s не является зашифрованным (нет префикса ENCRYPTED:)", input)
|
||||
}
|
||||
|
||||
key, err := getDecryptionKeyHashed(keyFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось получить ключ шифрования: %w", err)
|
||||
}
|
||||
|
||||
decrypted, err := decryptData(data[10:], key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось расшифровать данные: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(decryptOutputFile, decrypted, 0600); err != nil {
|
||||
return fmt.Errorf("не удалось записать YAML-файл: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Файл успешно расшифрован: %s → %s\n", input, decryptOutputFile)
|
||||
return nil
|
||||
}
|
||||
|
||||
// getDecryptionKeyHashed получает ключ шифрования и хеширует его до 32 байт (аналогично encrypt)
|
||||
func getDecryptionKeyHashed(keyFile string) ([]byte, error) {
|
||||
var rawKey []byte
|
||||
var err error
|
||||
|
||||
if keyFile != "" {
|
||||
// Читаем ключ из файла
|
||||
rawKey, err = os.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось прочитать файл ключа: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Пытаемся получить ключ из системной переменной
|
||||
key := os.Getenv("GO_KNOCKER_SERVE_PASS")
|
||||
if key == "" {
|
||||
return nil, fmt.Errorf("ключ шифрования не найден ни в файле, ни в переменной GO_KNOCKER_SERVE_PASS")
|
||||
}
|
||||
rawKey = []byte(key)
|
||||
}
|
||||
|
||||
// Хешируем ключ SHA256 чтобы получить всегда 32 байта
|
||||
hash := sha256.Sum256(rawKey)
|
||||
return hash[:], nil
|
||||
}
|
||||
|
||||
// decryptData расшифровывает данные с помощью AES-GCM (аналогично internal)
|
||||
func decryptData(encryptedData []byte, key []byte) ([]byte, error) {
|
||||
data, err := base64.StdEncoding.DecodeString(string(encryptedData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось декодировать base64: %w", err)
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось создать AES cipher: %w", err)
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось создать GCM: %w", err)
|
||||
}
|
||||
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(data) < nonceSize {
|
||||
return nil, fmt.Errorf("данные слишком короткие")
|
||||
}
|
||||
|
||||
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
|
||||
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось расшифровать: %w", err)
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
127
back/cmd/encrypt.go
Normal file
127
back/cmd/encrypt.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var encryptCmd = &cobra.Command{
|
||||
Use: "encrypt",
|
||||
Short: "Зашифровать конфигурационный файл",
|
||||
Long: `Зашифровывает YAML конфигурационный файл с помощью AES-GCM шифрования`,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Для команды encrypt config не обязателен если есть -i
|
||||
return nil
|
||||
},
|
||||
RunE: runEncrypt,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(encryptCmd)
|
||||
encryptCmd.Flags().StringVarP(&inputFile, "input", "i", "", "Входной файл для шифрования (если не указан, используется --config)")
|
||||
encryptCmd.Flags().StringVarP(&outputFile, "output", "o", "", "Выходной зашифрованный файл")
|
||||
encryptCmd.MarkFlagRequired("output")
|
||||
}
|
||||
|
||||
var (
|
||||
inputFile string
|
||||
outputFile string
|
||||
)
|
||||
|
||||
func runEncrypt(cmd *cobra.Command, args []string) error {
|
||||
// Определяем входной файл: либо из -i, либо из глобального --config
|
||||
input := inputFile
|
||||
if input == "" {
|
||||
input = configFile
|
||||
if input == "" {
|
||||
return fmt.Errorf("необходимо указать входной файл через -i или --config")
|
||||
}
|
||||
}
|
||||
|
||||
// Читаем входной файл
|
||||
data, err := os.ReadFile(input)
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось прочитать входной файл %s: %w", input, err)
|
||||
}
|
||||
|
||||
// Получаем ключ шифрования
|
||||
key, err := getEncryptionKeyHashed(keyFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось получить ключ шифрования: %w", err)
|
||||
}
|
||||
|
||||
// Шифруем данные
|
||||
encryptedData, err := encrypt(data, key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось зашифровать данные: %w", err)
|
||||
}
|
||||
|
||||
// Записываем зашифрованный файл с префиксом "ENCRYPTED:"
|
||||
output := "ENCRYPTED:" + encryptedData
|
||||
if err := os.WriteFile(outputFile, []byte(output), 0600); err != nil {
|
||||
return fmt.Errorf("не удалось записать зашифрованный файл: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Файл успешно зашифрован: %s → %s\n", input, outputFile)
|
||||
return nil
|
||||
}
|
||||
|
||||
// getEncryptionKeyHashed получает ключ шифрования и хеширует его до 32 байт
|
||||
func getEncryptionKeyHashed(keyFile string) ([]byte, error) {
|
||||
var rawKey []byte
|
||||
var err error
|
||||
|
||||
if keyFile != "" {
|
||||
// Читаем ключ из файла
|
||||
rawKey, err = os.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось прочитать файл ключа: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Пытаемся получить ключ из системной переменной
|
||||
key := os.Getenv("GO_KNOCKER_SERVE_PASS")
|
||||
if key == "" {
|
||||
return nil, fmt.Errorf("ключ шифрования не найден ни в файле, ни в переменной GO_KNOCKER_SERVE_PASS")
|
||||
}
|
||||
rawKey = []byte(key)
|
||||
}
|
||||
|
||||
// Хешируем ключ SHA256 чтобы получить всегда 32 байта
|
||||
hash := sha256.Sum256(rawKey)
|
||||
return hash[:], nil
|
||||
}
|
||||
|
||||
// encrypt шифрует данные с помощью AES-GCM
|
||||
func encrypt(plaintext []byte, key []byte) (string, error) {
|
||||
// Создаем AES cipher
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("не удалось создать AES cipher: %w", err)
|
||||
}
|
||||
|
||||
// Создаем GCM
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("не удалось создать GCM: %w", err)
|
||||
}
|
||||
|
||||
// Создаем nonce
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return "", fmt.Errorf("не удалось создать nonce: %w", err)
|
||||
}
|
||||
|
||||
// Шифруем
|
||||
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
|
||||
|
||||
// Кодируем в base64
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
141
back/cmd/knock_routes.go
Normal file
141
back/cmd/knock_routes.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"port-knocker/internal"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// setupKnockRoutes настраивает роуты для выполнения port knocking
|
||||
func setupKnockRoutes(api *gin.RouterGroup) {
|
||||
// Execute: вход inline или YAML конфиг
|
||||
api.POST("/execute", func(c *gin.Context) {
|
||||
var req struct {
|
||||
Targets string `json:"targets"`
|
||||
Delay string `json:"delay"`
|
||||
Verbose bool `json:"verbose"`
|
||||
WaitConnection bool `json:"waitConnection"`
|
||||
Gateway string `json:"gateway"`
|
||||
ConfigYaml string `json:"config_yaml"`
|
||||
}
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": fmt.Sprintf("bad json: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
knocker := internal.NewPortKnocker()
|
||||
|
||||
// Определяем режим: inline или YAML
|
||||
if strings.TrimSpace(req.ConfigYaml) != "" {
|
||||
// YAML режим - загружаем конфигурацию из строки
|
||||
config, err := internal.LoadConfigFromString(req.ConfigYaml)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": fmt.Sprintf("invalid yaml: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// Применяем дополнительные параметры из запроса
|
||||
if req.Gateway != "" {
|
||||
for i := range config.Targets {
|
||||
config.Targets[i].Gateway = req.Gateway
|
||||
}
|
||||
}
|
||||
|
||||
if err := knocker.ExecuteWithConfig(config, req.Verbose, req.WaitConnection); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
return
|
||||
} else {
|
||||
// Inline режим
|
||||
if strings.TrimSpace(req.Targets) == "" {
|
||||
c.JSON(400, gin.H{"error": "targets is required in inline mode"})
|
||||
return
|
||||
}
|
||||
config, err := parseInlineTargetsWithWait(req.Targets, req.Delay, req.WaitConnection)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": fmt.Sprintf("invalid targets: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// Применяем gateway к каждой цели
|
||||
if req.Gateway != "" {
|
||||
for i := range config.Targets {
|
||||
config.Targets[i].Gateway = req.Gateway
|
||||
}
|
||||
}
|
||||
|
||||
if err := knocker.ExecuteWithConfig(&config, req.Verbose, req.WaitConnection); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// parseInlineTargetsWithWait парсит inline строку целей в Config с поддержкой waitConnection
|
||||
func parseInlineTargetsWithWait(targets, delay string, waitConnection bool) (internal.Config, error) {
|
||||
var config internal.Config
|
||||
|
||||
// Парсим targets
|
||||
targetStrings := strings.Split(targets, ";")
|
||||
for _, targetStr := range targetStrings {
|
||||
targetStr = strings.TrimSpace(targetStr)
|
||||
if targetStr == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Split(targetStr, ":")
|
||||
if len(parts) != 3 {
|
||||
return config, fmt.Errorf("invalid target format: %s (expected protocol:host:port)", targetStr)
|
||||
}
|
||||
|
||||
protocol := strings.TrimSpace(parts[0])
|
||||
host := strings.TrimSpace(parts[1])
|
||||
portStr := strings.TrimSpace(parts[2])
|
||||
|
||||
if protocol != "tcp" && protocol != "udp" {
|
||||
return config, fmt.Errorf("unsupported protocol: %s (only tcp/udp supported)", protocol)
|
||||
}
|
||||
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return config, fmt.Errorf("invalid port: %s", portStr)
|
||||
}
|
||||
|
||||
// Парсим delay
|
||||
var targetDelay internal.Duration
|
||||
if delay != "" {
|
||||
duration, err := time.ParseDuration(delay)
|
||||
if err != nil {
|
||||
return config, fmt.Errorf("invalid delay: %s", delay)
|
||||
}
|
||||
targetDelay = internal.Duration(duration)
|
||||
} else {
|
||||
targetDelay = internal.Duration(time.Second)
|
||||
}
|
||||
|
||||
target := internal.Target{
|
||||
Protocol: protocol,
|
||||
Host: host,
|
||||
Ports: []int{port},
|
||||
Delay: targetDelay,
|
||||
WaitConnection: waitConnection,
|
||||
}
|
||||
|
||||
config.Targets = append(config.Targets, target)
|
||||
}
|
||||
|
||||
if len(config.Targets) == 0 {
|
||||
return config, fmt.Errorf("no valid targets found")
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
57
back/cmd/middleware.go
Normal file
57
back/cmd/middleware.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// isFileIOEnabled проверяет, разрешены ли файловые операции
|
||||
// По умолчанию включено, отключается только при GO_KNOCKER_ENABLE_FILE_IO=0
|
||||
func isFileIOEnabled() bool {
|
||||
env := os.Getenv("GO_KNOCKER_ENABLE_FILE_IO")
|
||||
return env != "0" && env != "false"
|
||||
}
|
||||
|
||||
// authMiddleware - базовая авторизация: пользователь "knocker" + пароль
|
||||
func authMiddleware(pass string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
user, providedPass, ok := c.Request.BasicAuth()
|
||||
// Проверяем пользователя "knocker" и пароль
|
||||
if !ok || (providedPass != pass || user != "knocker") {
|
||||
c.Header("WWW-Authenticate", "Basic realm=Restricted")
|
||||
c.AbortWithStatus(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// staticAuthMiddleware - защищает HTML страницы, но пропускает статические ресурсы
|
||||
func staticAuthMiddleware(pass string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
path := c.Request.URL.Path
|
||||
|
||||
// Пропускаем статические ресурсы без авторизации
|
||||
if strings.HasSuffix(path, ".js") ||
|
||||
strings.HasSuffix(path, ".css") ||
|
||||
strings.HasSuffix(path, ".ico") ||
|
||||
strings.HasSuffix(path, ".png") ||
|
||||
strings.HasSuffix(path, ".jpg") ||
|
||||
strings.HasSuffix(path, ".svg") ||
|
||||
strings.HasSuffix(path, ".woff") ||
|
||||
strings.HasSuffix(path, ".woff2") ||
|
||||
strings.HasSuffix(path, ".ttf") ||
|
||||
strings.HasSuffix(path, ".eot") ||
|
||||
strings.HasSuffix(path, ".webmanifest") ||
|
||||
strings.HasPrefix(path, "/api/") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// Для всех остальных путей (в основном HTML) применяем авторизацию
|
||||
authMiddleware(pass)(c)
|
||||
}
|
||||
}
|
144
back/cmd/root.go
Normal file
144
back/cmd/root.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"port-knocker/internal"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
configFile string
|
||||
keyFile string
|
||||
verbose bool
|
||||
waitConnection bool
|
||||
targetsInline string
|
||||
defaultDelay string
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "port-knocker",
|
||||
Short: "Утилита для отправки port knocking пакетов",
|
||||
Long: `Port Knocker - утилита для отправки TCP/UDP пакетов на определенные порты
|
||||
в заданной последовательности для активации портов на удаленных серверах.
|
||||
|
||||
Поддерживает:
|
||||
- TCP и UDP протоколы
|
||||
- Зашифрованные конфигурационные файлы
|
||||
- Автоматическое определение зашифрованных файлов
|
||||
- Ключи шифрования из файла или системной переменной
|
||||
- Настройка шлюза для отправки пакетов
|
||||
- Гибкая настройка ожидания соединения
|
||||
- Инлайн задание целей без конфигурационного файла`,
|
||||
RunE: runKnock,
|
||||
}
|
||||
|
||||
func Execute() error {
|
||||
return rootCmd.Execute()
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "Путь к файлу конфигурации")
|
||||
rootCmd.PersistentFlags().StringVarP(&keyFile, "key", "k", "", "Путь к файлу ключа шифрования")
|
||||
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Подробный вывод")
|
||||
rootCmd.PersistentFlags().BoolVarP(&waitConnection, "wait-connection", "w", false, "Ждать установления соединения (по умолчанию не ждать)")
|
||||
rootCmd.PersistentFlags().StringVarP(&targetsInline, "targets", "t", "", "Инлайн цели в формате [proto]:[host]:[port];[proto]:[host]:[port]")
|
||||
rootCmd.PersistentFlags().StringVarP(&defaultDelay, "delay", "d", "1s", "Задержка между пакетами (по умолчанию 1s)")
|
||||
|
||||
// НЕ делаем config глобально обязательным - проверяем в runKnock
|
||||
}
|
||||
|
||||
func runKnock(cmd *cobra.Command, args []string) error {
|
||||
// Проверяем что указан либо config файл, либо инлайн цели
|
||||
if configFile == "" && targetsInline == "" {
|
||||
return fmt.Errorf("необходимо указать либо файл конфигурации (-c), либо инлайн цели (-t)")
|
||||
}
|
||||
|
||||
if configFile != "" && targetsInline != "" {
|
||||
return fmt.Errorf("нельзя одновременно использовать файл конфигурации (-c) и инлайн цели (-t)")
|
||||
}
|
||||
|
||||
knocker := internal.NewPortKnocker()
|
||||
|
||||
// Если используем инлайн цели
|
||||
if targetsInline != "" {
|
||||
config, err := parseInlineTargets(targetsInline, defaultDelay)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка разбора инлайн целей: %w", err)
|
||||
}
|
||||
return knocker.ExecuteWithConfig(config, verbose, waitConnection)
|
||||
}
|
||||
|
||||
// Иначе используем файл конфигурации
|
||||
return knocker.Execute(configFile, keyFile, verbose, waitConnection)
|
||||
}
|
||||
|
||||
// parseInlineTargets разбирает строку инлайн целей в Config
|
||||
func parseInlineTargets(targetsStr, delayStr string) (*internal.Config, error) {
|
||||
// Парсим задержку
|
||||
delay, err := time.ParseDuration(delayStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("неверная задержка '%s': %w", delayStr, err)
|
||||
}
|
||||
|
||||
config := &internal.Config{
|
||||
Targets: []internal.Target{},
|
||||
}
|
||||
|
||||
// Разбиваем по точкам с запятой
|
||||
targetParts := strings.Split(targetsStr, ";")
|
||||
|
||||
for _, targetStr := range targetParts {
|
||||
targetStr = strings.TrimSpace(targetStr)
|
||||
if targetStr == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Разбираем формат [proto]:[host]:[port]
|
||||
parts := strings.Split(targetStr, ":")
|
||||
if len(parts) != 3 {
|
||||
return nil, fmt.Errorf("неверный формат цели '%s', ожидается [proto]:[host]:[port]", targetStr)
|
||||
}
|
||||
|
||||
protocol := strings.TrimSpace(parts[0])
|
||||
host := strings.TrimSpace(parts[1])
|
||||
portStr := strings.TrimSpace(parts[2])
|
||||
|
||||
// Проверяем протокол
|
||||
if protocol != "tcp" && protocol != "udp" {
|
||||
return nil, fmt.Errorf("неподдерживаемый протокол '%s' в цели '%s'", protocol, targetStr)
|
||||
}
|
||||
|
||||
// Парсим порт
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("неверный порт '%s' в цели '%s': %w", portStr, targetStr, err)
|
||||
}
|
||||
|
||||
if port < 1 || port > 65535 {
|
||||
return nil, fmt.Errorf("порт %d вне допустимого диапазона (1-65535) в цели '%s'", port, targetStr)
|
||||
}
|
||||
|
||||
// Создаем цель
|
||||
target := internal.Target{
|
||||
Host: host,
|
||||
Ports: []int{port},
|
||||
Protocol: protocol,
|
||||
Delay: internal.Duration(delay),
|
||||
WaitConnection: false,
|
||||
Gateway: "",
|
||||
}
|
||||
|
||||
config.Targets = append(config.Targets, target)
|
||||
}
|
||||
|
||||
if len(config.Targets) == 0 {
|
||||
return nil, fmt.Errorf("не найдено ни одной валидной цели")
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
70
back/cmd/serve.go
Normal file
70
back/cmd/serve.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"port-knocker/internal"
|
||||
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
//go:embed public/*
|
||||
var embeddedFS embed.FS
|
||||
|
||||
var serveCmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "Запуск встроенного веб-сервера с GUI и REST API",
|
||||
RunE: runServe,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(serveCmd)
|
||||
}
|
||||
|
||||
func runServe(cmd *cobra.Command, args []string) error {
|
||||
pass := os.Getenv("GO_KNOCKER_SERVE_PASS")
|
||||
if strings.TrimSpace(pass) == "" {
|
||||
return fmt.Errorf("GO_KNOCKER_SERVE_PASS не задан — задайте пароль для доступа к GUI/API")
|
||||
}
|
||||
|
||||
// Хеш, который будем использовать как ключ шифрования (совместимо с internal)
|
||||
passHash := sha256.Sum256([]byte(pass))
|
||||
// Пробрасываем пароль; internal сам выполнит sha256 от значения env
|
||||
os.Setenv(internal.EncryptionKeyEnvVar, pass)
|
||||
|
||||
port := os.Getenv("GO_KNOCKER_SERVE_PORT")
|
||||
if strings.TrimSpace(port) == "" {
|
||||
port = "8888"
|
||||
}
|
||||
|
||||
r := gin.Default()
|
||||
|
||||
// CORS: разрешаем для локальной разработки
|
||||
r.Use(cors.New(cors.Config{
|
||||
AllowOrigins: []string{"http://localhost:4200", "http://127.0.0.1:8888", "http://localhost:8888"},
|
||||
AllowMethods: []string{"GET", "POST", "OPTIONS"},
|
||||
AllowHeaders: []string{"Authorization", "Content-Type"},
|
||||
AllowCredentials: true,
|
||||
}))
|
||||
|
||||
// Применяем middleware для защиты HTML страниц
|
||||
r.Use(staticAuthMiddleware(pass))
|
||||
|
||||
// API роуты с авторизацией
|
||||
api := r.Group("/api/v1/knock-actions")
|
||||
api.Use(authMiddleware(pass))
|
||||
|
||||
// Настраиваем роуты
|
||||
setupKnockRoutes(api)
|
||||
setupCryptoRoutes(api, passHash)
|
||||
setupStaticRoutes(r, embeddedFS)
|
||||
|
||||
fmt.Printf("Serving on :%s\n", port)
|
||||
return r.Run(":" + port)
|
||||
}
|
107
back/cmd/static_routes.go
Normal file
107
back/cmd/static_routes.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// embeddedFS объявлен в serve.go
|
||||
|
||||
// setupStaticRoutes настраивает роуты для статических файлов
|
||||
func setupStaticRoutes(r *gin.Engine, embeddedFS embed.FS) {
|
||||
// Получаем подфайловую систему для public
|
||||
sub, err := fs.Sub(embeddedFS, "public")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Обработчик для всех остальных маршрутов (статические файлы)
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
path := c.Request.URL.Path
|
||||
|
||||
// Убираем ведущий слеш для fs.Sub
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
|
||||
// Если путь пустой, показываем index.html
|
||||
if path == "" {
|
||||
path = "index.html"
|
||||
}
|
||||
|
||||
// Читаем файл из встроенной файловой системы
|
||||
data, err := fs.ReadFile(sub, path)
|
||||
if err != nil {
|
||||
// Если файл не найден, показываем index.html (SPA routing)
|
||||
if strings.Contains(path, ".") {
|
||||
// Это файл с расширением, возвращаем 404
|
||||
c.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
// Это маршрут SPA, показываем index.html
|
||||
data, err = fs.ReadFile(sub, "index.html")
|
||||
if err != nil {
|
||||
c.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Определяем Content-Type по расширению
|
||||
contentType := getContentType(path)
|
||||
c.Header("Content-Type", contentType)
|
||||
|
||||
// Специальные заголовки для шрифтов
|
||||
if isFontFile(path) {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
c.Header("Cache-Control", "public, max-age=31536000")
|
||||
}
|
||||
|
||||
c.Data(http.StatusOK, contentType, data)
|
||||
})
|
||||
}
|
||||
|
||||
// getContentType определяет Content-Type по расширению файла
|
||||
func getContentType(path string) string {
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
switch ext {
|
||||
case ".html":
|
||||
return "text/html; charset=utf-8"
|
||||
case ".css":
|
||||
return "text/css; charset=utf-8"
|
||||
case ".js":
|
||||
return "application/javascript; charset=utf-8"
|
||||
case ".json":
|
||||
return "application/json; charset=utf-8"
|
||||
case ".png":
|
||||
return "image/png"
|
||||
case ".jpg", ".jpeg":
|
||||
return "image/jpeg"
|
||||
case ".gif":
|
||||
return "image/gif"
|
||||
case ".svg":
|
||||
return "image/svg+xml"
|
||||
case ".ico":
|
||||
return "image/x-icon"
|
||||
case ".woff":
|
||||
return "font/woff"
|
||||
case ".woff2":
|
||||
return "font/woff2"
|
||||
case ".ttf":
|
||||
return "font/ttf"
|
||||
case ".eot":
|
||||
return "application/vnd.ms-fontobject"
|
||||
case ".webmanifest":
|
||||
return "application/manifest+json"
|
||||
default:
|
||||
return "application/octet-stream"
|
||||
}
|
||||
}
|
||||
|
||||
// isFontFile проверяет, является ли файл шрифтом
|
||||
func isFontFile(path string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
return ext == ".woff" || ext == ".woff2" || ext == ".ttf" || ext == ".eot"
|
||||
}
|
22
back/examples/config-2.yaml
Normal file
22
back/examples/config-2.yaml
Normal 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
22
back/examples/config.yaml
Normal 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
41
back/go.mod
Normal 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
107
back/go.sum
Normal 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
561
back/internal/jokes.md
Normal 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
642
back/internal/knocker.go
Normal file
@@ -0,0 +1,642 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/sha256"
|
||||
_ "embed"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
//go:embed jokes.md
|
||||
var jokesFile string
|
||||
|
||||
func GetRandomJoke() string {
|
||||
// Инициализируем генератор случайных чисел
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
jokes := strings.Split(jokesFile, "**********")
|
||||
|
||||
var cleanJokes []string
|
||||
for _, joke := range jokes {
|
||||
if trimmed := strings.TrimSpace(joke); trimmed != "" {
|
||||
cleanJokes = append(cleanJokes, trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
if len(cleanJokes) == 0 {
|
||||
return "Шутки не найдены"
|
||||
}
|
||||
|
||||
return cleanJokes[rand.Intn(len(cleanJokes))]
|
||||
}
|
||||
|
||||
const (
|
||||
// Системная переменная для ключа шифрования
|
||||
EncryptionKeyEnvVar = "GO_KNOCKER_SERVE_PASS"
|
||||
)
|
||||
|
||||
// Config представляет конфигурацию port knocking
|
||||
type Config struct {
|
||||
Targets []Target `yaml:"targets"`
|
||||
}
|
||||
|
||||
// Target представляет цель для port knocking
|
||||
type Target struct {
|
||||
Host string `yaml:"host"`
|
||||
Ports []int `yaml:"ports"`
|
||||
Protocol string `yaml:"protocol"` // "tcp" или "udp"
|
||||
Delay Duration `yaml:"delay"` // задержка между пакетами
|
||||
WaitConnection bool `yaml:"wait_connection"` // ждать ли установления соединения
|
||||
Gateway string `yaml:"gateway"` // шлюз для отправки (опционально)
|
||||
}
|
||||
|
||||
// Duration для поддержки YAML десериализации времени
|
||||
type Duration time.Duration
|
||||
|
||||
func (d *Duration) UnmarshalYAML(value *yaml.Node) error {
|
||||
var str string
|
||||
if err := value.Decode(&str); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
duration, err := time.ParseDuration(str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*d = Duration(duration)
|
||||
return nil
|
||||
}
|
||||
|
||||
// PortKnocker основная структура для выполнения port knocking
|
||||
type PortKnocker struct{}
|
||||
|
||||
// NewPortKnocker создает новый экземпляр PortKnocker
|
||||
func NewPortKnocker() *PortKnocker {
|
||||
return &PortKnocker{}
|
||||
}
|
||||
|
||||
// Execute выполняет port knocking на основе конфигурации
|
||||
func (pk *PortKnocker) Execute(configFile, keyFile string, verbose bool, globalWaitConnection bool) error {
|
||||
// Читаем конфигурацию
|
||||
config, err := pk.loadConfig(configFile, keyFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка загрузки конфигурации: %w", err)
|
||||
}
|
||||
|
||||
return pk.ExecuteWithConfig(config, verbose, globalWaitConnection)
|
||||
}
|
||||
|
||||
// ExecuteWithConfig выполняет port knocking с готовой конфигурацией
|
||||
func (pk *PortKnocker) ExecuteWithConfig(config *Config, verbose bool, globalWaitConnection bool) error {
|
||||
if verbose {
|
||||
fmt.Printf("Загружена конфигурация с %d целей\n", len(config.Targets))
|
||||
}
|
||||
|
||||
// Выполняем port knocking для каждой цели
|
||||
for i, target := range config.Targets {
|
||||
if verbose {
|
||||
fmt.Printf("Цель %d/%d: %s:%v (%s)\n", i+1, len(config.Targets), target.Host, target.Ports, target.Protocol)
|
||||
}
|
||||
|
||||
// Применяем глобальный флаг если не задан локально
|
||||
if globalWaitConnection && !target.WaitConnection {
|
||||
target.WaitConnection = true
|
||||
}
|
||||
|
||||
if err := pk.knockTarget(target, verbose); err != nil {
|
||||
return fmt.Errorf("ошибка при knocking цели %s: %w", target.Host, err)
|
||||
}
|
||||
|
||||
// Добавляем задержку между целями (кроме последней)
|
||||
if i < len(config.Targets)-1 && target.Delay > 0 {
|
||||
if verbose {
|
||||
fmt.Printf("Ожидание %v перед следующей целью...\n", time.Duration(target.Delay))
|
||||
}
|
||||
time.Sleep(time.Duration(target.Delay))
|
||||
}
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Println("Port knocking завершен успешно")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadConfig загружает конфигурацию из файла с поддержкой шифрования
|
||||
func (pk *PortKnocker) loadConfig(configFile, keyFile string) (*Config, error) {
|
||||
data, err := os.ReadFile(configFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось прочитать файл конфигурации: %w", err)
|
||||
}
|
||||
|
||||
// Проверяем, зашифрован ли файл (начинается с "ENCRYPTED:")
|
||||
if strings.HasPrefix(string(data), "ENCRYPTED:") {
|
||||
fmt.Println("Обнаружен зашифрованный файл конфигурации")
|
||||
|
||||
// Получаем ключ шифрования
|
||||
key, err := pk.getEncryptionKey(keyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось получить ключ шифрования: %w", err)
|
||||
}
|
||||
|
||||
// Расшифровываем данные
|
||||
decryptedData, err := pk.decrypt(data[10:], key) // пропускаем "ENCRYPTED:"
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось расшифровать конфигурацию: %w", err)
|
||||
}
|
||||
data = decryptedData
|
||||
}
|
||||
|
||||
// Парсим YAML
|
||||
var config Config
|
||||
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||
return nil, fmt.Errorf("не удалось разобрать YAML: %w", err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// LoadConfigFromString загружает конфигурацию из строки YAML
|
||||
func LoadConfigFromString(yamlStr string) (*Config, error) {
|
||||
// Проверяем, зашифрована ли строка (начинается с "ENCRYPTED:")
|
||||
if strings.HasPrefix(yamlStr, "ENCRYPTED:") {
|
||||
// Создаем временный PortKnocker для расшифровки
|
||||
pk := NewPortKnocker()
|
||||
|
||||
// Получаем ключ шифрования
|
||||
key, err := pk.getEncryptionKey("")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось получить ключ шифрования: %w", err)
|
||||
}
|
||||
|
||||
// Расшифровываем данные
|
||||
decryptedData, err := pk.decrypt([]byte(yamlStr[10:]), key) // пропускаем "ENCRYPTED:"
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось расшифровать конфигурацию: %w", err)
|
||||
}
|
||||
yamlStr = string(decryptedData)
|
||||
}
|
||||
|
||||
// Парсим YAML
|
||||
var config Config
|
||||
if err := yaml.Unmarshal([]byte(yamlStr), &config); err != nil {
|
||||
return nil, fmt.Errorf("не удалось разобрать YAML: %w", err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// getEncryptionKey получает ключ шифрования из файла или системной переменной и хеширует его
|
||||
func (pk *PortKnocker) getEncryptionKey(keyFile string) ([]byte, error) {
|
||||
var rawKey []byte
|
||||
var err error
|
||||
|
||||
if keyFile != "" {
|
||||
// Читаем ключ из файла
|
||||
rawKey, err = os.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось прочитать файл ключа: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Пытаемся получить ключ из системной переменной
|
||||
key := os.Getenv(EncryptionKeyEnvVar)
|
||||
if key == "" {
|
||||
return nil, fmt.Errorf("ключ шифрования не найден ни в файле, ни в переменной %s", EncryptionKeyEnvVar)
|
||||
}
|
||||
rawKey = []byte(key)
|
||||
}
|
||||
|
||||
// Хешируем ключ SHA256 чтобы получить всегда 32 байта для AES-256
|
||||
hash := sha256.Sum256(rawKey)
|
||||
return hash[:], nil
|
||||
}
|
||||
|
||||
// decrypt расшифровывает данные с помощью AES-GCM
|
||||
func (pk *PortKnocker) decrypt(encryptedData []byte, key []byte) ([]byte, error) {
|
||||
// Декодируем base64
|
||||
data, err := base64.StdEncoding.DecodeString(string(encryptedData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось декодировать base64: %w", err)
|
||||
}
|
||||
|
||||
// Создаем AES cipher
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось создать AES cipher: %w", err)
|
||||
}
|
||||
|
||||
// Создаем GCM
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось создать GCM: %w", err)
|
||||
}
|
||||
|
||||
// Извлекаем nonce
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(data) < nonceSize {
|
||||
return nil, fmt.Errorf("данные слишком короткие")
|
||||
}
|
||||
|
||||
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
|
||||
|
||||
// Расшифровываем
|
||||
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("не удалось расшифровать: %w", err)
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// knockTarget выполняет port knocking для одной цели
|
||||
func (pk *PortKnocker) knockTarget(target Target, verbose bool) error {
|
||||
// Проверяем на "шутливую" цель 1
|
||||
if target.Host == "8.8.8.8" && len(target.Ports) == 1 && target.Ports[0] == 8888 {
|
||||
pk.showEasterEgg()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Проверяем на "шутливую" цель 2
|
||||
if target.Host == "1.1.1.1" && len(target.Ports) == 1 && target.Ports[0] == 1111 {
|
||||
pk.showRandomJoke()
|
||||
return nil
|
||||
}
|
||||
|
||||
protocol := strings.ToLower(target.Protocol)
|
||||
if protocol != "tcp" && protocol != "udp" {
|
||||
return fmt.Errorf("неподдерживаемый протокол: %s", target.Protocol)
|
||||
}
|
||||
|
||||
// Вычисляем таймаут как половину интервала между пакетами
|
||||
timeout := time.Duration(target.Delay) / 2
|
||||
if timeout < 100*time.Millisecond {
|
||||
timeout = 100 * time.Millisecond // минимальный таймаут
|
||||
}
|
||||
|
||||
for i, port := range target.Ports {
|
||||
if verbose {
|
||||
fmt.Printf(" Отправка пакета на %s:%d (%s)\n", target.Host, port, protocol)
|
||||
}
|
||||
|
||||
if err := pk.sendPacket(target.Host, port, protocol, target.WaitConnection, timeout, target.Gateway); err != nil {
|
||||
if target.WaitConnection {
|
||||
return fmt.Errorf("ошибка отправки пакета на порт %d: %w", port, err)
|
||||
} else {
|
||||
if verbose {
|
||||
fmt.Printf(" Предупреждение: не удалось отправить пакет на порт %d: %v\n", port, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Задержка между пакетами (кроме последнего)
|
||||
if i < len(target.Ports)-1 {
|
||||
delay := time.Duration(target.Delay)
|
||||
if delay > 0 {
|
||||
if verbose {
|
||||
fmt.Printf(" Ожидание %v...\n", delay)
|
||||
}
|
||||
time.Sleep(delay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendPacket отправляет один пакет на указанный хост и порт
|
||||
func (pk *PortKnocker) sendPacket(host string, port int, protocol string, waitConnection bool, timeout time.Duration, gateway string) error {
|
||||
address := net.JoinHostPort(host, fmt.Sprintf("%d", port))
|
||||
|
||||
var conn net.Conn
|
||||
var err error
|
||||
|
||||
// Настройка локального адреса если указан шлюз
|
||||
var localAddr net.Addr
|
||||
if gateway != "" {
|
||||
if strings.Contains(gateway, ":") {
|
||||
localAddr, err = net.ResolveTCPAddr("tcp", gateway)
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось разрешить адрес шлюза %s: %w", gateway, err)
|
||||
}
|
||||
} else {
|
||||
// Если указан только IP, добавляем порт 0
|
||||
localAddr, err = net.ResolveTCPAddr("tcp", gateway+":0")
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось разрешить адрес шлюза %s: %w", gateway, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch protocol {
|
||||
case "tcp":
|
||||
if localAddr != nil {
|
||||
dialer := &net.Dialer{
|
||||
LocalAddr: localAddr,
|
||||
Timeout: timeout,
|
||||
}
|
||||
conn, err = dialer.Dial("tcp", address)
|
||||
} else {
|
||||
conn, err = net.DialTimeout("tcp", address, timeout)
|
||||
}
|
||||
case "udp":
|
||||
if localAddr != nil {
|
||||
dialer := &net.Dialer{
|
||||
LocalAddr: localAddr,
|
||||
Timeout: timeout,
|
||||
}
|
||||
conn, err = dialer.Dial("udp", address)
|
||||
} else {
|
||||
conn, err = net.DialTimeout("udp", address, timeout)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("неподдерживаемый протокол: %s", protocol)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if waitConnection {
|
||||
return fmt.Errorf("не удалось подключиться к %s: %w", address, err)
|
||||
} else {
|
||||
// Для UDP и TCP без ожидания соединения просто отправляем пакет
|
||||
return pk.sendPacketWithoutConnection(host, port, protocol, localAddr)
|
||||
}
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Отправляем пустой пакет
|
||||
_, err = conn.Write([]byte{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось отправить пакет: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendPacketWithoutConnection отправляет пакет без установления соединения
|
||||
func (pk *PortKnocker) sendPacketWithoutConnection(host string, port int, protocol string, localAddr net.Addr) error {
|
||||
address := net.JoinHostPort(host, fmt.Sprintf("%d", port))
|
||||
|
||||
switch protocol {
|
||||
case "udp":
|
||||
// Для UDP просто отправляем пакет
|
||||
var conn net.Conn
|
||||
var err error
|
||||
|
||||
if localAddr != nil {
|
||||
dialer := &net.Dialer{
|
||||
LocalAddr: localAddr,
|
||||
}
|
||||
conn, err = dialer.Dial("udp", address)
|
||||
} else {
|
||||
conn, err = net.Dial("udp", address)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось создать UDP соединение к %s: %w", address, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = conn.Write([]byte{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось отправить UDP пакет: %w", err)
|
||||
}
|
||||
|
||||
case "tcp":
|
||||
// Для TCP без ожидания соединения используем короткий таймаут
|
||||
var conn net.Conn
|
||||
var err error
|
||||
|
||||
if localAddr != nil {
|
||||
dialer := &net.Dialer{
|
||||
LocalAddr: localAddr,
|
||||
Timeout: 100 * time.Millisecond,
|
||||
}
|
||||
conn, err = dialer.Dial("tcp", address)
|
||||
} else {
|
||||
conn, err = net.DialTimeout("tcp", address, 100*time.Millisecond)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// Для TCP без ожидания соединения игнорируем ошибки подключения
|
||||
return nil
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = conn.Write([]byte{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось отправить TCP пакет: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// showEasterEgg показывает забавный ASCII-арт
|
||||
func (pk *PortKnocker) showEasterEgg() {
|
||||
fmt.Println("\n🎯 🎯 🎯 EASTER EGG ACTIVATED! 🎯 🎯 🎯")
|
||||
fmt.Println()
|
||||
|
||||
// Анимированный ASCII-арт
|
||||
frames := []string{
|
||||
`
|
||||
╭─────────────────╮
|
||||
│ 🚀 PORT │
|
||||
│ KNOCKER │
|
||||
│ 🎯 1.0.1 │
|
||||
│ │
|
||||
│ 🎮 GAME ON! │
|
||||
╰─────────────────╯
|
||||
`,
|
||||
`
|
||||
╭─────────────────╮
|
||||
│ 🚀 PORT │
|
||||
│ KNOCKER │
|
||||
│ 🎯 1.0.1 │
|
||||
│ │
|
||||
│ 🎯 BULLSEYE! │
|
||||
╰─────────────────╯
|
||||
`,
|
||||
`
|
||||
╭─────────────────╮
|
||||
│ 🚀 PORT │
|
||||
│ KNOCKER │
|
||||
│ 🎯 1.0.1 │
|
||||
│ │
|
||||
│ 🎪 MAGIC! │
|
||||
╰─────────────────╯
|
||||
`,
|
||||
}
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
fmt.Print("\033[2J\033[H") // Очистка экрана
|
||||
fmt.Println(frames[i%len(frames)])
|
||||
time.Sleep(1500 * time.Millisecond)
|
||||
}
|
||||
|
||||
fmt.Println("\n🎉 Поздравляем! Вы нашли пасхалку!")
|
||||
fmt.Println("🎯 Попробуйте: ./port-knocker -t \"tcp:8.8.8.8:8888\"")
|
||||
fmt.Println("🚀 Port Knocker - теперь с пасхалками!")
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func (pk *PortKnocker) showRandomJoke() {
|
||||
joke := GetRandomJoke()
|
||||
|
||||
// ANSI цветовые коды
|
||||
const (
|
||||
colorReset = "\033[0m"
|
||||
colorRed = "\033[31m"
|
||||
colorGreen = "\033[32m"
|
||||
colorYellow = "\033[33m"
|
||||
colorBlue = "\033[34m"
|
||||
colorPurple = "\033[35m"
|
||||
colorCyan = "\033[36m"
|
||||
colorWhite = "\033[37m"
|
||||
colorBold = "\033[1m"
|
||||
)
|
||||
|
||||
// Функция для подсчета видимой длины строки (без ANSI кодов) в рунах
|
||||
visibleLength := func(s string) int {
|
||||
// Удаляем ANSI escape последовательности
|
||||
clean := s
|
||||
for strings.Contains(clean, "\033[") {
|
||||
start := strings.Index(clean, "\033[")
|
||||
end := strings.Index(clean[start:], "m")
|
||||
if end == -1 {
|
||||
break
|
||||
}
|
||||
clean = clean[:start] + clean[start+end+1:]
|
||||
}
|
||||
// Возвращаем количество рун, а не байт
|
||||
return len([]rune(clean))
|
||||
}
|
||||
|
||||
// Функция для умного разбиения строки
|
||||
splitLine := func(line string, maxWidth int) []string {
|
||||
runes := []rune(line)
|
||||
if len(runes) <= maxWidth {
|
||||
return []string{line}
|
||||
}
|
||||
|
||||
var result []string
|
||||
remaining := line
|
||||
|
||||
for len([]rune(remaining)) > maxWidth {
|
||||
// Ищем позицию для разрыва в пределах maxWidth
|
||||
breakPos := maxWidth
|
||||
remainingRunes := []rune(remaining)
|
||||
|
||||
for i := maxWidth; i >= 0; i-- {
|
||||
if i < len(remainingRunes) {
|
||||
char := remainingRunes[i]
|
||||
// Разрываем на пробеле, знаке пунктуации или в конце строки
|
||||
if char == ' ' || char == ',' || char == '.' || char == '!' ||
|
||||
char == '?' || char == ':' || char == ';' || char == '-' {
|
||||
breakPos = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Если не нашли подходящего места, разрываем по maxWidth
|
||||
if breakPos == maxWidth {
|
||||
breakPos = maxWidth
|
||||
}
|
||||
|
||||
// Создаем строку из рун
|
||||
breakString := string(remainingRunes[:breakPos])
|
||||
result = append(result, strings.TrimSpace(breakString))
|
||||
remaining = strings.TrimSpace(string(remainingRunes[breakPos:]))
|
||||
}
|
||||
|
||||
if len([]rune(remaining)) > 0 {
|
||||
result = append(result, remaining)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Разбиваем исходную шутку на строки
|
||||
originalLines := strings.Split(joke, "\n")
|
||||
|
||||
// Обрабатываем каждую строку и разбиваем длинные
|
||||
var processedLines []string
|
||||
for _, line := range originalLines {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
splitLines := splitLine(line, 80)
|
||||
processedLines = append(processedLines, splitLines...)
|
||||
}
|
||||
|
||||
// Находим максимальную длину строки для рамки (в рунах)
|
||||
maxLength := 0
|
||||
for _, line := range processedLines {
|
||||
lineLength := len([]rune(line))
|
||||
if lineLength > maxLength {
|
||||
maxLength = lineLength
|
||||
}
|
||||
}
|
||||
|
||||
// Убеждаемся, что maxLength не меньше минимальной ширины для заголовков
|
||||
minWidth := 60 // Минимальная ширина для заголовков
|
||||
if maxLength < minWidth {
|
||||
maxLength = minWidth
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("%s%s╭%s", colorPurple, colorBold, colorReset)
|
||||
fmt.Printf("%s%s", colorYellow, strings.Repeat("─", maxLength+2))
|
||||
fmt.Printf("%s%s╮%s\n", colorPurple, colorBold, colorReset)
|
||||
|
||||
headerText := " Зацени Анектотец! 🤣 "
|
||||
fmt.Printf("%s%s│%s", colorPurple, colorBold, colorReset)
|
||||
fmt.Printf("%s%s%s%s", colorCyan, colorBold, headerText, colorReset)
|
||||
fmt.Printf("%s%s", colorYellow, strings.Repeat(" ", 1+maxLength-visibleLength(headerText)))
|
||||
fmt.Printf("%s%s│%s\n", colorPurple, colorBold, colorReset)
|
||||
|
||||
fmt.Printf("%s%s├%s", colorPurple, colorBold, colorReset)
|
||||
fmt.Printf("%s%s", colorYellow, strings.Repeat("─", maxLength+2))
|
||||
fmt.Printf("%s%s┤%s\n", colorPurple, colorBold, colorReset)
|
||||
|
||||
// Выводим обработанные строки шутки
|
||||
for _, line := range processedLines {
|
||||
fmt.Printf("%s%s│%s", colorPurple, colorBold, colorReset)
|
||||
fmt.Printf("%s%s%s", colorWhite, line, colorReset)
|
||||
fmt.Printf("%s%s", colorYellow, strings.Repeat(" ", 2+maxLength-len([]rune(line))))
|
||||
fmt.Printf("%s%s│%s\n", colorPurple, colorBold, colorReset)
|
||||
}
|
||||
|
||||
fmt.Printf("%s%s├%s", colorPurple, colorBold, colorReset)
|
||||
fmt.Printf("%s%s", colorYellow, strings.Repeat("─", maxLength+2))
|
||||
fmt.Printf("%s%s┤%s\n", colorPurple, colorBold, colorReset)
|
||||
|
||||
// Вычисляем правильную ширину для нижних строк
|
||||
cmdText := "Попробуйте: ./port-knocker -t \"tcp:1.1.1.1:1111\""
|
||||
titleText := "🚀 Port Knocker - теперь с шутками! 🤣"
|
||||
|
||||
fmt.Printf("%s%s│%s", colorPurple, colorBold, colorReset)
|
||||
fmt.Printf("%s%s%s%s", colorGreen, colorBold, cmdText, colorReset)
|
||||
fmt.Printf("%s%s", colorYellow, strings.Repeat(" ", 2+maxLength-visibleLength(cmdText)))
|
||||
fmt.Printf("%s%s│%s\n", colorPurple, colorBold, colorReset)
|
||||
|
||||
fmt.Printf("%s%s│%s", colorPurple, colorBold, colorReset)
|
||||
fmt.Printf("%s%s%s%s", colorBlue, colorBold, titleText, colorReset)
|
||||
fmt.Printf("%s%s", colorYellow, strings.Repeat(" ", maxLength-visibleLength(titleText)))
|
||||
fmt.Printf("%s%s│%s\n", colorPurple, colorBold, colorReset)
|
||||
|
||||
fmt.Printf("%s%s╰%s", colorPurple, colorBold, colorReset)
|
||||
fmt.Printf("%s%s", colorYellow, strings.Repeat("─", maxLength+2))
|
||||
fmt.Printf("%s%s╯%s\n", colorPurple, colorBold, colorReset)
|
||||
fmt.Println()
|
||||
}
|
568
back/jokes.md
Normal file
568
back/jokes.md
Normal 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
21
back/main.go
Normal 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
204
back/scripts/quick-release.sh
Executable 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. Обновите документацию если нужно"
|
39
back/scripts/release-notes.md
Normal file
39
back/scripts/release-notes.md
Normal 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\"
|
||||
|
||||
```
|
Reference in New Issue
Block a user