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

16 KiB
Raw Blame History

Встраиваем веб-GUI в консольную утилиту: практический гайд

Содержание

Введение

Давайте по‑простому. Хотим к консольной утилите прикрутить небольшой веб‑интерфейс, чтобы не гонять команды вручную. Собрали один раз — и отдаем статику прямо из 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

Браузер попросит логин/пароль (BasicAuth). Логин любой (например 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 — удобно распространять единый бинарник.
  • Безопасность — простой BasicAuth. Переменная GO_KNOCKER_SERVE_PASS обязательна: без пароля сервер не стартует.

Под капотом статику обслуживает setupStaticRoutes, обратите внимание на SPAмаршрутизацию (фоллбэк на index.html):

// Если файл не найден и это маршрут SPA — показываем index.html

Минимальный GUI

Оставил только то, что реально нужно для «пнул порты». В main полный интерфейс ...

Ключевой шаблон компонента:

<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>

Логика отправки запроса:

this.http.post('/api/v1/knock-actions/execute', {
  targets: v.targets,
  delay: v.delay,
  waitConnection: v.waitConnection
}).subscribe(...)

ну да, ну да надо сервис и все такое ...

Так как страницы GUI защищены BasicAuth, браузер после ввода пароля сам будет добавлять заголовок 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.

SPA под произвольным префиксом (/ui/simple): base href и deploy-url

Иногда нужно отдавать SPA не с корня /, а, скажем, по пути /ui/simple. Для Angular это значит две вещи: правильный <base href> в index.html и корректные пути к ассетам.

Вариант A: собрать с заданным base-href и deploy-url:

# пример сборки под префикс /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 вручную после сборки (минимальный вариант):

<!-- 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/:

public/
├── ui/
│   └── simple/
│       ├── index.html      # с <base href="/ui/simple/">
│       ├── main-*.js
│       ├── styles-*.css
│       └── assets/...
└── ...

Проверка:

http://localhost:8888/ui/simple

Если при прямом заходе на вложенный роут /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