sshkeeper/internal/model/server.go

237 lines
6.5 KiB
Go

package model
import (
"fmt"
"strings"
"time"
)
type AuthMethod string
const (
AuthPassword AuthMethod = "password"
AuthKey AuthMethod = "key"
AuthKeyPassphrase AuthMethod = "key_passphrase"
AuthAgent AuthMethod = "agent"
)
type TestStatus string
const (
TestUnknown TestStatus = "unknown"
TestOK TestStatus = "ok"
TestFailed TestStatus = "failed"
)
type Server struct {
ID int64 `json:"id"`
Alias string `json:"alias"`
DisplayName string `json:"display_name"`
Host string `json:"host"`
Port int `json:"port"`
User string `json:"user"`
AuthMethod AuthMethod `json:"auth_method"`
IdentityFile string `json:"identity_file"`
ProxyJump string `json:"proxy_jump"`
Route Route `json:"route"`
GroupName string `json:"group_name"`
Notes string `json:"notes"`
StartupCommand string `json:"startup_command"`
Tags []string `json:"tags"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LastConnectedAt *time.Time `json:"last_connected_at"`
LastTestAt *time.Time `json:"last_test_at"`
LastTestStatus TestStatus `json:"last_test_status"`
LastTestError string `json:"last_test_error"`
}
type SecretType string
const (
SecretSSHPassword SecretType = "ssh_password"
SecretKeyPassphrase SecretType = "key_passphrase"
SecretSudoPassword SecretType = "sudo_password"
SecretCustom SecretType = "custom_secret"
)
type Secret struct {
ID string `json:"id"`
Type SecretType `json:"type"`
Nonce []byte `json:"nonce"`
Data []byte `json:"data"`
}
type ForwardType string
const (
ForwardLocal ForwardType = "local"
ForwardRemote ForwardType = "remote"
ForwardDynamic ForwardType = "dynamic"
)
type Forward struct {
ID int64 `json:"id"`
ServerID int64 `json:"server_id"`
Name string `json:"name"`
Description string `json:"description"`
Type ForwardType `json:"type"`
LocalAddr string `json:"local_addr"`
LocalPort int `json:"local_port"`
RemoteAddr string `json:"remote_addr"`
RemotePort int `json:"remote_port"`
Enabled bool `json:"enabled"`
}
// ForwardHumanExplanation returns a human-readable explanation of the forward.
func (f *Forward) ForwardHumanExplanation(serverAlias string) string {
switch f.Type {
case ForwardLocal:
return fmt.Sprintf("Port %s:%d on this machine will be forwarded through %s to %s:%d.",
f.LocalAddr, f.LocalPort, serverAlias, f.RemoteAddr, f.RemotePort)
case ForwardRemote:
return fmt.Sprintf("Port %s:%d on %s will be forwarded to %s:%d on this machine.",
f.RemoteAddr, f.RemotePort, serverAlias, f.LocalAddr, f.LocalPort)
case ForwardDynamic:
return fmt.Sprintf("SOCKS proxy on %s:%d will route traffic through %s.",
f.LocalAddr, f.LocalPort, serverAlias)
default:
return fmt.Sprintf("Forward %s: %s:%d → %s:%d", f.Type, f.LocalAddr, f.LocalPort, f.RemoteAddr, f.RemotePort)
}
}
// ForwardSSHArgs returns the OpenSSH arguments for this forward.
func (f *Forward) ForwardSSHArgs() []string {
switch f.Type {
case ForwardLocal:
return []string{"-L", fmt.Sprintf("%s:%d:%s:%d", f.LocalAddr, f.LocalPort, f.RemoteAddr, f.RemotePort)}
case ForwardRemote:
return []string{"-R", fmt.Sprintf("%s:%d:%s:%d", f.RemoteAddr, f.RemotePort, f.LocalAddr, f.LocalPort)}
case ForwardDynamic:
return []string{"-D", fmt.Sprintf("%s:%d", f.LocalAddr, f.LocalPort)}
default:
return nil
}
}
// ForwardListen returns the listen address:port string.
func (f *Forward) ForwardListen() string {
switch f.Type {
case ForwardLocal, ForwardDynamic:
return fmt.Sprintf("%s:%d", f.LocalAddr, f.LocalPort)
case ForwardRemote:
return fmt.Sprintf("%s:%d", f.RemoteAddr, f.RemotePort)
default:
return ""
}
}
// ForwardTarget returns the target address:port string.
func (f *Forward) ForwardTarget() string {
switch f.Type {
case ForwardLocal:
return fmt.Sprintf("%s:%d", f.RemoteAddr, f.RemotePort)
case ForwardRemote:
return fmt.Sprintf("%s:%d", f.LocalAddr, f.LocalPort)
case ForwardDynamic:
return "SOCKS"
default:
return ""
}
}
type Tag struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
// --- Route ---
// RouteHop represents a single jump host in a route.
// IsProfile: true = use Alias (references a sshkeeper profile), false = use Raw (literal address).
type RouteHop struct {
Alias string `json:"alias"`
Raw string `json:"raw"`
IsProfile bool `json:"is_profile"`
}
// Route represents the SSH jump route for a server.
// Mode is computed from Hops length: 0=direct, 1=via, 2+=chain
type Route struct {
Hops []RouteHop `json:"hops"`
}
// RouteMode returns the computed route mode.
func (r Route) RouteMode() string {
switch len(r.Hops) {
case 0:
return "direct"
case 1:
return "via"
default:
return "chain"
}
}
// ProxyJumpString builds the -J argument value from hops.
func (r Route) ProxyJumpString() string {
parts := make([]string, len(r.Hops))
for i, h := range r.Hops {
if h.IsProfile {
parts[i] = h.Alias
} else {
parts[i] = h.Raw
}
}
return strings.Join(parts, ",")
}
// DisplaySummary returns a human-readable route summary.
// direct → target / bastion → target / bastion → dmz-gw → target
func (r Route) DisplaySummary(target string) string {
if len(r.Hops) == 0 {
return "direct → " + target
}
names := make([]string, len(r.Hops))
for i, h := range r.Hops {
if h.IsProfile {
names[i] = h.Alias
} else {
names[i] = h.Raw
}
}
return strings.Join(names, " → ") + " → " + target
}
// HasProfileLinks returns true if any hop references a known profile.
func (r Route) HasProfileLinks() bool {
for _, h := range r.Hops {
if h.IsProfile {
return true
}
}
return false
}
type CommandTemplate struct {
ID int64 `json:"id"`
ServerID int64 `json:"server_id"`
Name string `json:"name"`
Command string `json:"command"`
Description string `json:"description"`
}
// --- Tunnel ---
// TunnelState represents a running or stopped tunnel process.
type TunnelState struct {
ID int64 `json:"id"`
ServerID int64 `json:"server_id"`
ServerAlias string `json:"server_alias"`
Name string `json:"name"`
PID int `json:"pid"`
ForwardIDs []int64 `json:"forward_ids"`
StartedAt time.Time `json:"started_at"`
LastError string `json:"last_error"`
}