Skip to content

Commit 7814369

Browse files
committed
feat: use ssh key fingerprint + username pair to save stats, redo blacklist to make it look like blacklisted usernames need an ssh key, remove some old logic that is no longer needed
1 parent 03bd6ea commit 7814369

File tree

7 files changed

+147
-115
lines changed

7 files changed

+147
-115
lines changed

.github/workflows/deploy.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
name: Deploy
22

33
on:
4+
push:
5+
branches: [ prod ]
46
workflow_dispatch:
57

68
jobs:

internal/server/server.go

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/f-gillmann/wordle-ssh/internal/ui"
2020
"github.com/f-gillmann/wordle-ssh/internal/wordle"
2121
"github.com/muesli/termenv"
22+
gossh "golang.org/x/crypto/ssh"
2223
)
2324

2425
const (
@@ -132,41 +133,38 @@ func New(config Config) (*Server, error) {
132133
return nil, fmt.Errorf("failed to fetch wordle word: %w", err)
133134
}
134135

136+
checkBlacklist := func(ctx ssh.Context) bool {
137+
username := ctx.User()
138+
if stats.IsBlacklisted(username) {
139+
config.Logger.Info("Blocked connection from blacklisted user", "username", username, "address", ctx.RemoteAddr(), "client", ctx.ClientVersion())
140+
return false
141+
}
142+
143+
return true
144+
}
145+
135146
// Create wish server with bubbletea middleware
136147
wishServer, err := wish.NewServer(
137148
wish.WithAddress(fmt.Sprintf("%s:%s", config.Host, config.Port)),
138149
wish.WithHostKeyPath(config.HostKeyPath),
150+
wish.WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
151+
return checkBlacklist(ctx)
152+
}),
139153
wish.WithMiddleware(
140-
s.blacklistCommandMiddleware,
141154
bubbletea.MiddlewareWithColorProfile(s.teaHandler, termenv.ANSI256),
142155
activeterm.Middleware(),
143156
logging.StructuredMiddlewareWithLogger(config.Logger, config.LogLevel),
144157
),
145158
)
159+
146160
if err != nil {
147161
return nil, fmt.Errorf("failed to create server: %w", err)
148162
}
149-
s.wishServer = wishServer
150163

164+
s.wishServer = wishServer
151165
return s, nil
152166
}
153167

154-
// blacklistCommandMiddleware rejects command execution from blacklisted users
155-
func (s *Server) blacklistCommandMiddleware(next ssh.Handler) ssh.Handler {
156-
return func(sess ssh.Session) {
157-
username := sess.User()
158-
if stats.IsBlacklisted(username) {
159-
// Check if they're trying to execute a command
160-
if len(sess.Command()) > 0 {
161-
s.config.Logger.Info("Rejected command from blacklisted user", "username", username, "command", sess.Command())
162-
_ = sess.Exit(1)
163-
return
164-
}
165-
}
166-
next(sess)
167-
}
168-
}
169-
170168
// refreshWordleWord fetches the Wordle word only if it's a new day
171169
func (s *Server) refreshWordleWord() error {
172170
today := time.Now().Format("2006-01-02")
@@ -201,22 +199,22 @@ func (s *Server) teaHandler(sshSession ssh.Session) (tea.Model, []tea.ProgramOpt
201199
username = "anonymous"
202200
}
203201

204-
// Check if username is blacklisted
205-
isBlacklisted := stats.IsBlacklisted(username)
206-
207-
// Check if user has already played today (but not for blacklisted users)
208-
hasPlayed := false
209-
if !isBlacklisted {
210-
var err error
211-
hasPlayed, err = s.statsStore.HasPlayedToday(username, s.wordleDate)
202+
// Get SSH key fingerprint from session
203+
sshKeyFingerprint := gossh.FingerprintSHA256(sshSession.PublicKey())
204+
s.config.Logger.Debug("User connecting",
205+
"username", username,
206+
"ssh_key_fingerprint", sshKeyFingerprint,
207+
"key_type", sshSession.PublicKey().Type(),
208+
)
212209

213-
if err != nil {
214-
s.config.Logger.Error("Failed to check if user played today", "error", err, "username", username)
215-
}
210+
// Check if user has already played today
211+
hasPlayed, err := s.statsStore.HasPlayedToday(username, sshKeyFingerprint, s.wordleDate)
212+
if err != nil {
213+
s.config.Logger.Error("Failed to check if user played today", "error", err, "username", username)
216214
}
217215

218216
// Create the app model with the current word, stats store, and logger
219-
m := ui.NewAppModel(s.wordleWord, s.wordleDate, username, s.statsStore, hasPlayed, isBlacklisted, s.config.Logger)
217+
m := ui.NewAppModel(s.wordleWord, s.wordleDate, username, sshKeyFingerprint, s.statsStore, hasPlayed, s.config.Logger)
220218

221219
opts := []tea.ProgramOption{tea.WithAltScreen()}
222220
opts = append(opts, bubbletea.MakeOptions(sshSession)...)

internal/stats/blacklist.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ var BlacklistedUsernames = map[string]bool{
2020
"default": true,
2121
"1234": true,
2222
"ftp": true,
23-
}
23+
"nginx": true,
24+
"ubnt": true}
2425

2526
// IsBlacklisted checks if a username is blacklisted
2627
func IsBlacklisted(username string) bool {

internal/stats/stats.go

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
// UserStats represents a user's game statistics
1414
type UserStats struct {
1515
Username string
16+
SSHKeyFingerprint string
1617
GamesPlayed int
1718
GamesWon int
1819
GamesLost int
@@ -54,7 +55,8 @@ func NewStore(dbPath string, logger *log.Logger) (*Store, error) {
5455
func (s *Store) initSchema() error {
5556
schema := `
5657
CREATE TABLE IF NOT EXISTS user_stats (
57-
username TEXT PRIMARY KEY,
58+
username TEXT NOT NULL,
59+
ssh_key_fingerprint TEXT NOT NULL,
5860
games_played INTEGER DEFAULT 0,
5961
games_won INTEGER DEFAULT 0,
6062
games_lost INTEGER DEFAULT 0,
@@ -71,11 +73,13 @@ func (s *Store) initSchema() error {
7173
last_word_date TEXT,
7274
last_game_result TEXT,
7375
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
74-
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
76+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
77+
PRIMARY KEY (username, ssh_key_fingerprint)
7578
);
7679
7780
CREATE INDEX IF NOT EXISTS idx_last_played ON user_stats(last_played);
7881
CREATE INDEX IF NOT EXISTS idx_games_won ON user_stats(games_won DESC);
82+
CREATE INDEX IF NOT EXISTS idx_ssh_key ON user_stats(ssh_key_fingerprint);
7983
`
8084

8185
if _, err := s.db.Exec(schema); err != nil {
@@ -94,6 +98,7 @@ func (s *Store) scanUserStats(scanner interface {
9498

9599
err := scanner.Scan(
96100
&stats.Username,
101+
&stats.SSHKeyFingerprint,
97102
&stats.GamesPlayed,
98103
&stats.GamesWon,
99104
&stats.GamesLost,
@@ -122,50 +127,72 @@ func (s *Store) scanUserStats(scanner interface {
122127
return nil
123128
}
124129

125-
// GetUserStats retrieves statistics for a user
126-
func (s *Store) GetUserStats(username string) (*UserStats, error) {
130+
// GetUserStats retrieves statistics for a user by username AND ssh_key_fingerprint pair
131+
func (s *Store) GetUserStats(username string, sshKeyFingerprint string) (*UserStats, error) {
132+
s.logger.Debug("Reading user stats", "username", username, "ssh_key_fingerprint", sshKeyFingerprint)
133+
127134
query := `
128-
SELECT username, games_played, games_won, games_lost, current_streak, max_streak,
135+
SELECT username, ssh_key_fingerprint, games_played, games_won, games_lost, current_streak, max_streak,
129136
guess_dist_1, guess_dist_2, guess_dist_3, guess_dist_4, guess_dist_5, guess_dist_6,
130137
total_guesses, last_played, COALESCE(last_word_date, ''), COALESCE(last_game_result, '')
131138
FROM user_stats
132-
WHERE username = ?
139+
WHERE username = ? AND ssh_key_fingerprint = ?
133140
`
134141

135142
var stats UserStats
136143

137-
err := s.scanUserStats(s.db.QueryRow(query, username), &stats)
144+
err := s.scanUserStats(s.db.QueryRow(query, username, sshKeyFingerprint), &stats)
138145
if errors.Is(err, sql.ErrNoRows) {
139146
// Return empty stats for new user
147+
s.logger.Debug("No existing stats found, returning empty stats for new user", "username", username)
140148
return &UserStats{
141-
Username: username,
149+
Username: username,
150+
SSHKeyFingerprint: sshKeyFingerprint,
142151
}, nil
143152
}
144153

145154
if err != nil {
146155
return nil, fmt.Errorf("failed to get user stats: %w", err)
147156
}
148157

158+
s.logger.Debug("Successfully retrieved user stats",
159+
"username", username,
160+
"games_played", stats.GamesPlayed,
161+
"games_won", stats.GamesWon,
162+
"current_streak", stats.CurrentStreak,
163+
"last_word_date", stats.LastWordDate,
164+
)
165+
149166
return &stats, nil
150167
}
151168

152169
// HasPlayedToday checks if the user has already played today's word
153-
func (s *Store) HasPlayedToday(username string, wordDate string) (bool, error) {
154-
stats, err := s.GetUserStats(username)
170+
func (s *Store) HasPlayedToday(username string, sshKeyFingerprint string, wordDate string) (bool, error) {
171+
s.logger.Debug("Checking if user has played today", "username", username, "word_date", wordDate)
172+
173+
stats, err := s.GetUserStats(username, sshKeyFingerprint)
155174
if err != nil {
156175
return false, err
157176
}
158177

159-
return stats.LastWordDate == wordDate, nil
178+
hasPlayed := stats.LastWordDate == wordDate
179+
s.logger.Debug("Played today check result",
180+
"username", username,
181+
"has_played", hasPlayed,
182+
"last_word_date", stats.LastWordDate,
183+
"current_word_date", wordDate,
184+
)
185+
186+
return hasPlayed, nil
160187
}
161188

162189
// RecordWin records a winning game for a user
163-
func (s *Store) RecordWin(username string, guesses int, wordDate string, gameResult string) error {
190+
func (s *Store) RecordWin(username string, sshKeyFingerprint string, guesses int, wordDate string, gameResult string) error {
164191
if guesses < 1 || guesses > 6 {
165192
return fmt.Errorf("invalid number of guesses: %d", guesses)
166193
}
167194

168-
stats, err := s.GetUserStats(username)
195+
stats, err := s.GetUserStats(username, sshKeyFingerprint)
169196
if err != nil {
170197
return err
171198
}
@@ -192,8 +219,8 @@ func (s *Store) RecordWin(username string, guesses int, wordDate string, gameRes
192219
}
193220

194221
// RecordLoss records a losing game for a user
195-
func (s *Store) RecordLoss(username string, wordDate string, gameResult string) error {
196-
stats, err := s.GetUserStats(username)
222+
func (s *Store) RecordLoss(username string, sshKeyFingerprint string, wordDate string, gameResult string) error {
223+
stats, err := s.GetUserStats(username, sshKeyFingerprint)
197224
if err != nil {
198225
return err
199226
}
@@ -215,13 +242,27 @@ func (s *Store) RecordLoss(username string, wordDate string, gameResult string)
215242

216243
// saveUserStats saves or updates user statistics
217244
func (s *Store) saveUserStats(stats *UserStats) error {
245+
// Create a hash of the SSH public key for logging
246+
247+
s.logger.Debug("Saving user stats",
248+
"username", stats.Username,
249+
"ssh_key_fingerprint", stats.SSHKeyFingerprint,
250+
"games_played", stats.GamesPlayed,
251+
"games_won", stats.GamesWon,
252+
"games_lost", stats.GamesLost,
253+
"current_streak", stats.CurrentStreak,
254+
"max_streak", stats.MaxStreak,
255+
"total_guesses", stats.TotalGuesses,
256+
"last_word_date", stats.LastWordDate,
257+
)
258+
218259
query := `
219260
INSERT INTO user_stats (
220-
username, games_played, games_won, games_lost, current_streak, max_streak,
261+
username, ssh_key_fingerprint, games_played, games_won, games_lost, current_streak, max_streak,
221262
guess_dist_1, guess_dist_2, guess_dist_3, guess_dist_4, guess_dist_5, guess_dist_6,
222263
total_guesses, last_played, last_word_date, last_game_result, updated_at
223-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
224-
ON CONFLICT(username) DO UPDATE SET
264+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
265+
ON CONFLICT(username, ssh_key_fingerprint) DO UPDATE SET
225266
games_played = excluded.games_played,
226267
games_won = excluded.games_won,
227268
games_lost = excluded.games_lost,
@@ -242,6 +283,7 @@ func (s *Store) saveUserStats(stats *UserStats) error {
242283

243284
_, err := s.db.Exec(query,
244285
stats.Username,
286+
stats.SSHKeyFingerprint,
245287
stats.GamesPlayed,
246288
stats.GamesWon,
247289
stats.GamesLost,
@@ -263,6 +305,7 @@ func (s *Store) saveUserStats(stats *UserStats) error {
263305
return fmt.Errorf("failed to save user stats: %w", err)
264306
}
265307

308+
s.logger.Debug("Successfully saved user stats", "username", stats.Username, "ssh_key_fingerprint", stats.SSHKeyFingerprint)
266309
return nil
267310
}
268311

0 commit comments

Comments
 (0)