a geicko-2 based round robin ranking system designed to test c++ battleship submissions battleship.dunkirk.sh
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: working uploads

+235 -77
+4 -2
go.mod
··· 27 27 github.com/creack/pty v1.1.21 // indirect 28 28 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 29 29 github.com/go-logfmt/logfmt v0.6.0 // indirect 30 + github.com/kr/fs v0.1.0 // indirect 30 31 github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 31 32 github.com/mattn/go-isatty v0.0.20 // indirect 32 33 github.com/mattn/go-localereader v0.0.1 // indirect ··· 34 35 github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 35 36 github.com/muesli/cancelreader v0.2.2 // indirect 36 37 github.com/muesli/termenv v0.16.0 // indirect 38 + github.com/pkg/sftp v1.13.10 // indirect 37 39 github.com/rivo/uniseg v0.4.7 // indirect 38 40 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 39 - golang.org/x/crypto v0.37.0 // indirect 41 + golang.org/x/crypto v0.41.0 // indirect 40 42 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect 41 43 golang.org/x/sys v0.36.0 // indirect 42 - golang.org/x/text v0.24.0 // indirect 44 + golang.org/x/text v0.28.0 // indirect 43 45 )
+7
go.sum
··· 42 42 github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 43 43 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 44 44 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 45 + github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= 46 + github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 45 47 github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 46 48 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 47 49 github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= ··· 60 62 github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 61 63 github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= 62 64 github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= 65 + github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU= 66 + github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA= 63 67 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 64 68 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 65 69 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= ··· 71 75 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 72 76 golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 73 77 golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 78 + golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= 79 + golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= 74 80 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= 75 81 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 76 82 golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= ··· 81 87 golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 82 88 golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 83 89 golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 90 + golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= 84 91 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 85 92 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+10 -2
main.go
··· 16 16 "github.com/charmbracelet/wish" 17 17 "github.com/charmbracelet/wish/bubbletea" 18 18 "github.com/charmbracelet/wish/logging" 19 + "github.com/charmbracelet/wish/scp" 19 20 ) 20 21 21 22 const ( ··· 40 41 // Start web server 41 42 go startWebServer() 42 43 43 - // Start SSH server with TUI 44 + // Start SSH server with TUI, SCP, and SFTP 45 + toClient, fromClient := newSCPHandlers() 44 46 s, err := wish.NewServer( 45 47 wish.WithAddress(host + ":" + sshPort), 46 48 wish.WithHostKeyPath(".ssh/battleship_arena"), 49 + wish.WithSubsystem("sftp", sftpHandler), 47 50 wish.WithMiddleware( 48 - scpMiddleware(), 51 + scp.Middleware(toClient, fromClient), 49 52 bubbletea.Middleware(teaHandler), 50 53 logging.Middleware(), 51 54 ), ··· 76 79 } 77 80 78 81 func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) { 82 + // Don't handle non-interactive sessions (SCP/SFTP have commands) 83 + if len(s.Command()) > 0 { 84 + return nil, nil 85 + } 86 + 79 87 pty, _, active := s.Pty() 80 88 if !active { 81 89 wish.Fatalln(s, "no active terminal")
+51 -73
scp.go
··· 2 2 3 3 import ( 4 4 "fmt" 5 - "io" 6 5 "log" 7 6 "os" 8 7 "path/filepath" 8 + "strings" 9 9 10 10 "github.com/charmbracelet/ssh" 11 - "github.com/charmbracelet/wish" 11 + "github.com/charmbracelet/wish/scp" 12 12 ) 13 13 14 - // Add SCP support as a custom middleware 15 - func scpMiddleware() wish.Middleware { 16 - return func(sh ssh.Handler) ssh.Handler { 17 - return func(s ssh.Session) { 18 - cmd := s.Command() 19 - if len(cmd) > 0 && cmd[0] == "scp" { 20 - handleSCP(s, cmd) 21 - return 22 - } 23 - sh(s) 24 - } 14 + func newSCPHandlers() (scp.CopyToClientHandler, scp.CopyFromClientHandler) { 15 + // Use FileSystemHandler as base 16 + baseHandler := scp.NewFileSystemHandler(uploadDir) 17 + 18 + // Wrap it to add validation and user namespacing 19 + uploadHandler := &validatingHandler{ 20 + baseHandler: baseHandler, 25 21 } 22 + 23 + // Return nil for downloads (disabled), wrapped handler for uploads 24 + return nil, uploadHandler 26 25 } 27 26 28 - func handleSCP(s ssh.Session, cmd []string) { 29 - // Parse SCP command 30 - target := false 31 - filename := "" 32 - 33 - for i, arg := range cmd { 34 - if arg == "-t" { 35 - target = true 36 - } else if i == len(cmd)-1 { 37 - filename = filepath.Base(arg) 38 - } 39 - } 27 + type validatingHandler struct { 28 + baseHandler scp.CopyFromClientHandler 29 + } 40 30 41 - if !target { 42 - log.Printf("SCP source mode not supported from %s", s.User()) 43 - fmt.Fprintf(s, "SCP source mode not supported\n") 44 - s.Exit(1) 45 - return 31 + func (h *validatingHandler) Write(s ssh.Session, entry *scp.FileEntry) (int64, error) { 32 + filename := filepath.Base(entry.Name) 33 + log.Printf("SCP Write called: entry.Name=%s, filename=%s, size=%d", entry.Name, filename, entry.Size) 34 + 35 + // Skip validation for directory markers (SCP protocol negotiation) 36 + if filename == "~" || filename == "." || filename == ".." { 37 + log.Printf("Skipping directory marker: %s", filename) 38 + return 0, nil 46 39 } 47 - 48 - // Validate filename 49 - matched, _ := filepath.Match("memory_functions_*.cpp", filename) 50 - if !matched { 40 + 41 + // Validate filename (must be memory_functions_*.cpp) 42 + if !strings.HasPrefix(filename, "memory_functions_") || !strings.HasSuffix(filename, ".cpp") { 51 43 log.Printf("Invalid filename from %s: %s", s.User(), filename) 52 - fmt.Fprintf(s, "Only memory_functions_*.cpp files are accepted\n") 53 - s.Exit(1) 54 - return 44 + return 0, fmt.Errorf("only memory_functions_*.cpp files are accepted") 55 45 } 56 46 57 - // Create user directory 47 + // Create user-specific subdirectory 58 48 userDir := filepath.Join(uploadDir, s.User()) 59 49 if err := os.MkdirAll(userDir, 0755); err != nil { 60 50 log.Printf("Failed to create user directory: %v", err) 61 - s.Exit(1) 62 - return 51 + return 0, err 63 52 } 64 53 65 - // SCP protocol: send 0 byte to indicate ready 66 - fmt.Fprintf(s, "\x00") 67 - 68 - // Read SCP header (C0644 size filename) 69 - buf := make([]byte, 1024) 70 - n, err := s.Read(buf) 71 - if err != nil { 72 - log.Printf("Failed to read SCP header: %v", err) 73 - s.Exit(1) 74 - return 54 + // Modify the entry to write to user's subdirectory 55 + userEntry := &scp.FileEntry{ 56 + Name: filepath.Join(s.User(), filename), 57 + Mode: entry.Mode, 58 + Size: entry.Size, 59 + Reader: entry.Reader, 75 60 } 76 - 77 - // Acknowledge header 78 - fmt.Fprintf(s, "\x00") 61 + 62 + log.Printf("Writing to: %s", filepath.Join(uploadDir, userEntry.Name)) 79 63 80 - // Save file 81 - dstPath := filepath.Join(userDir, filename) 82 - file, err := os.Create(dstPath) 64 + n, err := h.baseHandler.Write(s, userEntry) 83 65 if err != nil { 84 - log.Printf("Failed to create file: %v", err) 85 - s.Exit(1) 86 - return 87 - } 88 - defer file.Close() 89 - 90 - // Read file content 91 - _, err = io.Copy(file, io.LimitReader(s, int64(n))) 92 - if err != nil && err != io.EOF { 93 - log.Printf("Failed to write file: %v", err) 94 - s.Exit(1) 95 - return 66 + log.Printf("Write error: %v", err) 67 + return n, err 96 68 } 97 69 98 - // Final acknowledgment 99 - fmt.Fprintf(s, "\x00") 100 - 101 - log.Printf("Uploaded %s from %s", filename, s.User()) 70 + log.Printf("Uploaded %s from %s (%d bytes)", filename, s.User(), n) 102 71 addSubmission(s.User(), filename) 103 72 104 - s.Exit(0) 73 + return n, nil 74 + } 75 + 76 + func (h *validatingHandler) Mkdir(s ssh.Session, entry *scp.DirEntry) error { 77 + // Allow mkdir but namespace it to user directory 78 + userEntry := &scp.DirEntry{ 79 + Name: filepath.Join(s.User(), entry.Name), 80 + Mode: entry.Mode, 81 + } 82 + return h.baseHandler.Mkdir(s, userEntry) 105 83 }
+163
sftp.go
··· 1 + package main 2 + 3 + import ( 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 + 17 + func sftpHandler(s ssh.Session) { 18 + userDir := filepath.Join(uploadDir, s.User()) 19 + 20 + // Create user directory if it doesn't exist 21 + if err := os.MkdirAll(userDir, 0755); err != nil { 22 + log.Printf("Failed to create user directory: %v", err) 23 + return 24 + } 25 + 26 + handler := &sftpFileHandler{ 27 + baseDir: userDir, 28 + username: s.User(), 29 + } 30 + 31 + server := sftp.NewRequestServer(s, sftp.Handlers{ 32 + FileGet: handler, 33 + FilePut: handler, 34 + FileCmd: handler, 35 + FileList: handler, 36 + }) 37 + 38 + if err := server.Serve(); err == io.EOF { 39 + server.Close() 40 + } else if err != nil { 41 + log.Printf("sftp server error: %v", err) 42 + wish.Fatalln(s, err) 43 + } 44 + } 45 + 46 + type sftpFileHandler struct { 47 + baseDir string 48 + username string 49 + } 50 + 51 + // Fileread for downloads (disabled) 52 + func (h *sftpFileHandler) Fileread(r *sftp.Request) (io.ReaderAt, error) { 53 + return nil, fmt.Errorf("downloads not supported") 54 + } 55 + 56 + // Filewrite for uploads 57 + func (h *sftpFileHandler) Filewrite(r *sftp.Request) (io.WriterAt, error) { 58 + filename := filepath.Base(r.Filepath) 59 + 60 + // Validate filename 61 + if !strings.HasPrefix(filename, "memory_functions_") || !strings.HasSuffix(filename, ".cpp") { 62 + log.Printf("Invalid filename from %s: %s", h.username, filename) 63 + return nil, fmt.Errorf("only memory_functions_*.cpp files are accepted") 64 + } 65 + 66 + dstPath := filepath.Join(h.baseDir, filename) 67 + log.Printf("SFTP: Creating file %s for user %s", dstPath, h.username) 68 + 69 + flags := r.Pflags() 70 + var osFlags int 71 + if flags.Creat { 72 + osFlags |= os.O_CREATE 73 + } 74 + if flags.Trunc { 75 + osFlags |= os.O_TRUNC 76 + } 77 + if flags.Write { 78 + osFlags |= os.O_WRONLY 79 + } 80 + 81 + file, err := os.OpenFile(dstPath, osFlags, 0644) 82 + if err != nil { 83 + log.Printf("Failed to create file: %v", err) 84 + return nil, err 85 + } 86 + 87 + return &fileWriterAt{ 88 + file: file, 89 + filename: filename, 90 + username: h.username, 91 + }, nil 92 + } 93 + 94 + // Filecmd handles file operations 95 + func (h *sftpFileHandler) Filecmd(r *sftp.Request) error { 96 + switch r.Method { 97 + case "Setstat", "Rename", "Remove", "Mkdir", "Rmdir": 98 + // Allow these operations within user directory 99 + return nil 100 + default: 101 + return sftp.ErrSSHFxOpUnsupported 102 + } 103 + } 104 + 105 + // Filelist for directory listings 106 + func (h *sftpFileHandler) Filelist(r *sftp.Request) (sftp.ListerAt, error) { 107 + switch r.Method { 108 + case "List": 109 + entries, err := os.ReadDir(h.baseDir) 110 + if err != nil { 111 + return nil, err 112 + } 113 + infos := make([]fs.FileInfo, 0, len(entries)) 114 + for _, entry := range entries { 115 + info, err := entry.Info() 116 + if err != nil { 117 + continue 118 + } 119 + infos = append(infos, info) 120 + } 121 + return listerAt(infos), nil 122 + case "Stat": 123 + info, err := os.Stat(filepath.Join(h.baseDir, r.Filepath)) 124 + if err != nil { 125 + return nil, err 126 + } 127 + return listerAt{info}, nil 128 + default: 129 + return nil, sftp.ErrSSHFxOpUnsupported 130 + } 131 + } 132 + 133 + type listerAt []fs.FileInfo 134 + 135 + func (l listerAt) ListAt(ls []fs.FileInfo, offset int64) (int, error) { 136 + if offset >= int64(len(l)) { 137 + return 0, io.EOF 138 + } 139 + n := copy(ls, l[offset:]) 140 + if n < len(ls) { 141 + return n, io.EOF 142 + } 143 + return n, nil 144 + } 145 + 146 + type fileWriterAt struct { 147 + file *os.File 148 + filename string 149 + username string 150 + } 151 + 152 + func (f *fileWriterAt) WriteAt(p []byte, off int64) (int, error) { 153 + return f.file.WriteAt(p, off) 154 + } 155 + 156 + func (f *fileWriterAt) Close() error { 157 + err := f.file.Close() 158 + if err == nil { 159 + log.Printf("SFTP: Uploaded %s from %s", f.filename, f.username) 160 + addSubmission(f.username, f.filename) 161 + } 162 + return err 163 + }