238 lines
15 KiB
Go
238 lines
15 KiB
Go
package keymgr
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
"ssh-access-tui/internal/model"
|
||
)
|
||
|
||
func genTimestamp() string {
|
||
return time.Now().UTC().Format("2006-01-02 15:04 UTC")
|
||
}
|
||
|
||
// RenderInstructions produces a plain-Russian, no-jargon README for a
|
||
// non-technical Windows user whose only guaranteed tool is the OpenSSH
|
||
// client bundled with Windows 10/11. Deliberately says nothing about
|
||
// "rights", "forwarding", "jump hosts" etc — just: run this one command and
|
||
// keep it open, then these local ports lead to these services.
|
||
func RenderInstructions(username, keyFileName string, forwards []AllocatedForward) string {
|
||
var b strings.Builder
|
||
|
||
b.WriteString("КАК ПОДКЛЮЧИТЬСЯ (Windows)\n")
|
||
b.WriteString("===========================\n\n")
|
||
fmt.Fprintf(&b, "Ваш логин: %s\n\n", username)
|
||
|
||
b.WriteString("В этом архиве 4 файла:\n")
|
||
fmt.Fprintf(&b, " - %s — ваш секретный ключ (никому не передавайте и не пересылайте)\n", keyFileName)
|
||
fmt.Fprintf(&b, " - %s.pub — открытая часть ключа (можно не трогать)\n", keyFileName)
|
||
b.WriteString(" - config — настройка подключения\n")
|
||
b.WriteString(" - INSTRUCTIONS.txt — этот файл\n\n")
|
||
|
||
b.WriteString("ШАГ 1. Разложите файлы по нужной папке\n")
|
||
b.WriteString("---------------------------------------\n")
|
||
b.WriteString("Создайте (если её ещё нет) папку:\n\n")
|
||
b.WriteString(" C:\\Users\\ВАШЕ_ИМЯ\\.ssh\\\n\n")
|
||
fmt.Fprintf(&b, "И скопируйте туда все 3 файла из архива (%s, %s.pub, config).\n", keyFileName, keyFileName)
|
||
b.WriteString("Важно: у файла config не должно быть никакого расширения (не config.txt).\n\n")
|
||
|
||
b.WriteString("ШАГ 2. Настройте доступ к файлу ключа\n")
|
||
b.WriteString("----------------------------------------\n")
|
||
b.WriteString("Windows откажется использовать секретный ключ, если его может прочитать\n")
|
||
b.WriteString("кто-то ещё, кроме вас. Откройте PowerShell и выполните по очереди (это две\n")
|
||
b.WriteString("отдельные команды, копируйте и выполняйте одну за другой):\n\n")
|
||
fmt.Fprintf(&b, " icacls \"$env:USERPROFILE\\.ssh\\%s\" /inheritance:r\n", keyFileName)
|
||
fmt.Fprintf(&b, " icacls \"$env:USERPROFILE\\.ssh\\%s\" /grant:r \"${env:USERNAME}:(R)\"\n\n", keyFileName)
|
||
b.WriteString("Если файлы лежат не в стандартной папке .ssh, а где-то ещё — замените путь\n")
|
||
b.WriteString("в обеих командах на реальный путь к файлу ключа (в кавычках, как выше).\n\n")
|
||
|
||
b.WriteString("ШАГ 3. Подключитесь\n")
|
||
b.WriteString("---------------------\n")
|
||
b.WriteString("В PowerShell введите команду:\n\n")
|
||
b.WriteString(" ssh -N jump\n\n")
|
||
b.WriteString("Окно «зависнет» и ничего не будет происходить — это нормально, так и должно\n")
|
||
b.WriteString("быть. НЕ ЗАКРЫВАЙТЕ это окно — пока оно открыто, работают все сервисы из\n")
|
||
b.WriteString("таблицы ниже. Закрыли окно (или выключили компьютер) — доступ пропал, просто\n")
|
||
b.WriteString("откройте заново командой выше, когда снова понадобится.\n\n")
|
||
|
||
b.WriteString("ШАГ 4. Пользуйтесь\n")
|
||
b.WriteString("--------------------\n")
|
||
b.WriteString("Эта настройка открывает у вас на компьютере (локально) следующие адреса.\n")
|
||
b.WriteString("В программе, которая должна подключиться к сервису (браузер, клиент базы\n")
|
||
b.WriteString("данных и т.п.), укажите вместо настоящего адреса тот, что указан ниже как\n")
|
||
b.WriteString("«127.0.0.1:порт» — и она подключится к нужному сервису через это соединение.\n\n")
|
||
|
||
// A numbered list rather than an aligned table: this file is opened in
|
||
// Notepad with whatever font the user has, so column alignment based on
|
||
// character counts (especially mixing Cyrillic and digits) can't be
|
||
// relied on to line up — a simple list reads correctly regardless.
|
||
for i, f := range forwards {
|
||
what := f.ServiceName
|
||
if f.Description != "" {
|
||
what = fmt.Sprintf("%s (%s)", f.ServiceName, f.Description)
|
||
}
|
||
fmt.Fprintf(&b, " %d. 127.0.0.1:%d\n", i+1, f.LocalPort)
|
||
fmt.Fprintf(&b, " Это: %s\n", what)
|
||
fmt.Fprintf(&b, " Реально находится на: %s:%d\n\n", f.HostName, f.Port)
|
||
}
|
||
if len(forwards) == 0 {
|
||
b.WriteString(" (сервисы ещё не назначены — обратитесь к администратору)\n")
|
||
}
|
||
b.WriteString("\n")
|
||
|
||
b.WriteString("ЕСЛИ ЧТО-ТО НЕ РАБОТАЕТ\n")
|
||
b.WriteString("------------------------\n")
|
||
b.WriteString("Написано «This account is currently not available» и соединение сразу\n")
|
||
b.WriteString("обрывается? Это нормально, если вы вводили команду БЕЗ \"-N\" (например,\n")
|
||
b.WriteString("просто \"ssh jump\") — обычный вход сюда специально отключён, используйте\n")
|
||
b.WriteString("именно команду из шага 3 (\"ssh -N jump\").\n\n")
|
||
b.WriteString("Если не работает именно шаг 3 или шаг 4 — обратитесь к администратору,\n")
|
||
b.WriteString("который выдал вам этот архив. Возможно, изменился список сервисов — тогда\n")
|
||
b.WriteString("вам пришлют обновлённый архив.\n")
|
||
|
||
return b.String()
|
||
}
|
||
|
||
// RenderConfigUpdateInstructions is a short follow-up notice for a user who
|
||
// already completed the full setup once (RenderInstructions) and just needs
|
||
// to know their service list changed — their key did not change and needs
|
||
// no attention, only the config file is replaced.
|
||
func RenderConfigUpdateInstructions(username string, forwards []AllocatedForward) string {
|
||
var b strings.Builder
|
||
|
||
b.WriteString("ОБНОВЛЕНИЕ СПИСКА СЕРВИСОВ\n")
|
||
b.WriteString("===========================\n\n")
|
||
fmt.Fprintf(&b, "Ваш логин: %s\n\n", username)
|
||
b.WriteString("Ваш ключ НЕ менялся — трогать его не нужно, продолжайте пользоваться тем же.\n")
|
||
b.WriteString("Изменился только список сервисов, поэтому нужно заменить один файл.\n\n")
|
||
|
||
b.WriteString("Замените файл config (из этого архива) на:\n\n")
|
||
b.WriteString(" C:\\Users\\ВАШЕ_ИМЯ\\.ssh\\config\n\n")
|
||
b.WriteString("(тот же путь, что и раньше — просто перезапишите старый файл новым).\n\n")
|
||
|
||
b.WriteString("Дальше подключайтесь как обычно, командой:\n\n")
|
||
b.WriteString(" ssh -N jump\n\n")
|
||
|
||
b.WriteString("Актуальный список сервисов:\n\n")
|
||
for i, f := range forwards {
|
||
what := f.ServiceName
|
||
if f.Description != "" {
|
||
what = fmt.Sprintf("%s (%s)", f.ServiceName, f.Description)
|
||
}
|
||
fmt.Fprintf(&b, " %d. 127.0.0.1:%d\n", i+1, f.LocalPort)
|
||
fmt.Fprintf(&b, " Это: %s\n", what)
|
||
fmt.Fprintf(&b, " Реально находится на: %s:%d\n\n", f.HostName, f.Port)
|
||
}
|
||
if len(forwards) == 0 {
|
||
b.WriteString(" (сервисы не назначены — обратитесь к администратору)\n\n")
|
||
}
|
||
|
||
b.WriteString("Если что-то не работает — обратитесь к администратору.\n")
|
||
return b.String()
|
||
}
|
||
|
||
// RenderInstructionsProxyJump is the ProxyJump-mode counterpart of
|
||
// RenderInstructions: same audience and same 4-file archive, but each
|
||
// service gets its own "Host <alias>" entry (see
|
||
// RenderClientConfigProxyJump) reached with a plain "ssh <alias>" instead of
|
||
// one always-open tunnel exposing local ports — steps 3/4 are adapted
|
||
// accordingly.
|
||
func RenderInstructionsProxyJump(username, keyFileName string, forwards []model.Forward) string {
|
||
var b strings.Builder
|
||
|
||
b.WriteString("КАК ПОДКЛЮЧИТЬСЯ (Windows)\n")
|
||
b.WriteString("===========================\n\n")
|
||
fmt.Fprintf(&b, "Ваш логин: %s\n\n", username)
|
||
|
||
b.WriteString("В этом архиве 4 файла:\n")
|
||
fmt.Fprintf(&b, " - %s — ваш секретный ключ (никому не передавайте и не пересылайте)\n", keyFileName)
|
||
fmt.Fprintf(&b, " - %s.pub — открытая часть ключа (можно не трогать)\n", keyFileName)
|
||
b.WriteString(" - config — настройка подключения\n")
|
||
b.WriteString(" - INSTRUCTIONS.txt — этот файл\n\n")
|
||
|
||
b.WriteString("ШАГ 1. Разложите файлы по нужной папке\n")
|
||
b.WriteString("---------------------------------------\n")
|
||
b.WriteString("Создайте (если её ещё нет) папку:\n\n")
|
||
b.WriteString(" C:\\Users\\ВАШЕ_ИМЯ\\.ssh\\\n\n")
|
||
fmt.Fprintf(&b, "И скопируйте туда все 3 файла из архива (%s, %s.pub, config).\n", keyFileName, keyFileName)
|
||
b.WriteString("Важно: у файла config не должно быть никакого расширения (не config.txt).\n\n")
|
||
|
||
b.WriteString("ШАГ 2. Настройте доступ к файлу ключа\n")
|
||
b.WriteString("----------------------------------------\n")
|
||
b.WriteString("Windows откажется использовать секретный ключ, если его может прочитать\n")
|
||
b.WriteString("кто-то ещё, кроме вас. Откройте PowerShell и выполните по очереди (это две\n")
|
||
b.WriteString("отдельные команды, копируйте и выполняйте одну за другой):\n\n")
|
||
fmt.Fprintf(&b, " icacls \"$env:USERPROFILE\\.ssh\\%s\" /inheritance:r\n", keyFileName)
|
||
fmt.Fprintf(&b, " icacls \"$env:USERPROFILE\\.ssh\\%s\" /grant:r \"${env:USERNAME}:(R)\"\n\n", keyFileName)
|
||
b.WriteString("Если файлы лежат не в стандартной папке .ssh, а где-то ещё — замените путь\n")
|
||
b.WriteString("в обеих командах на реальный путь к файлу ключа (в кавычках, как выше).\n\n")
|
||
|
||
b.WriteString("ШАГ 3. Подключайтесь\n")
|
||
b.WriteString("---------------------\n")
|
||
b.WriteString("Никакое окно держать открытым не нужно. Когда понадобится один из сервисов\n")
|
||
b.WriteString("из списка ниже, просто выполните в PowerShell соответствующую ему команду\n")
|
||
b.WriteString("\"ssh <название>\" — она сама пройдёт через промежуточный узел, отдельно\n")
|
||
b.WriteString("ничего для этого включать не нужно.\n\n")
|
||
|
||
b.WriteString("ШАГ 4. Ваши сервисы\n")
|
||
b.WriteString("---------------------\n")
|
||
|
||
aliases := hostAliases(forwards)
|
||
for i, f := range forwards {
|
||
what := f.ServiceName
|
||
if f.Description != "" {
|
||
what = fmt.Sprintf("%s (%s)", f.ServiceName, f.Description)
|
||
}
|
||
fmt.Fprintf(&b, " %d. ssh %s\n", i+1, aliases[i])
|
||
fmt.Fprintf(&b, " Это: %s\n", what)
|
||
fmt.Fprintf(&b, " Реально находится на: %s:%d\n\n", f.HostName, f.Port)
|
||
}
|
||
if len(forwards) == 0 {
|
||
b.WriteString(" (сервисы ещё не назначены — обратитесь к администратору)\n")
|
||
}
|
||
b.WriteString("\n")
|
||
|
||
b.WriteString("ЕСЛИ ЧТО-ТО НЕ РАБОТАЕТ\n")
|
||
b.WriteString("------------------------\n")
|
||
b.WriteString("Если команда из шага 4 сразу обрывается или пишет ошибку — обратитесь к\n")
|
||
b.WriteString("администратору, который выдал вам этот архив. Возможно, изменился список\n")
|
||
b.WriteString("сервисов — тогда вам пришлют обновлённый архив.\n")
|
||
|
||
return b.String()
|
||
}
|
||
|
||
// RenderConfigUpdateInstructionsProxyJump is the ProxyJump-mode counterpart
|
||
// of RenderConfigUpdateInstructions.
|
||
func RenderConfigUpdateInstructionsProxyJump(username string, forwards []model.Forward) string {
|
||
var b strings.Builder
|
||
|
||
b.WriteString("ОБНОВЛЕНИЕ СПИСКА СЕРВИСОВ\n")
|
||
b.WriteString("===========================\n\n")
|
||
fmt.Fprintf(&b, "Ваш логин: %s\n\n", username)
|
||
b.WriteString("Ваш ключ НЕ менялся — трогать его не нужно, продолжайте пользоваться тем же.\n")
|
||
b.WriteString("Изменился только список сервисов, поэтому нужно заменить один файл.\n\n")
|
||
|
||
b.WriteString("Замените файл config (из этого архива) на:\n\n")
|
||
b.WriteString(" C:\\Users\\ВАШЕ_ИМЯ\\.ssh\\config\n\n")
|
||
b.WriteString("(тот же путь, что и раньше — просто перезапишите старый файл новым).\n\n")
|
||
|
||
b.WriteString("Актуальный список сервисов (подключайтесь напрямую соответствующей командой):\n\n")
|
||
aliases := hostAliases(forwards)
|
||
for i, f := range forwards {
|
||
what := f.ServiceName
|
||
if f.Description != "" {
|
||
what = fmt.Sprintf("%s (%s)", f.ServiceName, f.Description)
|
||
}
|
||
fmt.Fprintf(&b, " %d. ssh %s\n", i+1, aliases[i])
|
||
fmt.Fprintf(&b, " Это: %s\n", what)
|
||
fmt.Fprintf(&b, " Реально находится на: %s:%d\n\n", f.HostName, f.Port)
|
||
}
|
||
if len(forwards) == 0 {
|
||
b.WriteString(" (сервисы не назначены — обратитесь к администратору)\n\n")
|
||
}
|
||
|
||
b.WriteString("Если что-то не работает — обратитесь к администратору.\n")
|
||
return b.String()
|
||
}
|