13 KiB
Встраиваем веб-GUI в консольную утилиту: практический гайд
id: 10
readTime: 15-20 минут
date: 2025-09-10 18:00
author: Direct-Dev (aka Антон Кузнецов)
level: Средний
tags: #go #angular #spa #embed #static #cli #webui #devops
version: 1.0.1
Содержание
Введение
Давайте по‑простому. Хотим к консольной утилите прикрутить небольшой веб‑интерфейс, чтобы не гонять команды вручную. Собрали один раз — и отдаем статику прямо из Go‑бинарника (или из рядом лежащей папки). В примере оставим только самое нужное:
- Targets (строка вида
tcp:host:port;udp:host:port...
) - Delay (например
1s
) - Флаг Wait connection
- Кнопка Execute
Если нужен «боевой» вариант — всегда можно нарастить поля и логику. Но начнем с минимума.
Как быстро повторить (клонируем, собираем, запускаем)
1 Клонируем репозиторий и переключаемся на ветку для статьи:
# HTTPS клон
git clone https://direct-dev.ru/gitea/GiteaAdmin/knock-gui.git
cd knock-gui
# Переход на ветку с упрощенным UI
git checkout for-article
Источник репозитория: https://direct-dev.ru/gitea/GiteaAdmin/knock-gui
2 Ставим зависимости фронта и собираем UI:
cd ui
# Установим зависимости проекта (node/npm должны быть установлены)
npm ci
# Собираем продовую сборку
./build-for-embeding.sh ../back/cmd/public
cd ..
# или через make
make embed-ui
3 Запускаем бэкенд (Go 1.21+):
# Обязательный пароль для Basic-Auth (GUI и API)
export GO_KNOCKER_SERVE_PASS=changeme
# Порт опционален (по умолчанию 8888)
export GO_KNOCKER_SERVE_PORT=8888
# Вариант A: запустить из исходников
cd back
go run ./
# или Вариант B: собрать бинарник и запустить
go build -o knocker-serve ./
./knocker-serve serve
# ну или так
make run PASS=superpass PORT=8888
4 Открываем в браузере:
http://localhost:8888
Браузер попросит логин/пароль (Basic‑Auth). Логин любой (например knocker
), пароль — тот, что в GO_KNOCKER_SERVE_PASS
. После этого UI загрузится, и та же авторизация «подхватится» для API‑вызовов.
5 Заполняем нужные таргеты: tcp:10.10.10.10:8080;udp:10.10.10.20:8888
6 Нажимаем Execute — и смотрим результат. Ошибка авторизации? Проверьте пароль в переменной окружения и перезагрузите страницу.
Структура проекта
Чтобы лучше ориентироваться, вот ключевые директории и файлы (сокращенно):
knock-gui/
├── ui/ # Angular SPA (минимальный GUI)
│ ├── src/app/knock/
│ │ ├── knock-page.component.ts
│ │ └── knock-page.component.html
│ ├── build-for-embeding.sh # Скрипт сборки и копирования артефактов в back
│ └── ...
├── back/ # Go backend (встроенная статика + API)
│ ├── cmd/
│ │ ├── serve.go # запуск сервера, basic-auth, CORS
│ │ ├── static_routes.go # раздача встроенной статики (SPA routing)
│ │ └── knock_routes.go # эндпойнты API
│ ├── internal/knocker.go # логика работы
│ └── main.go
└── article/ # материалы статьи
Идея и архитектура
- Фронтенд (Angular SPA) — статические файлы (
index.html
,*.js
,*.css
). - Бэкенд (Go + Gin) — раздаёт эти файлы и держит REST API. Для статики используем
embed.FS
— удобно распространять единый бинарник. - Безопасность — простой Basic‑Auth. Переменная
GO_KNOCKER_SERVE_PASS
обязательна: без пароля сервер не стартует.
Под капотом статику обслуживает setupStaticRoutes
, обратите внимание на SPA‑маршрутизацию (фоллбэк на index.html
):
// Если файл не найден и это маршрут SPA — показываем index.html
Минимальный GUI
Оставили только то, что реально нужно для «пнул порт — и поехали». Поля формы и кнопка запуска — никаких лишних переключателей.
Ключевой шаблон компонента:
<div class="container">
<p-card header="Port Knocker (Минимальный UI)">
<form [formGroup]="form" (ngSubmit)="execute()" class="p-fluid">
<div class="grid">
<div class="col-12">
<label>Targets</label>
<input pInputText type="text" formControlName="targets" placeholder="tcp:host:port;udp:host:port" class="w-full" />
</div>
<div class="col-12 md:col-6">
<label>Delay</label>
<input pInputText type="text" formControlName="delay" placeholder="1s" class="w-full" />
</div>
<div class="col-12 md:col-6 flex align-items-center gap-2">
<p-checkbox formControlName="waitConnection" [binary]="true"></p-checkbox>
<label class="checkbox-label">Wait connection</label>
</div>
<div class="col-12">
<button pButton type="submit" label="Execute" class="w-full" [loading]="executing" [disabled]="executing || form.invalid"></button>
</div>
</div>
</form>
</p-card>
</div>
Логика отправки запроса — проста как три рубля:
this.http.post('/api/v1/knock-actions/execute', {
targets: v.targets,
delay: v.delay,
waitConnection: v.waitConnection
}).subscribe(...)
Так как страницы GUI защищены Basic‑Auth, браузер после ввода пароля сам будет добавлять заголовок Authorization и к XHR‑запросам — отдельно в коде его прокидывать не нужно.
Сборка фронтенда
Собираем Angular‑приложение и копируем артефакты туда, откуда Go будет отдавать статику.
Скрипт сборки:
#!/bin/bash
# Использование: ./build-for-embeding.sh /abs/path/to/back/cmd/public
npx ng build --configuration production
mkdir -p "$DESTINATION_DIR"
cp -r /home/su/projects/angular/project-front/dist/project-front/browser/* "$DESTINATION_DIR"
- После
ng build
файлы лежат вui/dist/project-front/browser/
. - Мы копируем их в
back/cmd/public/
, откуда сервер раздаёт статику (а в релизе — всё упакуется вembed.FS
).
Встраивание в Go-сервис
Старт сервера и базовая авторизация — в serve.go
:
//go:embed public/*
var embeddedFS embed.FS
...
pass := os.Getenv("GO_KNOCKER_SERVE_PASS")
if strings.TrimSpace(pass) == "" {
return fmt.Errorf("GO_KNOCKER_SERVE_PASS не задан — задайте пароль для доступа к GUI/API")
}
...
setupStaticRoutes(r, embeddedFS)
То есть без пароля сервер не стартует — и это хорошо.
Альтернативно можно отдать файлы с диска, но embed.FS
удобнее: один бинарник — и поехали.
API: контракт и примеры
Endpoint: POST /api/v1/knock-actions/execute
Request JSON:
{
"targets": "tcp:127.0.0.1:22;udp:1.2.3.4:53",
"delay": "1s",
"waitConnection": false
}
Response 200:
{ "status": "ok" }
Проверка через curl (не забудьте базовую авторизацию):
curl -X POST http://localhost:8888/api/v1/knock-actions/execute \
-u knocker:changeme \
-H 'Content-Type: application/json' \
-d '{"targets":"tcp:127.0.0.1:22","delay":"1s","waitConnection":false}'
Запуск и проверка
1 Собрать фронт и скопировать файлы:
cd ui
./build-for-embeding.sh ../back/cmd/public
cd ..
2 Запустить бэкенд:
export GO_KNOCKER_SERVE_PASS=changeme
export GO_KNOCKER_SERVE_PORT=8888
cd back
go run ./
3 Открыть в браузере /
и проверить, что форма грузится, а Execute
бьёт в API:
http://localhost:8888
Если что-то не так — загляните в логи терминала и сетевую вкладку DevTools.
FAQ и типичные ошибки
- «Сервер ругается на пароль»: не задали
GO_KNOCKER_SERVE_PASS
или запустили в другой сессии. Экспортните переменную и перезапустите. - «GUI просит логин/пароль, а потом 401 на API»: проверьте правильность пароля, перезагрузите страницу. Браузер должен автоматом подставлять Authorization.
- «Статика не находится»: проверьте, что после сборки файлы UI скопированы в
back/cmd/public/
и сервер перезапущен. - «SPA роуты 404 при F5»:
static_routes.go
делает фоллбэк наindex.html
. Если меняли пути — не сломайте фоллбек. - «CORS в разработке»: мы разрешили
http://localhost:4200
и:8888
. Если запускаете UI отдельно, не забудьте CORS.
Если хочется глубже — можно прикрутить версионирование артефактов, CI/CD и полноценный embed всех файлов. Но для старта достаточно описанных шагов — клон, сборка, копирование, запуск. Увидел проблему — открывайте тикет в репозитории: https://direct-dev.ru/gitea/GiteaAdmin/knock-gui