article feature

This commit is contained in:
2025-09-10 16:34:23 +06:00
parent 539d9c492d
commit 7493ec95aa
11 changed files with 34 additions and 2036 deletions

View File

@@ -43,7 +43,7 @@ back-build: embed-ui back-deps
cd $(BACK_DIR) && go build -o knocker-serve . cd $(BACK_DIR) && go build -o knocker-serve .
run: back-build run: back-build
cd $(BACK_DIR) && GO_KNOCKER_SERVE_PASS=$(PASS) GO_KNOCKER_SERVE_PORT=$(PORT) ./knocker-serve serve cd $(BACK_DIR) && GO_KNOCKER_SERVE_PASS=$(PASS) GO_KNOCKER_SERVE_PORT=$(PORT) ./knocker-serve -v serve
run-bg: back-build run-bg: back-build
cd $(BACK_DIR) && nohup env GO_KNOCKER_SERVE_PASS=$(PASS) GO_KNOCKER_SERVE_PORT=$(PORT) ./knocker-serve serve > /tmp/knocker.log 2>&1 & echo $$! && sleep 1 && tail -n +1 /tmp/knocker.log | sed -n '1,60p' cd $(BACK_DIR) && nohup env GO_KNOCKER_SERVE_PASS=$(PASS) GO_KNOCKER_SERVE_PORT=$(PORT) ./knocker-serve serve > /tmp/knocker.log 2>&1 & echo $$! && sleep 1 && tail -n +1 /tmp/knocker.log | sed -n '1,60p'

View File

@@ -12,19 +12,22 @@ version: 1.0.0
## Содержание ## Содержание
1. [Введение](#введение) - [Встраиваем веб-GUI в консольную утилиту: практический гайд](#встраиваем-веб-gui-в-консольную-утилиту-практический-гайд)
2. [Идея и архитектура](#идея-и-архитектура) - [Содержание](#содержание)
3. [Минимальный GUI](#минимальный-gui) - [Введение](#введение)
4. [Сборка фронтенда](#сборка-фронтенда) - [Идея и архитектура](#идея-и-архитектура)
5. [Встраивание в Go-сервис](#встраивание-в-go-сервис) - [Минимальный GUI](#минимальный-gui)
6. [API: контракт и примеры](#api-контракт-и-примеры) - [Сборка фронтенда](#сборка-фронтенда)
7. [Запуск и проверка](#запуск-и-проверка) - [Встраивание в Go-сервис](#встраивание-в-go-сервис)
8. [FAQ и типичные ошибки](#faq-и-типичные-ошибки) - [API: контракт и примеры](#api-контракт-и-примеры)
- [Запуск и проверка](#запуск-и-проверка)
- [FAQ и типичные ошибки](#faq-и-типичные-ошибки)
## Введение ## Введение
Задача: добавить простой веб-интерфейс к консольной утилите, собрать его один раз и отдавать статические файлы прямо из бинарника или из каталога рядом. В качестве примера используем минимальный UI со следующими полями: Задача: добавить простой веб-интерфейс к консольной утилите, собрать его один раз и отдавать статические файлы прямо из бинарника или из каталога рядом. В качестве примера используем минимальный UI со следующими полями:
- Targets (строка вида `tcp:host:port;udp:host:port`)
- Targets (строка вида `tcp:host:port;udp:host:port...`)
- Delay (например `1s`) - Delay (например `1s`)
- Флаг Wait connection - Флаг Wait connection
- Кнопка Execute - Кнопка Execute
@@ -42,6 +45,7 @@ version: 1.0.0
Мы упростили компонент до минимума в ветке `for-article`. Основной экран — одна форма и кнопка запуска. Мы упростили компонент до минимума в ветке `for-article`. Основной экран — одна форма и кнопка запуска.
Ключевой шаблон компонента: Ключевой шаблон компонента:
```12:60:/home/su/projects/articles/embed-gui-article/ui/src/app/knock/knock-page.component.html ```12:60:/home/su/projects/articles/embed-gui-article/ui/src/app/knock/knock-page.component.html
<div class="container"> <div class="container">
<p-card header="Port Knocker (Минимальный UI)"> <p-card header="Port Knocker (Минимальный UI)">
@@ -69,6 +73,7 @@ version: 1.0.0
``` ```
Логика отправки запроса: Логика отправки запроса:
```1:40:/home/su/projects/articles/embed-gui-article/ui/src/app/knock/knock-page.component.ts ```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', { this.http.post('/api/v1/knock-actions/execute', {
targets: v.targets, targets: v.targets,
@@ -82,6 +87,7 @@ this.http.post('/api/v1/knock-actions/execute', {
Собираем Angular-приложение и копируем артефакты в каталог, из которого Go-сервис будет обслуживать статику. Собираем Angular-приложение и копируем артефакты в каталог, из которого Go-сервис будет обслуживать статику.
Скрипт сборки: Скрипт сборки:
```1:20:/home/su/projects/articles/embed-gui-article/ui/build-for-embeding.sh ```1:20:/home/su/projects/articles/embed-gui-article/ui/build-for-embeding.sh
#!/bin/bash #!/bin/bash
# Использование: ./build-for-embeding.sh /abs/path/to/back/cmd/public # Использование: ./build-for-embeding.sh /abs/path/to/back/cmd/public
@@ -96,12 +102,14 @@ cp -r /home/su/projects/angular/project-front/dist/project-front/browser/* "$DES
## Встраивание в Go-сервис ## Встраивание в Go-сервис
Вариант А: раздача файлов из папки (`back/cmd/public`). Вариант А: раздача файлов из папки (`back/cmd/public`).
- Положите файлы фронта в `back/cmd/public`. - Положите файлы фронта в `back/cmd/public`.
- Убедитесь, что роутер отдаёт `/` и `/*` из этой папки. - Убедитесь, что роутер отдаёт `/` и `/*` из этой папки.
Вариант Б: `embed.FS` в бинарник. Вариант Б: `embed.FS` в бинарник.
Пример (концептуально): Пример (концептуально):
```go ```go
package main package main
@@ -128,6 +136,7 @@ func main() {
Endpoint: `POST /api/v1/knock-actions/execute` Endpoint: `POST /api/v1/knock-actions/execute`
Request JSON: Request JSON:
```json ```json
{ {
"targets": "tcp:127.0.0.1:22;udp:1.2.3.4:53", "targets": "tcp:127.0.0.1:22;udp:1.2.3.4:53",
@@ -137,11 +146,13 @@ Request JSON:
``` ```
Response 200: Response 200:
```json ```json
{ "status": "ok" } { "status": "ok" }
``` ```
Пример curl: Пример curl:
```bash ```bash
curl -X POST http://localhost:8080/api/v1/knock-actions/execute \ curl -X POST http://localhost:8080/api/v1/knock-actions/execute \
-H 'Content-Type: application/json' \ -H 'Content-Type: application/json' \
@@ -150,17 +161,19 @@ curl -X POST http://localhost:8080/api/v1/knock-actions/execute \
## Запуск и проверка ## Запуск и проверка
1) Собрать фронт и скопировать файлы: 1 Собрать фронт и скопировать файлы:
```bash ```bash
cd /home/su/projects/articles/embed-gui-article/ui cd /home/su/projects/articles/embed-gui-article/ui
./build-for-embeding.sh /home/su/projects/articles/embed-gui-article/back/cmd/public ./build-for-embeding.sh /home/su/projects/articles/embed-gui-article/back/cmd/public
``` ```
2) Запустить бэкенд (варианты): 2 Запустить бэкенд (варианты):
- Через make/скрипт, если есть. - Через make/скрипт, если есть.
- Локально `go run ./back` (или соответствующая команда в вашем проекте). - Локально `go run ./back` (или соответствующая команда в вашем проекте).
3) Открыть в браузере `/` и проверить, что форма грузится, а `Execute` бьёт в API. 3 Открыть в браузере `/` и проверить, что форма грузится, а `Execute` бьёт в API.
## FAQ и типичные ошибки ## FAQ и типичные ошибки

File diff suppressed because it is too large Load Diff

View File

@@ -71,7 +71,7 @@ func setupKnockRoutes(api *gin.RouterGroup) {
} }
} }
if err := knocker.ExecuteWithConfig(&config, req.Verbose, req.WaitConnection); err != nil { if err := knocker.ExecuteWithConfig(&config, true || req.Verbose, req.WaitConnection); err != nil {
c.JSON(400, gin.H{"error": err.Error()}) c.JSON(400, gin.H{"error": err.Error()})
return return
} }

View File

@@ -7,7 +7,7 @@ import (
"port-knocker/cmd" "port-knocker/cmd"
) )
// Version и BuildTime устанавливаются при сборке через ldflags
var ( var (
Version = "v1.0.10" Version = "v1.0.10"
BuildTime = "unknown" BuildTime = "unknown"

View File

@@ -112,6 +112,7 @@ git push origin main
# Сборка бинарников # Сборка бинарников
log_info "Собираем бинарники для всех платформ..." log_info "Собираем бинарники для всех платформ..."
export VERSION_NUM="${VERSION#v}" export VERSION_NUM="${VERSION#v}"
# shellcheck disable=SC2155
export BUILD_TIME=$(date -u '+%Y-%m-%d_%H:%M:%S') export BUILD_TIME=$(date -u '+%Y-%m-%d_%H:%M:%S')
# Функция сборки для платформы # Функция сборки для платформы
@@ -155,6 +156,7 @@ log_info "Создаем Git тег..."
# Читаем release-notes.md и сохраняем содержимое в переменную NOTES # Читаем release-notes.md и сохраняем содержимое в переменную NOTES
NOTES=$(cat docs/scripts/release-notes.md) NOTES=$(cat docs/scripts/release-notes.md)
# Заменяем все переменные вида $VERSION в NOTES на их значения # Заменяем все переменные вида $VERSION в NOTES на их значения
# shellcheck disable=SC2001
NOTES=$(echo "$NOTES" | sed "s/\\\$VERSION/$VERSION/g") NOTES=$(echo "$NOTES" | sed "s/\\\$VERSION/$VERSION/g")
git tag -a "$VERSION" -m "$NOTES" git tag -a "$VERSION" -m "$NOTES"

Binary file not shown.

View File

@@ -1,9 +1,8 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { BasicKnockPageComponent } from './basic-knock/basic-knock-page.component'; import { BasicKnockPageComponent } from './basic-knock/basic-knock-page.component';
import { FsaKnockPageComponent } from './fsa-knock/fsa-knock-page.component';
export const routes: Routes = [ export const routes: Routes = [
{ path: '', component: BasicKnockPageComponent }, { path: '', component: BasicKnockPageComponent },
{ path: 'fsa', component: FsaKnockPageComponent },
{ path: '**', redirectTo: '' } { path: '**', redirectTo: '' }
]; ];

View File

@@ -15,7 +15,7 @@ import { KnockPageComponent } from '../knock/knock-page.component';
template: ` template: `
<div class="container"> <div class="container">
<!-- Встраиваем основной компонент в базовом режиме --> <!-- Встраиваем основной компонент в базовом режиме -->
<app-knock-page [enableFSA]="false" [canUseFSA]="canUseFSA"></app-knock-page> <app-knock-page></app-knock-page>
</div> </div>
<!-- Информационное модальное окно --> <!-- Информационное модальное окно -->

View File

@@ -1,132 +0,0 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { CardModule } from 'primeng/card';
import { ButtonModule } from 'primeng/button';
import { DialogModule } from 'primeng/dialog';
import { KnockPageComponent } from '../knock/knock-page.component';
@Component({
selector: 'app-fsa-knock-page',
standalone: true,
imports: [
CommonModule, RouterModule, CardModule, ButtonModule, DialogModule, KnockPageComponent
],
template: `
<div class="container">
<div *ngIf="!isFSASupported" class="text-center">
<h3>File System Access API не поддерживается</h3>
<p>Эта функциональность требует браузер с поддержкой File System Access API:</p>
<ul class="text-left mt-3">
<li>Google Chrome 86+</li>
<li>Microsoft Edge 86+</li>
<li>Opera 72+</li>
</ul>
<p class="mt-3">Ваш браузер: <strong>{{ browserInfo }}</strong></p>
<button pButton
type="button"
label="Перейти к основной версии"
class="p-button-outlined mt-3"
routerLink="/">
</button>
</div>
<div *ngIf="isFSASupported">
<!-- Встраиваем основной компонент с поддержкой FSA -->
<app-knock-page [enableFSA]="true" [canUseFSA]="true"></app-knock-page>
</div>
</div>
<!-- Информационное модальное окно -->
<p-dialog header="🚀 Расширенная версия с File System Access"
[(visible)]="showInfoDialog"
[modal]="true"
[closable]="true"
[draggable]="false"
[resizable]="false"
styleClass="info-dialog">
<div class="dialog-content">
<p class="mb-3">
Эта версия поддерживает прямое редактирование файлов на диске.
Файлы будут автоматически перезаписываться после шифрования/дешифрования.
</p>
<div class="p-3 bg-green-50 border-round">
<p class="text-sm mb-2">
✅ <strong>Доступные возможности:</strong>
</p>
<ul class="text-sm mb-0">
<li>Прямое открытие файлов с диска</li>
<li>Автоматическое сохранение изменений</li>
<li>Перезапись зашифрованных файлов "на месте"</li>
<li>Быстрая работа без диалогов загрузки/скачивания</li>
</ul>
</div>
</div>
</p-dialog>
`,
styles: [`
.container {
max-width: 1200px;
margin: 0 auto;
padding: 1rem;
}
ul {
display: inline-block;
text-align: left;
}
.info-link {
color: #3b82f6;
cursor: pointer;
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease;
}
.info-link:hover {
color: #1d4ed8;
text-decoration: underline;
}
.bg-green-50 {
background-color: #f0fdf4;
}
.dialog-content {
min-width: 450px;
}
`]
})
export class FsaKnockPageComponent {
isFSASupported = false;
browserInfo = '';
showInfoDialog = false;
constructor() {
this.checkFSASupport();
this.getBrowserInfo();
}
private checkFSASupport() {
const w = window as any;
this.isFSASupported = typeof w.showOpenFilePicker === 'function';
}
private getBrowserInfo() {
const ua = navigator.userAgent;
if (ua.includes('Chrome') && !ua.includes('Edg/')) {
this.browserInfo = 'Google Chrome';
} else if (ua.includes('Edg/')) {
this.browserInfo = 'Microsoft Edge';
} else if (ua.includes('Opera') || ua.includes('OPR/')) {
this.browserInfo = 'Opera';
} else if (ua.includes('Firefox')) {
this.browserInfo = 'Mozilla Firefox';
} else if (ua.includes('Safari') && !ua.includes('Chrome')) {
this.browserInfo = 'Safari';
} else {
this.browserInfo = 'Неизвестный браузер';
}
}
}

View File

@@ -1,5 +1,5 @@
<div class="container"> <div class="container">
<p-card header="Port Knocker (Минимальный UI)"> <p-card header="Port Knocker (Minimal UI)">
<form [formGroup]="form" (ngSubmit)="execute()" class="p-fluid"> <form [formGroup]="form" (ngSubmit)="execute()" class="p-fluid">
<div class="grid"> <div class="grid">
<div class="col-12"> <div class="col-12">
@@ -8,7 +8,7 @@
pInputText pInputText
type="text" type="text"
formControlName="targets" formControlName="targets"
placeholder="tcp:host:port;udp:host:port" placeholder="tcp:host:port;udp:host:port;...;tcp:host:port"
class="w-full" class="w-full"
/> />
</div> </div>