a geicko-2 based round robin ranking system designed to test c++ battleship submissions
battleship.dunkirk.sh
1package server
2
3import (
4 "fmt"
5 "io"
6 "io/fs"
7 "log"
8 "os"
9 "path/filepath"
10 "strings"
11
12 "github.com/charmbracelet/ssh"
13 "github.com/charmbracelet/wish"
14 "github.com/pkg/sftp"
15
16 "battleship-arena/internal/storage"
17)
18
19func SFTPHandler(uploadDir string) func(ssh.Session) {
20 return func(s ssh.Session) {
21 userDir := filepath.Join(uploadDir, s.User())
22
23 if err := os.MkdirAll(userDir, 0755); err != nil {
24 log.Printf("Failed to create user directory: %v", err)
25 return
26 }
27
28 handler := &sftpFileHandler{
29 baseDir: userDir,
30 username: s.User(),
31 }
32
33 server := sftp.NewRequestServer(s, sftp.Handlers{
34 FileGet: handler,
35 FilePut: handler,
36 FileCmd: handler,
37 FileList: handler,
38 })
39
40 if err := server.Serve(); err == io.EOF {
41 server.Close()
42 } else if err != nil {
43 log.Printf("sftp server error: %v", err)
44 wish.Fatalln(s, err)
45 }
46 }
47}
48
49type sftpFileHandler struct {
50 baseDir string
51 username string
52}
53
54// Fileread for downloads (disabled)
55func (h *sftpFileHandler) Fileread(r *sftp.Request) (io.ReaderAt, error) {
56 return nil, fmt.Errorf("downloads not supported")
57}
58
59// Filewrite for uploads
60func (h *sftpFileHandler) Filewrite(r *sftp.Request) (io.WriterAt, error) {
61 filename := filepath.Base(r.Filepath)
62
63 // Validate filename
64 if !strings.HasPrefix(filename, "memory_functions_") || !strings.HasSuffix(filename, ".cpp") {
65 log.Printf("Invalid filename from %s: %s", h.username, filename)
66 return nil, fmt.Errorf("only memory_functions_*.cpp files are accepted")
67 }
68
69 dstPath := filepath.Join(h.baseDir, filename)
70 log.Printf("SFTP: Creating file %s for user %s", dstPath, h.username)
71
72 // Remove old file if it exists to ensure clean overwrite
73 if _, err := os.Stat(dstPath); err == nil {
74 log.Printf("SFTP: Removing old file: %s", dstPath)
75 os.Remove(dstPath)
76 }
77
78 flags := r.Pflags()
79 var osFlags int
80 if flags.Creat {
81 osFlags |= os.O_CREATE
82 }
83 if flags.Trunc {
84 osFlags |= os.O_TRUNC
85 }
86 if flags.Write {
87 osFlags |= os.O_WRONLY
88 }
89
90 file, err := os.OpenFile(dstPath, osFlags, 0644)
91 if err != nil {
92 log.Printf("Failed to create file: %v", err)
93 return nil, err
94 }
95
96 return &fileWriterAt{
97 file: file,
98 filename: filename,
99 username: h.username,
100 }, nil
101}
102
103// Filecmd handles file operations
104func (h *sftpFileHandler) Filecmd(r *sftp.Request) error {
105 switch r.Method {
106 case "Setstat", "Rename", "Remove", "Mkdir", "Rmdir":
107 // Allow these operations within user directory
108 return nil
109 default:
110 return sftp.ErrSSHFxOpUnsupported
111 }
112}
113
114// Filelist for directory listings
115func (h *sftpFileHandler) Filelist(r *sftp.Request) (sftp.ListerAt, error) {
116 switch r.Method {
117 case "List":
118 entries, err := os.ReadDir(h.baseDir)
119 if err != nil {
120 return nil, err
121 }
122 infos := make([]fs.FileInfo, 0, len(entries))
123 for _, entry := range entries {
124 info, err := entry.Info()
125 if err != nil {
126 continue
127 }
128 infos = append(infos, info)
129 }
130 return listerAt(infos), nil
131 case "Stat":
132 info, err := os.Stat(filepath.Join(h.baseDir, r.Filepath))
133 if err != nil {
134 return nil, err
135 }
136 return listerAt{info}, nil
137 default:
138 return nil, sftp.ErrSSHFxOpUnsupported
139 }
140}
141
142type listerAt []fs.FileInfo
143
144func (l listerAt) ListAt(ls []fs.FileInfo, offset int64) (int, error) {
145 if offset >= int64(len(l)) {
146 return 0, io.EOF
147 }
148 n := copy(ls, l[offset:])
149 if n < len(ls) {
150 return n, io.EOF
151 }
152 return n, nil
153}
154
155type fileWriterAt struct {
156 file *os.File
157 filename string
158 username string
159}
160
161func (f *fileWriterAt) WriteAt(p []byte, off int64) (int, error) {
162 return f.file.WriteAt(p, off)
163}
164
165func (f *fileWriterAt) Close() error {
166 err := f.file.Close()
167 if err == nil {
168 log.Printf("SFTP: Uploaded %s from %s", f.filename, f.username)
169
170 // Add submission and trigger testing
171 submissionID, err := storage.AddSubmission(f.username, f.filename)
172 if err != nil {
173 log.Printf("Failed to add submission: %v", err)
174 } else {
175 log.Printf("Queued submission %d for testing", submissionID)
176 // The worker will pick it up automatically
177 }
178 }
179 return err
180}