a geicko-2 based round robin ranking system designed to test c++ battleship submissions
battleship.dunkirk.sh
1package server
2
3import (
4 "errors"
5 "fmt"
6 "log"
7 "strings"
8
9 "github.com/charmbracelet/ssh"
10 "github.com/charmbracelet/wish"
11 gossh "golang.org/x/crypto/ssh"
12
13 "battleship-arena/internal/storage"
14)
15
16var (
17 adminPasscode string
18 externalURL string
19)
20
21func GetServerURL() string {
22 // Strip protocol (http://, https://) from URL for SSH commands
23 url := externalURL
24 url = strings.TrimPrefix(url, "https://")
25 url = strings.TrimPrefix(url, "http://")
26 return url
27}
28
29func SetConfig(passcode, url string) {
30 adminPasscode = passcode
31 externalURL = url
32 log.Printf("✓ Config loaded: url=%s\n", url)
33}
34
35func PublicKeyAuthHandler(ctx ssh.Context, key ssh.PublicKey) bool {
36 publicKeyStr := strings.TrimSpace(string(gossh.MarshalAuthorizedKey(key)))
37
38 log.Printf("Auth attempt: user=%s, key_fingerprint=%s", ctx.User(), gossh.FingerprintSHA256(key))
39
40 // Try to find user by public key
41 user, err := storage.GetUserByPublicKey(publicKeyStr)
42 if err != nil {
43 log.Printf("Error looking up user by public key: %v", err)
44 return false
45 }
46
47 if user != nil {
48 // Existing user - verify username matches
49 log.Printf("Found existing user: %s (trying to login as: %s)", user.Username, ctx.User())
50 if user.Username == ctx.User() {
51 ctx.SetValue("user_id", user.ID)
52 ctx.SetValue("needs_onboarding", false)
53 storage.UpdateUserLastLogin(user.Username)
54 log.Printf("✓ Authenticated %s", user.Username)
55 return true
56 }
57 // Public key registered to different username
58 log.Printf("❌ Public key registered to %s, but trying to auth as %s", user.Username, ctx.User())
59 return false
60 }
61
62 log.Printf("New user detected: %s", ctx.User())
63
64 // New user - check if username is taken
65 existingUser, err := storage.GetUserByUsername(ctx.User())
66 if err != nil {
67 log.Printf("Error looking up username: %v", err)
68 return false
69 }
70
71 if existingUser != nil {
72 // Username taken by someone else
73 log.Printf("❌ Username %s already taken", ctx.User())
74 return false
75 }
76
77 // New user with available username - allow and mark for onboarding
78 log.Printf("✓ New user %s allowed for onboarding", ctx.User())
79 ctx.SetValue("public_key", publicKeyStr)
80 ctx.SetValue("needs_onboarding", true)
81 return true
82}
83
84func PasswordAuthHandler(ctx ssh.Context, password string) bool {
85 // Check for admin passcode override
86 if password == adminPasscode {
87 log.Printf("🔑 Admin passcode used for user: %s", ctx.User())
88
89 // Check if user exists
90 user, err := storage.GetUserByUsername(ctx.User())
91 if err != nil {
92 log.Printf("Error looking up username: %v", err)
93 return false
94 }
95
96 if user != nil {
97 // Existing user - allow login
98 ctx.SetValue("user_id", user.ID)
99 ctx.SetValue("needs_onboarding", false)
100 ctx.SetValue("admin_override", true)
101 log.Printf("✓ Admin authenticated as %s", user.Username)
102 return true
103 }
104
105 // New user - create with dummy key
106 log.Printf("✓ Admin creating new user: %s", ctx.User())
107 dummyKey := fmt.Sprintf("admin-override-%s", ctx.User())
108 newUser, err := storage.CreateUser(ctx.User(), ctx.User(), "Admin created user", "", dummyKey)
109 if err != nil {
110 log.Printf("Error creating user: %v", err)
111 return false
112 }
113
114 ctx.SetValue("user_id", newUser.ID)
115 ctx.SetValue("needs_onboarding", false)
116 ctx.SetValue("admin_override", true)
117 log.Printf("✓ Admin created and authenticated as %s", ctx.User())
118 return true
119 }
120
121 // Regular password auth disabled
122 return false
123}
124
125func SessionHandler(s ssh.Session) {
126 needsOnboarding := false
127 if val := s.Context().Value("needs_onboarding"); val != nil {
128 needsOnboarding = val.(bool)
129 }
130
131 if needsOnboarding {
132 // Run onboarding flow
133 if err := runOnboarding(s); err != nil {
134 wish.Errorln(s, fmt.Sprintf("Onboarding failed: %v", err))
135 return
136 }
137 }
138
139 // Normal session continues
140 wish.Println(s, "Welcome to Battleship Arena!")
141}
142
143func runOnboarding(s ssh.Session) error {
144 username := s.User()
145 publicKeyStr := ""
146 if val := s.Context().Value("public_key"); val != nil {
147 publicKeyStr = val.(string)
148 }
149
150 if publicKeyStr == "" {
151 return errors.New("no public key found")
152 }
153
154 wish.Println(s, "\n🚢 Welcome to Battleship Arena!")
155 wish.Println(s, fmt.Sprintf("Setting up account for: %s\n", username))
156
157 // Get name
158 wish.Print(s, "What's your full name? (required): ")
159 name, err := readLine(s)
160 if err != nil {
161 return err
162 }
163 if name == "" {
164 return errors.New("name is required")
165 }
166
167 // Get bio
168 wish.Print(s, "Bio (optional, press Enter to skip): ")
169 bio, err := readLine(s)
170 if err != nil {
171 return err
172 }
173
174 // Get link
175 wish.Print(s, "Link (optional, press Enter to skip): ")
176 link, err := readLine(s)
177 if err != nil {
178 return err
179 }
180
181 // Create user
182 _, err = storage.CreateUser(username, name, bio, link, publicKeyStr)
183 if err != nil {
184 return fmt.Errorf("failed to create user: %v", err)
185 }
186
187 wish.Println(s, "\n✅ Account created successfully!")
188 wish.Println(s, "You can now upload your battleship AI and compete!\n")
189
190 // Update context
191 s.Context().SetValue("needs_onboarding", false)
192
193 return nil
194}
195
196func readLine(s ssh.Session) (string, error) {
197 var line []byte
198 buf := make([]byte, 1)
199
200 for {
201 n, err := s.Read(buf)
202 if err != nil {
203 return "", err
204 }
205 if n == 0 {
206 continue
207 }
208
209 b := buf[0]
210
211 // Handle newline
212 if b == '\n' || b == '\r' {
213 return string(line), nil
214 }
215
216 // Handle backspace
217 if b == 127 || b == 8 {
218 if len(line) > 0 {
219 line = line[:len(line)-1]
220 s.Write([]byte("\b \b"))
221 }
222 continue
223 }
224
225 // Handle printable characters
226 if b >= 32 && b < 127 {
227 line = append(line, b)
228 s.Write(buf[:1])
229 }
230 }
231}