a geicko-2 based round robin ranking system designed to test c++ battleship submissions
battleship.dunkirk.sh
1package sftp
2
3// sftp server counterpart
4
5import (
6 "encoding"
7 "errors"
8 "fmt"
9 "io"
10 "io/fs"
11 "io/ioutil"
12 "os"
13 "path/filepath"
14 "strconv"
15 "sync"
16 "syscall"
17 "time"
18)
19
20const (
21 // SftpServerWorkerCount defines the number of workers for the SFTP server
22 SftpServerWorkerCount = 8
23)
24
25type file interface {
26 Stat() (os.FileInfo, error)
27 ReadAt(b []byte, off int64) (int, error)
28 WriteAt(b []byte, off int64) (int, error)
29 Readdir(int) ([]os.FileInfo, error)
30 Name() string
31 Truncate(int64) error
32 Chmod(mode fs.FileMode) error
33 Chown(uid, gid int) error
34 Close() error
35}
36
37// Server is an SSH File Transfer Protocol (sftp) server.
38// This is intended to provide the sftp subsystem to an ssh server daemon.
39// This implementation currently supports most of sftp server protocol version 3,
40// as specified at https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt
41type Server struct {
42 *serverConn
43 debugStream io.Writer
44 readOnly bool
45 pktMgr *packetManager
46 openFiles map[string]file
47 openFilesLock sync.RWMutex
48 handleCount int
49 workDir string
50 winRoot bool
51 maxTxPacket uint32
52}
53
54func (svr *Server) nextHandle(f file) string {
55 svr.openFilesLock.Lock()
56 defer svr.openFilesLock.Unlock()
57 svr.handleCount++
58 handle := strconv.Itoa(svr.handleCount)
59 svr.openFiles[handle] = f
60 return handle
61}
62
63func (svr *Server) closeHandle(handle string) error {
64 svr.openFilesLock.Lock()
65 defer svr.openFilesLock.Unlock()
66 if f, ok := svr.openFiles[handle]; ok {
67 delete(svr.openFiles, handle)
68 return f.Close()
69 }
70
71 return EBADF
72}
73
74func (svr *Server) getHandle(handle string) (file, bool) {
75 svr.openFilesLock.RLock()
76 defer svr.openFilesLock.RUnlock()
77 f, ok := svr.openFiles[handle]
78 return f, ok
79}
80
81type serverRespondablePacket interface {
82 encoding.BinaryUnmarshaler
83 id() uint32
84 respond(svr *Server) responsePacket
85}
86
87// NewServer creates a new Server instance around the provided streams, serving
88// content from the root of the filesystem. Optionally, ServerOption
89// functions may be specified to further configure the Server.
90//
91// A subsequent call to Serve() is required to begin serving files over SFTP.
92func NewServer(rwc io.ReadWriteCloser, options ...ServerOption) (*Server, error) {
93 svrConn := &serverConn{
94 conn: conn{
95 Reader: rwc,
96 WriteCloser: rwc,
97 },
98 }
99 s := &Server{
100 serverConn: svrConn,
101 debugStream: ioutil.Discard,
102 pktMgr: newPktMgr(svrConn),
103 openFiles: make(map[string]file),
104 maxTxPacket: defaultMaxTxPacket,
105 }
106
107 for _, o := range options {
108 if err := o(s); err != nil {
109 return nil, err
110 }
111 }
112
113 return s, nil
114}
115
116// A ServerOption is a function which applies configuration to a Server.
117type ServerOption func(*Server) error
118
119// WithDebug enables Server debugging output to the supplied io.Writer.
120func WithDebug(w io.Writer) ServerOption {
121 return func(s *Server) error {
122 s.debugStream = w
123 return nil
124 }
125}
126
127// ReadOnly configures a Server to serve files in read-only mode.
128func ReadOnly() ServerOption {
129 return func(s *Server) error {
130 s.readOnly = true
131 return nil
132 }
133}
134
135// WindowsRootEnumeratesDrives configures a Server to serve a virtual '/' for windows that lists all drives
136func WindowsRootEnumeratesDrives() ServerOption {
137 return func(s *Server) error {
138 s.winRoot = true
139 return nil
140 }
141}
142
143// WithAllocator enable the allocator.
144// After processing a packet we keep in memory the allocated slices
145// and we reuse them for new packets.
146// The allocator is experimental
147func WithAllocator() ServerOption {
148 return func(s *Server) error {
149 alloc := newAllocator()
150 s.pktMgr.alloc = alloc
151 s.conn.alloc = alloc
152 return nil
153 }
154}
155
156// WithServerWorkingDirectory sets a working directory to use as base
157// for relative paths.
158// If unset the default is current working directory (os.Getwd).
159func WithServerWorkingDirectory(workDir string) ServerOption {
160 return func(s *Server) error {
161 s.workDir = cleanPath(workDir)
162 return nil
163 }
164}
165
166// WithMaxTxPacket sets the maximum size of the payload returned to the client,
167// measured in bytes. The default value is 32768 bytes, and this option
168// can only be used to increase it. Setting this option to a larger value
169// should be safe, because the client decides the size of the requested payload.
170//
171// The default maximum packet size is 32768 bytes.
172func WithMaxTxPacket(size uint32) ServerOption {
173 return func(s *Server) error {
174 if size < defaultMaxTxPacket {
175 return errors.New("size must be greater than or equal to 32768")
176 }
177
178 s.maxTxPacket = size
179
180 return nil
181 }
182}
183
184type rxPacket struct {
185 pktType fxp
186 pktBytes []byte
187}
188
189// Up to N parallel servers
190func (svr *Server) sftpServerWorker(pktChan chan orderedRequest) error {
191 for pkt := range pktChan {
192 // readonly checks
193 readonly := true
194 switch pkt := pkt.requestPacket.(type) {
195 case notReadOnly:
196 readonly = false
197 case *sshFxpOpenPacket:
198 readonly = pkt.readonly()
199 case *sshFxpExtendedPacket:
200 readonly = pkt.readonly()
201 }
202
203 // If server is operating read-only and a write operation is requested,
204 // return permission denied
205 if !readonly && svr.readOnly {
206 svr.pktMgr.readyPacket(
207 svr.pktMgr.newOrderedResponse(statusFromError(pkt.id(), syscall.EPERM), pkt.orderID()),
208 )
209 continue
210 }
211
212 if err := handlePacket(svr, pkt); err != nil {
213 return err
214 }
215 }
216 return nil
217}
218
219func handlePacket(s *Server, p orderedRequest) error {
220 var rpkt responsePacket
221 orderID := p.orderID()
222 switch p := p.requestPacket.(type) {
223 case *sshFxInitPacket:
224 rpkt = &sshFxVersionPacket{
225 Version: sftpProtocolVersion,
226 Extensions: sftpExtensions,
227 }
228 case *sshFxpStatPacket:
229 // stat the requested file
230 info, err := os.Stat(s.toLocalPath(p.Path))
231 rpkt = &sshFxpStatResponse{
232 ID: p.ID,
233 info: info,
234 }
235 if err != nil {
236 rpkt = statusFromError(p.ID, err)
237 }
238 case *sshFxpLstatPacket:
239 // stat the requested file
240 info, err := s.lstat(s.toLocalPath(p.Path))
241 rpkt = &sshFxpStatResponse{
242 ID: p.ID,
243 info: info,
244 }
245 if err != nil {
246 rpkt = statusFromError(p.ID, err)
247 }
248 case *sshFxpFstatPacket:
249 f, ok := s.getHandle(p.Handle)
250 var err error = EBADF
251 var info os.FileInfo
252 if ok {
253 info, err = f.Stat()
254 rpkt = &sshFxpStatResponse{
255 ID: p.ID,
256 info: info,
257 }
258 }
259 if err != nil {
260 rpkt = statusFromError(p.ID, err)
261 }
262 case *sshFxpMkdirPacket:
263 // TODO FIXME: ignore flags field
264 err := os.Mkdir(s.toLocalPath(p.Path), 0o755)
265 rpkt = statusFromError(p.ID, err)
266 case *sshFxpRmdirPacket:
267 err := os.Remove(s.toLocalPath(p.Path))
268 rpkt = statusFromError(p.ID, err)
269 case *sshFxpRemovePacket:
270 err := os.Remove(s.toLocalPath(p.Filename))
271 rpkt = statusFromError(p.ID, err)
272 case *sshFxpRenamePacket:
273 err := os.Rename(s.toLocalPath(p.Oldpath), s.toLocalPath(p.Newpath))
274 rpkt = statusFromError(p.ID, err)
275 case *sshFxpSymlinkPacket:
276 err := os.Symlink(s.toLocalPath(p.Targetpath), s.toLocalPath(p.Linkpath))
277 rpkt = statusFromError(p.ID, err)
278 case *sshFxpClosePacket:
279 rpkt = statusFromError(p.ID, s.closeHandle(p.Handle))
280 case *sshFxpReadlinkPacket:
281 f, err := os.Readlink(s.toLocalPath(p.Path))
282 rpkt = &sshFxpNamePacket{
283 ID: p.ID,
284 NameAttrs: []*sshFxpNameAttr{
285 {
286 Name: f,
287 LongName: f,
288 Attrs: emptyFileStat,
289 },
290 },
291 }
292 if err != nil {
293 rpkt = statusFromError(p.ID, err)
294 }
295 case *sshFxpRealpathPacket:
296 f, err := filepath.Abs(s.toLocalPath(p.Path))
297 f = cleanPath(f)
298 rpkt = &sshFxpNamePacket{
299 ID: p.ID,
300 NameAttrs: []*sshFxpNameAttr{
301 {
302 Name: f,
303 LongName: f,
304 Attrs: emptyFileStat,
305 },
306 },
307 }
308 if err != nil {
309 rpkt = statusFromError(p.ID, err)
310 }
311 case *sshFxpOpendirPacket:
312 lp := s.toLocalPath(p.Path)
313
314 if stat, err := s.stat(lp); err != nil {
315 rpkt = statusFromError(p.ID, err)
316 } else if !stat.IsDir() {
317 rpkt = statusFromError(p.ID, &os.PathError{
318 Path: lp, Err: syscall.ENOTDIR,
319 })
320 } else {
321 rpkt = (&sshFxpOpenPacket{
322 ID: p.ID,
323 Path: p.Path,
324 Pflags: sshFxfRead,
325 }).respond(s)
326 }
327 case *sshFxpReadPacket:
328 var err error = EBADF
329 f, ok := s.getHandle(p.Handle)
330 if ok {
331 err = nil
332 data := p.getDataSlice(s.pktMgr.alloc, orderID, s.maxTxPacket)
333 n, _err := f.ReadAt(data, int64(p.Offset))
334 if _err != nil && (_err != io.EOF || n == 0) {
335 err = _err
336 }
337 rpkt = &sshFxpDataPacket{
338 ID: p.ID,
339 Length: uint32(n),
340 Data: data[:n],
341 // do not use data[:n:n] here to clamp the capacity, we allocated extra capacity above to avoid reallocations
342 }
343 }
344 if err != nil {
345 rpkt = statusFromError(p.ID, err)
346 }
347
348 case *sshFxpWritePacket:
349 f, ok := s.getHandle(p.Handle)
350 var err error = EBADF
351 if ok {
352 _, err = f.WriteAt(p.Data, int64(p.Offset))
353 }
354 rpkt = statusFromError(p.ID, err)
355 case *sshFxpExtendedPacket:
356 if p.SpecificPacket == nil {
357 rpkt = statusFromError(p.ID, ErrSSHFxOpUnsupported)
358 } else {
359 rpkt = p.respond(s)
360 }
361 case serverRespondablePacket:
362 rpkt = p.respond(s)
363 default:
364 return fmt.Errorf("unexpected packet type %T", p)
365 }
366
367 s.pktMgr.readyPacket(s.pktMgr.newOrderedResponse(rpkt, orderID))
368 return nil
369}
370
371// Serve serves SFTP connections until the streams stop or the SFTP subsystem
372// is stopped. It returns nil if the server exits cleanly.
373func (svr *Server) Serve() error {
374 defer func() {
375 if svr.pktMgr.alloc != nil {
376 svr.pktMgr.alloc.Free()
377 }
378 }()
379 var wg sync.WaitGroup
380 runWorker := func(ch chan orderedRequest) {
381 wg.Add(1)
382 go func() {
383 defer wg.Done()
384 if err := svr.sftpServerWorker(ch); err != nil {
385 svr.conn.Close() // shuts down recvPacket
386 }
387 }()
388 }
389 pktChan := svr.pktMgr.workerChan(runWorker)
390
391 var err error
392 var pkt requestPacket
393 var pktType fxp
394 var pktBytes []byte
395 for {
396 pktType, pktBytes, err = svr.serverConn.recvPacket(svr.pktMgr.getNextOrderID())
397 if err != nil {
398 // Check whether the connection terminated cleanly in-between packets.
399 if err == io.EOF {
400 err = nil
401 }
402 // we don't care about releasing allocated pages here, the server will quit and the allocator freed
403 break
404 }
405
406 pkt, err = makePacket(rxPacket{pktType, pktBytes})
407 if err != nil {
408 switch {
409 case errors.Is(err, errUnknownExtendedPacket):
410 //if err := svr.serverConn.sendError(pkt, ErrSshFxOpUnsupported); err != nil {
411 // debug("failed to send err packet: %v", err)
412 // svr.conn.Close() // shuts down recvPacket
413 // break
414 //}
415 default:
416 debug("makePacket err: %v", err)
417 svr.conn.Close() // shuts down recvPacket
418 break
419 }
420 }
421
422 pktChan <- svr.pktMgr.newOrderedRequest(pkt)
423 }
424
425 close(pktChan) // shuts down sftpServerWorkers
426 wg.Wait() // wait for all workers to exit
427
428 // close any still-open files
429 for handle, file := range svr.openFiles {
430 fmt.Fprintf(svr.debugStream, "sftp server file with handle %q left open: %v\n", handle, file.Name())
431 file.Close()
432 }
433 return err // error from recvPacket
434}
435
436type ider interface {
437 id() uint32
438}
439
440// The init packet has no ID, so we just return a zero-value ID
441func (p *sshFxInitPacket) id() uint32 { return 0 }
442
443type sshFxpStatResponse struct {
444 ID uint32
445 info os.FileInfo
446}
447
448func (p *sshFxpStatResponse) marshalPacket() ([]byte, []byte, error) {
449 l := 4 + 1 + 4 // uint32(length) + byte(type) + uint32(id)
450
451 b := make([]byte, 4, l)
452 b = append(b, sshFxpAttrs)
453 b = marshalUint32(b, p.ID)
454
455 var payload []byte
456 payload = marshalFileInfo(payload, p.info)
457
458 return b, payload, nil
459}
460
461func (p *sshFxpStatResponse) MarshalBinary() ([]byte, error) {
462 header, payload, err := p.marshalPacket()
463 return append(header, payload...), err
464}
465
466var emptyFileStat = []interface{}{uint32(0)}
467
468func (p *sshFxpOpenPacket) readonly() bool {
469 return !p.hasPflags(sshFxfWrite)
470}
471
472func (p *sshFxpOpenPacket) hasPflags(flags ...uint32) bool {
473 for _, f := range flags {
474 if p.Pflags&f == 0 {
475 return false
476 }
477 }
478 return true
479}
480
481func (p *sshFxpOpenPacket) respond(svr *Server) responsePacket {
482 var osFlags int
483 if p.hasPflags(sshFxfRead, sshFxfWrite) {
484 osFlags |= os.O_RDWR
485 } else if p.hasPflags(sshFxfWrite) {
486 osFlags |= os.O_WRONLY
487 } else if p.hasPflags(sshFxfRead) {
488 osFlags |= os.O_RDONLY
489 } else {
490 // how are they opening?
491 return statusFromError(p.ID, syscall.EINVAL)
492 }
493
494 // Don't use O_APPEND flag as it conflicts with WriteAt.
495 // The sshFxfAppend flag is a no-op here as the client sends the offsets.
496
497 if p.hasPflags(sshFxfCreat) {
498 osFlags |= os.O_CREATE
499 }
500 if p.hasPflags(sshFxfTrunc) {
501 osFlags |= os.O_TRUNC
502 }
503 if p.hasPflags(sshFxfExcl) {
504 osFlags |= os.O_EXCL
505 }
506
507 mode := os.FileMode(0o644)
508 // Like OpenSSH, we only handle permissions here, and only when the file is being created.
509 // Otherwise, the permissions are ignored.
510 if p.Flags&sshFileXferAttrPermissions != 0 {
511 fs, err := p.unmarshalFileStat(p.Flags)
512 if err != nil {
513 return statusFromError(p.ID, err)
514 }
515 mode = fs.FileMode() & os.ModePerm
516 }
517
518 f, err := svr.openfile(svr.toLocalPath(p.Path), osFlags, mode)
519 if err != nil {
520 return statusFromError(p.ID, err)
521 }
522
523 handle := svr.nextHandle(f)
524 return &sshFxpHandlePacket{ID: p.ID, Handle: handle}
525}
526
527func (p *sshFxpReaddirPacket) respond(svr *Server) responsePacket {
528 f, ok := svr.getHandle(p.Handle)
529 if !ok {
530 return statusFromError(p.ID, EBADF)
531 }
532
533 dirents, err := f.Readdir(128)
534 if err != nil {
535 return statusFromError(p.ID, err)
536 }
537
538 idLookup := osIDLookup{}
539
540 ret := &sshFxpNamePacket{ID: p.ID}
541 for _, dirent := range dirents {
542 ret.NameAttrs = append(ret.NameAttrs, &sshFxpNameAttr{
543 Name: dirent.Name(),
544 LongName: runLs(idLookup, dirent),
545 Attrs: []interface{}{dirent},
546 })
547 }
548 return ret
549}
550
551func (p *sshFxpSetstatPacket) respond(svr *Server) responsePacket {
552 path := svr.toLocalPath(p.Path)
553
554 debug("setstat name %q", path)
555
556 fs, err := p.unmarshalFileStat(p.Flags)
557
558 if err == nil && (p.Flags&sshFileXferAttrSize) != 0 {
559 err = os.Truncate(path, int64(fs.Size))
560 }
561 if err == nil && (p.Flags&sshFileXferAttrPermissions) != 0 {
562 err = os.Chmod(path, fs.FileMode())
563 }
564 if err == nil && (p.Flags&sshFileXferAttrUIDGID) != 0 {
565 err = os.Chown(path, int(fs.UID), int(fs.GID))
566 }
567 if err == nil && (p.Flags&sshFileXferAttrACmodTime) != 0 {
568 err = os.Chtimes(path, fs.AccessTime(), fs.ModTime())
569 }
570
571 return statusFromError(p.ID, err)
572}
573
574func (p *sshFxpFsetstatPacket) respond(svr *Server) responsePacket {
575 f, ok := svr.getHandle(p.Handle)
576 if !ok {
577 return statusFromError(p.ID, EBADF)
578 }
579
580 path := f.Name()
581
582 debug("fsetstat name %q", path)
583
584 fs, err := p.unmarshalFileStat(p.Flags)
585
586 if err == nil && (p.Flags&sshFileXferAttrSize) != 0 {
587 err = f.Truncate(int64(fs.Size))
588 }
589 if err == nil && (p.Flags&sshFileXferAttrPermissions) != 0 {
590 err = f.Chmod(fs.FileMode())
591 }
592 if err == nil && (p.Flags&sshFileXferAttrUIDGID) != 0 {
593 err = f.Chown(int(fs.UID), int(fs.GID))
594 }
595 if err == nil && (p.Flags&sshFileXferAttrACmodTime) != 0 {
596 type chtimer interface {
597 Chtimes(atime, mtime time.Time) error
598 }
599
600 switch f := interface{}(f).(type) {
601 case chtimer:
602 // future-compatible, for when/if *os.File supports Chtimes.
603 err = f.Chtimes(fs.AccessTime(), fs.ModTime())
604 default:
605 err = os.Chtimes(path, fs.AccessTime(), fs.ModTime())
606 }
607 }
608
609 return statusFromError(p.ID, err)
610}
611
612func statusFromError(id uint32, err error) *sshFxpStatusPacket {
613 ret := &sshFxpStatusPacket{
614 ID: id,
615 StatusError: StatusError{
616 // sshFXOk = 0
617 // sshFXEOF = 1
618 // sshFXNoSuchFile = 2 ENOENT
619 // sshFXPermissionDenied = 3
620 // sshFXFailure = 4
621 // sshFXBadMessage = 5
622 // sshFXNoConnection = 6
623 // sshFXConnectionLost = 7
624 // sshFXOPUnsupported = 8
625 Code: sshFxOk,
626 },
627 }
628 if err == nil {
629 return ret
630 }
631
632 debug("statusFromError: error is %T %#v", err, err)
633 ret.StatusError.Code = sshFxFailure
634 ret.StatusError.msg = err.Error()
635
636 if os.IsNotExist(err) {
637 ret.StatusError.Code = sshFxNoSuchFile
638 return ret
639 }
640 if code, ok := translateSyscallError(err); ok {
641 ret.StatusError.Code = code
642 return ret
643 }
644
645 if errors.Is(err, io.EOF) {
646 ret.StatusError.Code = sshFxEOF
647 return ret
648 }
649
650 var e fxerr
651 if errors.As(err, &e) {
652 ret.StatusError.Code = uint32(e)
653 return ret
654 }
655
656 return ret
657}