Files
knock-gui/article/embed-gui-guide.md

282 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Встраиваем веб-GUI в консольную утилиту: практический гайд
```metadata
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
```
## Содержание
- [Встраиваем веб-GUI в консольную утилиту: практический гайд](#встраиваем-веб-gui-в-консольную-утилиту-практический-гайд)
- [Содержание](#содержание)
- [Введение](#введение)
- [Как быстро повторить (клонируем, собираем, запускаем)](#как-быстро-повторить-клонируем-собираем-запускаем)
- [Структура проекта](#структура-проекта)
- [Идея и архитектура](#идея-и-архитектура)
- [Минимальный GUI](#минимальный-gui)
- [Сборка фронтенда](#сборка-фронтенда)
- [Встраивание в Go-сервис](#встраивание-в-go-сервис)
- [API: контракт и примеры](#api-контракт-и-примеры)
- [Запуск и проверка](#запуск-и-проверка)
- [FAQ и типичные ошибки](#faq-и-типичные-ошибки)
## Введение
Давайте по‑простому. Хотим к консольной утилите прикрутить небольшой веб‑интерфейс, чтобы не гонять команды вручную. Собрали один раз — и отдаем статику прямо из Goбинарника (или из рядом лежащей папки). В примере оставим только самое нужное:
- Targets (строка вида `tcp:host:port;udp:host:port...`)
- Delay (например `1s`)
- Флаг Wait connection
- Кнопка Execute
Если нужен «боевой» вариант — всегда можно нарастить поля и логику. Но начнем с минимума.
## Как быстро повторить (клонируем, собираем, запускаем)
1 Клонируем репозиторий и переключаемся на ветку для статьи:
```bash
# 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`](https://direct-dev.ru/gitea/GiteaAdmin/knock-gui)
2 Ставим зависимости фронта и собираем UI:
```bash
cd ui
# Установим зависимости проекта (node/npm должны быть установлены)
npm ci
# Собираем продовую сборку
./build-for-embeding.sh ../back/cmd/public
cd ..
# или через make
make embed-ui
```
3 Запускаем бэкенд (Go 1.21+):
```bash
# Обязательный пароль для 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 Открываем в браузере:
```text
http://localhost:8888
```
Браузер попросит логин/пароль (BasicAuth). Логин любой (например `knocker`), пароль — тот, что в `GO_KNOCKER_SERVE_PASS`. После этого UI загрузится, и та же авторизация «подхватится» для APIвызовов.
5 Заполняем нужные таргеты: tcp:10.10.10.10:8080;udp:10.10.10.20:8888
6 Нажимаем Execute — и смотрим результат. Ошибка авторизации? Проверьте пароль в переменной окружения и перезагрузите страницу.
## Структура проекта
Чтобы лучше ориентироваться, вот ключевые директории и файлы (сокращенно):
```text
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` — удобно распространять единый бинарник.
- **Безопасность** — простой BasicAuth. Переменная `GO_KNOCKER_SERVE_PASS` обязательна: без пароля сервер не стартует.
Под капотом статику обслуживает `setupStaticRoutes`, обратите внимание на SPAмаршрутизацию (фоллбэк на `index.html`):
```1:30:/home/su/projects/articles/embed-gui-article/back/cmd/static_routes.go
// Если файл не найден и это маршрут SPA — показываем index.html
```
## Минимальный GUI
Оставили только то, что реально нужно для «пнул порт — и поехали». Поля формы и кнопка запуска — никаких лишних переключателей.
Ключевой шаблон компонента:
```12:60:/home/su/projects/articles/embed-gui-article/ui/src/app/knock/knock-page.component.html
<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>
```
Логика отправки запроса — проста как три рубля:
```1:40:/home/su/projects/articles/embed-gui-article/ui/src/app/knock/knock-page.component.ts
this.http.post('/api/v1/knock-actions/execute', {
targets: v.targets,
delay: v.delay,
waitConnection: v.waitConnection
}).subscribe(...)
```
Так как страницы GUI защищены BasicAuth, браузер после ввода пароля сам будет добавлять заголовок Authorization и к XHRзапросам — отдельно в коде его прокидывать не нужно.
## Сборка фронтенда
Собираем Angularприложение и копируем артефакты туда, откуда Go будет отдавать статику.
Скрипт сборки:
```1:20:/home/su/projects/articles/embed-gui-article/ui/build-for-embeding.sh
#!/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`:
```1:26:/home/su/projects/articles/embed-gui-article/back/cmd/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:
```json
{
"targets": "tcp:127.0.0.1:22;udp:1.2.3.4:53",
"delay": "1s",
"waitConnection": false
}
```
Response 200:
```json
{ "status": "ok" }
```
Проверка через curl (не забудьте базовую авторизацию):
```bash
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 Собрать фронт и скопировать файлы:
```bash
cd ui
./build-for-embeding.sh ../back/cmd/public
cd ..
```
2 Запустить бэкенд:
```bash
export GO_KNOCKER_SERVE_PASS=changeme
export GO_KNOCKER_SERVE_PORT=8888
cd back
go run ./
```
3 Открыть в браузере `/` и проверить, что форма грузится, а `Execute` бьёт в API:
```text
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`](https://direct-dev.ru/gitea/GiteaAdmin/knock-gui)