added article md file to main
This commit is contained in:
345
article/embed-gui-guide.md
Normal file
345
article/embed-gui-guide.md
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
# Встраиваем веб-GUI в консольную утилиту: практический гайд
|
||||||
|
|
||||||
|
```metadata
|
||||||
|
id: 2
|
||||||
|
title: "Встраиваем веб-GUI в консольную утилиту: практический гайд"
|
||||||
|
readTime: 15-20 минут
|
||||||
|
date: 2025-09-10 18:00
|
||||||
|
author: Direct-Dev (Антон)
|
||||||
|
level: Средний
|
||||||
|
tags: #go #angular #spa #embed #static #cli #webui #devops
|
||||||
|
version: 1.0.2
|
||||||
|
```
|
||||||
|
|
||||||
|
## Содержание
|
||||||
|
|
||||||
|
- [Встраиваем веб-GUI в консольную утилиту: практический гайд](#встраиваем-веб-gui-в-консольную-утилиту-практический-гайд)
|
||||||
|
- [Содержание](#содержание)
|
||||||
|
- [Введение](#введение)
|
||||||
|
- [Клонируем, собираем, запускаем](#клонируем-собираем-запускаем)
|
||||||
|
- [Структура проекта](#структура-проекта)
|
||||||
|
- [Идея и архитектура](#идея-и-архитектура)
|
||||||
|
- [Минимальный GUI](#минимальный-gui)
|
||||||
|
- [Сборка фронтенда](#сборка-фронтенда)
|
||||||
|
- [Встраивание в Go-сервис](#встраивание-в-go-сервис)
|
||||||
|
- [API: контракт и примеры](#api-контракт-и-примеры)
|
||||||
|
- [Запуск и проверка](#запуск-и-проверка)
|
||||||
|
- [SPA под произвольным префиксом (/ui/simple): base href и deploy-url](#spa-под-произвольным-префиксом-uisimple-base-href-и-deploy-url)
|
||||||
|
- [FAQ и типичные ошибки](#faq-и-типичные-ошибки)
|
||||||
|
|
||||||
|
## Введение
|
||||||
|
|
||||||
|
Допустим есть желание к консольной Go утилите прикрутить небольшой веб‑интерфейс, чтобы не гонять команды вручную в терминале или расширить круг пользователей.
|
||||||
|
Как вариант добавляем api в утилиту, делаем SPA на ангуляре, например, дальше кидаем в проект api скомпилированное spa, cобрали бинарь и отдаем статику прямо из Go‑бинарника (или из рядом лежащей папки).
|
||||||
|
|
||||||
|
В качестве утилиты берем go-knocker - утилиту чтобы постучаться по портам ( такая штука повышающая безопасность серверов и устройств). Проект утилиты можно найти тут: <https://github.com/Direct-Dev-Ru/port-knocker.git>
|
||||||
|
|
||||||
|
в данном упрощенном интерфейсе используем только следующие поля и inline режим работы утилиты:
|
||||||
|
|
||||||
|
на форме будут следующие поля
|
||||||
|
|
||||||
|
- Targets (строка вида `tcp:host:port;udp:host:port...`)
|
||||||
|
- Delay (например `1s`)
|
||||||
|
- Флаг Wait connection
|
||||||
|
- Кнопка Execute
|
||||||
|
|
||||||
|
Если нужен более «продвинутый» вариант — всегда можно нарастить поля и логику.
|
||||||
|
|
||||||
|
## Клонируем, собираем, запускаем
|
||||||
|
|
||||||
|
Источник репозитория: [`https://direct-dev.ru/gitea/GiteaAdmin/knock-gui`](https://direct-dev.ru/gitea/GiteaAdmin/knock-gui)
|
||||||
|
|
||||||
|
1 Клонируем репозиторий и переключаемся на ветку for-article:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# HTTPS клон
|
||||||
|
git clone https://direct-dev.ru/gitea/GiteaAdmin/knock-gui.git
|
||||||
|
cd knock-gui
|
||||||
|
|
||||||
|
# Переход на ветку с упрощенным UI
|
||||||
|
git checkout for-article
|
||||||
|
```
|
||||||
|
|
||||||
|
2 переходим в папку фронта, ставим зависимости и собираем UI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ui
|
||||||
|
# Установим зависимости проекта (node/npm должны быть установлены)
|
||||||
|
npm ci
|
||||||
|
# Собираем продовую сборку
|
||||||
|
./build-for-embeding.sh ../back/cmd/public
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# или через make
|
||||||
|
cd ..
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
Браузер попросит логин/пароль (Basic‑Auth). Логин любой (например `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` — удобно распространять единый бинарник.
|
||||||
|
- **Безопасность** — простой Basic‑Auth. Переменная `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
|
||||||
|
|
||||||
|
Оставил только то, что реально нужно для «пнул порты». В main полный интерфейс ...
|
||||||
|
|
||||||
|
Ключевой шаблон компонента:
|
||||||
|
|
||||||
|
```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 (Minimal 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 защищены Basic‑Auth, браузер после ввода пароля сам будет добавлять заголовок 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.
|
||||||
|
|
||||||
|
## SPA под произвольным префиксом (/ui/simple): base href и deploy-url
|
||||||
|
|
||||||
|
Иногда нужно отдавать SPA не с корня `/`, а, скажем, по пути `/ui/simple`. Для Angular это значит две вещи: правильный `<base href>` в `index.html` и корректные пути к ассетам.
|
||||||
|
|
||||||
|
Вариант A: собрать с заданным base-href и deploy-url:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# пример сборки под префикс /ui/simple
|
||||||
|
npx ng build --configuration production \
|
||||||
|
--base-href /ui/simple/ \
|
||||||
|
--deploy-url /ui/simple/
|
||||||
|
```
|
||||||
|
|
||||||
|
Что это даёт:
|
||||||
|
|
||||||
|
- В `dist/.../browser/index.html` будет `<base href="/ui/simple/">`.
|
||||||
|
- Все ссылки на бандлы/ассеты будут начинаться с `/ui/simple/`.
|
||||||
|
|
||||||
|
Вариант B: поправить `index.html` вручную после сборки (минимальный вариант):
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- ui/dist/project-front/browser/index.html -->
|
||||||
|
<base href="/ui/simple/">
|
||||||
|
```
|
||||||
|
|
||||||
|
Важно: закрывающий слеш обязателен, иначе роутинг может «ехать».
|
||||||
|
|
||||||
|
Настройка бэкенда:
|
||||||
|
|
||||||
|
- Отгружайте содержимое собранной папки по маршруту `/ui/simple` (или скопируйте артефакты в `back/cmd/public/ui/simple/`).
|
||||||
|
- SPA‑фоллбэк должен отдавать `index.html` при запросах внутри префикса, если это не файловые ресурсы.
|
||||||
|
|
||||||
|
Если у вас универсальный обработчик (как в `setupStaticRoutes`) на корне, два простых подхода:
|
||||||
|
|
||||||
|
- Хранить файлы в `public/ui/simple/...` — тогда запросы к `/ui/simple/...` будут отработаны корректно.
|
||||||
|
- Сделать отдельный хендлер, который для путей с префиксом `/ui/simple` читает файлы из подкаталога, а на несуществующие файлы отвечает содержимым `public/ui/simple/index.html`.
|
||||||
|
|
||||||
|
Пример маппинга структуры в `public/`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
public/
|
||||||
|
├── ui/
|
||||||
|
│ └── simple/
|
||||||
|
│ ├── index.html # с <base href="/ui/simple/">
|
||||||
|
│ ├── main-*.js
|
||||||
|
│ ├── styles-*.css
|
||||||
|
│ └── assets/...
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Если при прямом заходе на вложенный роут `/ui/simple/some/child` видите 404 — значит фоллбэк не отрабатывает. Проверьте, что при отсутствии файла по этому пути сервер возвращает `public/ui/simple/index.html`.
|
||||||
|
|
||||||
|
## 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)
|
Reference in New Issue
Block a user