a geicko-2 based round robin ranking system designed to test c++ battleship submissions battleship.dunkirk.sh
at main 5.6 kB view raw
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}