Skip to content

Commit b41eeec

Browse files
authored
feat(config): add automatic backup before modifying config file (#7)
1 parent 6a96397 commit b41eeec

File tree

7 files changed

+105
-50
lines changed

7 files changed

+105
-50
lines changed

.goreleaser.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ builds:
1717
- 386
1818
main: ./cmd/main.go
1919
ldflags:
20-
-X main.version={{.Version}} -X main.gitCommit={{.Commit}} -X main.buildTime={{.Date}}
20+
-X main.version={{.Version}} -X main.gitCommit={{.Commit}}
2121

2222

2323

README.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
Lazyssh is a terminal-based, interactive SSH manager inspired by tools like lazydocker and k9s — but built for managing your fleet of servers directly from your terminal.
88
<br/>
9-
With lazyssh, you can quickly navigate, connect, manage, and transfer files between your local machine and any server defined in your ~/.ssh/config. No more remembering IP addresses or running long scp commands — just a clean, keyboard-driven UI.
9+
With lazyssh, you can quickly navigate, connect, manage, and transfer files between your local machine and any server defined in your `~/.ssh/config`. No more remembering IP addresses or running long scp commands — just a clean, keyboard-driven UI.
1010

1111
---
1212

@@ -31,12 +31,28 @@ With lazyssh, you can quickly navigate, connect, manage, and transfer files betw
3131
- 📁 Copy files between local and servers with an easy picker UI.
3232
- 📡 Port forwarding (local↔remote) from the UI.
3333
- 🔑 Enhanced Key Management:
34-
- Use default local public key (~/.ssh/id_ed25519.pub or ~/.ssh/id_rsa.pub)
34+
- Use default local public key (`~/.ssh/id_ed25519.pub` or `~/.ssh/id_rsa.pub`)
3535
- Paste custom public keys manually
3636
- Generate new keypairs and deploy them
37-
- Automatically append keys to ~/.ssh/authorized_keys with correct permissions
37+
- Automatically append keys to `~/.ssh/authorized_keys` with correct permissions
3838
---
3939

40+
## 🔐 Security Notice
41+
42+
lazyssh does not introduce any new security risks.
43+
It is simply a UI/TUI wrapper around your existing `~/.ssh/config` file.
44+
45+
- All SSH connections are executed through your system’s native ssh binary (OpenSSH).
46+
47+
- Private keys, passwords, and credentials are never stored, transmitted, or modified by lazyssh.
48+
49+
- Your existing IdentityFile paths and ssh-agent integrations work exactly as before.
50+
51+
- lazyssh only reads and updates your `~/.ssh/config`. A backup of the file is created automatically before any changes.
52+
53+
- File permissions on your SSH config are preserved to ensure security.
54+
55+
4056
## 📷 Screenshots
4157

4258
<div align="center">

cmd/main.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import (
1818
"fmt"
1919
"os"
2020
"path/filepath"
21-
"time"
2221

2322
"github.com/Adembc/lazyssh/internal/adapters/data/file"
2423
"github.com/Adembc/lazyssh/internal/logger"
@@ -31,7 +30,6 @@ import (
3130
var (
3231
version = "develop"
3332
gitCommit = "unknown"
34-
buildTime = time.Now().Format("2006-01-02 15:04:05")
3533
)
3634

3735
func main() {
@@ -55,7 +53,7 @@ func main() {
5553

5654
serverRepo := file.NewServerRepo(log, sshConfigFile, metaDataFile)
5755
serverService := services.NewServerService(log, serverRepo)
58-
tui := ui.NewTUI(log, serverService, version, gitCommit, buildTime)
56+
tui := ui.NewTUI(log, serverService, version, gitCommit)
5957

6058
rootCmd := &cobra.Command{
6159
Use: ui.AppName,

internal/adapters/data/file/ssh_config_manager.go

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package file
1616

1717
import (
1818
"fmt"
19+
"io"
1920
"os"
2021
"path/filepath"
2122

@@ -56,16 +57,41 @@ func (m *sshConfigManager) writeServers(servers []domain.Server) error {
5657
return err
5758
}
5859

59-
file, err := os.Create(m.filePath)
60+
if err := m.backupCurrentConfig(); err != nil {
61+
return err
62+
}
63+
64+
dir := filepath.Dir(m.filePath)
65+
tmp, err := os.CreateTemp(dir, ".lazyssh-tmp-*")
6066
if err != nil {
6167
return err
6268
}
63-
defer func() {
64-
_ = file.Close()
65-
}()
69+
defer func() { _ = os.Remove(tmp.Name()) }()
70+
71+
if err := os.Chmod(tmp.Name(), 0o600); err != nil {
72+
_ = tmp.Close()
73+
return err
74+
}
6675

6776
writer := &SSHConfigWriter{}
68-
return writer.Write(file, servers)
77+
if err := writer.Write(tmp, servers); err != nil {
78+
_ = tmp.Close()
79+
return err
80+
}
81+
82+
if err := tmp.Sync(); err != nil {
83+
_ = tmp.Close()
84+
return err
85+
}
86+
if err := tmp.Close(); err != nil { // close after sync to ensure contents are persisted
87+
return err
88+
}
89+
90+
if err := os.Rename(tmp.Name(), m.filePath); err != nil {
91+
return err
92+
}
93+
94+
return nil
6995
}
7096

7197
func (m *sshConfigManager) addServer(server domain.Server) error {
@@ -135,3 +161,49 @@ func (m *sshConfigManager) ensureDirectory() error {
135161
dir := filepath.Dir(m.filePath)
136162
return os.MkdirAll(dir, 0o700)
137163
}
164+
165+
// backupCurrentConfig creates ~/.lazyssh/backups/config.backup with 0600 perms,
166+
// overwriting it each time, but only if the source config exists.
167+
func (m *sshConfigManager) backupCurrentConfig() error {
168+
// If source config does not exist, skip backup
169+
if _, err := os.Stat(m.filePath); err != nil {
170+
if os.IsNotExist(err) {
171+
return nil
172+
}
173+
return err
174+
}
175+
176+
home, err := os.UserHomeDir()
177+
if err != nil {
178+
return err
179+
}
180+
backupDir := filepath.Join(home, ".lazyssh", "backups")
181+
// Ensure directory with 0700
182+
if err := os.MkdirAll(backupDir, 0o700); err != nil {
183+
return err
184+
}
185+
backupPath := filepath.Join(backupDir, "config.backup")
186+
// Copy file contents
187+
src, err := os.Open(m.filePath)
188+
if err != nil {
189+
return err
190+
}
191+
defer func() { _ = src.Close() }()
192+
193+
// #nosec G304 -- backupPath is generated internally and trusted
194+
dst, err := os.OpenFile(backupPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
195+
if err != nil {
196+
return err
197+
}
198+
199+
defer func() { _ = dst.Close() }()
200+
201+
if _, err := io.Copy(dst, src); err != nil {
202+
return err
203+
}
204+
205+
if err := dst.Sync(); err != nil {
206+
return err
207+
}
208+
return nil
209+
}

internal/adapters/ui/header.go

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,15 @@ import (
2525
type AppHeader struct {
2626
*tview.Flex
2727
version string
28-
buildTime string
2928
gitCommit string
3029
repoURL string
3130
}
3231

33-
func NewAppHeader(version, gitCommit, buildTime, repoURL string) *AppHeader {
32+
func NewAppHeader(version, gitCommit, repoURL string) *AppHeader {
3433
header := &AppHeader{
3534
Flex: tview.NewFlex(),
3635
version: version,
3736
repoURL: repoURL,
38-
buildTime: buildTime,
3937
gitCommit: gitCommit,
4038
}
4139
header.build()
@@ -85,13 +83,11 @@ func (h *AppHeader) buildCenterSection(bg tcell.Color) *tview.TextView {
8583
if commit != "" {
8684
commitTag = makeTag(commit, "#A78BFA") // violet
8785
}
88-
timeTag := makeTag(formatBuildTime(h.buildTime), "#3B82F6") // blue
8986

9087
text := versionTag
9188
if commitTag != "" {
9289
text += " " + commitTag
9390
}
94-
text += " " + timeTag
9591

9692
center.SetText(text)
9793
return center
@@ -126,30 +122,6 @@ func shortCommit(c string) string {
126122
return c
127123
}
128124

129-
// formatBuildTime tries to parse common time formats and returns a concise human-readable string.
130-
func formatBuildTime(s string) string {
131-
s = strings.TrimSpace(s)
132-
if s == "" {
133-
return "unknown"
134-
}
135-
layouts := []string{
136-
"2006-01-02 15:04:05",
137-
time.RFC3339,
138-
time.RFC1123,
139-
time.RFC1123Z,
140-
}
141-
var t time.Time
142-
var err error
143-
for _, l := range layouts {
144-
t, err = time.Parse(l, s)
145-
if err == nil {
146-
return t.Format("Mon, 02 Jan 2006 15:04")
147-
}
148-
}
149-
150-
return s
151-
}
152-
153125
// makeTag returns a rectangular-looking colored chip for the given text.
154126
func makeTag(text, bg string) string {
155127
text = strings.TrimSpace(text)

internal/adapters/ui/tui.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,8 @@ import (
2727
type tui struct {
2828
logger *zap.SugaredLogger
2929

30-
version string
31-
commit string
32-
buildDate string
30+
version string
31+
commit string
3332

3433
app *tview.Application
3534
serverService ports.ServerService
@@ -49,14 +48,13 @@ type tui struct {
4948
searchVisible bool
5049
}
5150

52-
func NewTUI(logger *zap.SugaredLogger, ss ports.ServerService, version, commit, buildDate string) *tui {
51+
func NewTUI(logger *zap.SugaredLogger, ss ports.ServerService, version, commit string) *tui {
5352
return &tui{
5453
logger: logger,
5554
app: tview.NewApplication(),
5655
serverService: ss,
5756
version: version,
5857
commit: commit,
59-
buildDate: buildDate,
6058
}
6159
}
6260

@@ -68,7 +66,7 @@ func (t *tui) Run() error {
6866
}()
6967
t.app.EnableMouse(true)
7068
t.initializeTheme().buildComponents().buildLayout().bindEvents().loadInitialData().loadSplashScreen()
71-
t.logger.Infow("starting TUI application", "version", t.version, "commit", t.commit, "buildDate", t.buildDate)
69+
t.logger.Infow("starting TUI application", "version", t.version, "commit", t.commit)
7270
if err := t.app.Run(); err != nil {
7371
t.logger.Errorw("application run error", "error", err)
7472
return err
@@ -89,7 +87,7 @@ func (t *tui) initializeTheme() *tui {
8987
}
9088

9189
func (t *tui) buildComponents() *tui {
92-
t.header = NewAppHeader(t.version, t.commit, t.buildDate, RepoURL)
90+
t.header = NewAppHeader(t.version, t.commit, RepoURL)
9391
t.searchBar = NewSearchBar().
9492
OnSearch(t.handleSearchInput).
9593
OnEscape(t.hideSearchBar)

makefile

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ SHELL = /usr/bin/env bash -o pipefail
2020
# Project variables
2121
PROJECT_NAME ?= $(shell basename $(CURDIR))
2222
VERSION ?= v0.1.0
23-
BUILD_TIME ?= $(shell date -u '+%Y-%m-%d_%H:%M:%S')
2423
GIT_COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
2524

2625
# Build variables
@@ -30,7 +29,7 @@ CMD_DIR ?= ./cmd
3029
PKG_LIST := $(shell go list ./...)
3130

3231
# LDFLAGS for version information
33-
LDFLAGS = -ldflags "-X main.version=$(VERSION) -X main.buildTime=$(BUILD_TIME) -X main.gitCommit=$(GIT_COMMIT)"
32+
LDFLAGS = -ldflags "-X main.version=$(VERSION) -X main.gitCommit=$(GIT_COMMIT)"
3433

3534
##@ Dependencies
3635

0 commit comments

Comments
 (0)