| package sftp |
| |
| import ( |
| "io" |
| "os" |
| "path" |
| "path/filepath" |
| "sync" |
| "syscall" |
| |
| "github.com/pkg/errors" |
| ) |
| |
| // Request contains the data and state for the incoming service request. |
| type Request struct { |
| // Get, Put, SetStat, Stat, Rename, Remove |
| // Rmdir, Mkdir, List, Readlink, Symlink |
| Method string |
| Filepath string |
| Attrs []byte // convert to sub-struct |
| Target string // for renames and sym-links |
| // packet data |
| pkt_id uint32 |
| packets chan packet_data |
| // reader/writer/readdir from handlers |
| stateLock *sync.RWMutex |
| state *state |
| } |
| |
| type state struct { |
| writerAt io.WriterAt |
| readerAt io.ReaderAt |
| endofdir bool // need to track when to send EOF for readdir |
| } |
| |
| type packet_data struct { |
| id uint32 |
| data []byte |
| length uint32 |
| offset int64 |
| } |
| |
| // New Request initialized based on packet data |
| func requestFromPacket(pkt hasPath) Request { |
| method := requestMethod(pkt) |
| request := NewRequest(method, pkt.getPath()) |
| request.pkt_id = pkt.id() |
| switch p := pkt.(type) { |
| case *sshFxpSetstatPacket: |
| request.Attrs = p.Attrs.([]byte) |
| case *sshFxpRenamePacket: |
| request.Target = filepath.Clean(p.Newpath) |
| case *sshFxpSymlinkPacket: |
| request.Target = filepath.Clean(p.Linkpath) |
| } |
| return request |
| } |
| |
| // NewRequest creates a new Request object. |
| func NewRequest(method, path string) Request { |
| request := Request{Method: method, Filepath: filepath.Clean(path)} |
| request.packets = make(chan packet_data, sftpServerWorkerCount) |
| request.state = &state{} |
| request.stateLock = &sync.RWMutex{} |
| return request |
| } |
| |
| // manage state |
| func (r Request) setState(s interface{}) { |
| r.stateLock.Lock() |
| defer r.stateLock.Unlock() |
| switch s := s.(type) { |
| case io.WriterAt: |
| r.state.writerAt = s |
| case io.ReaderAt: |
| r.state.readerAt = s |
| case bool: |
| r.state.endofdir = s |
| } |
| } |
| |
| func (r Request) getWriter() io.WriterAt { |
| r.stateLock.RLock() |
| defer r.stateLock.RUnlock() |
| return r.state.writerAt |
| } |
| |
| func (r Request) getReader() io.ReaderAt { |
| r.stateLock.RLock() |
| defer r.stateLock.RUnlock() |
| return r.state.readerAt |
| } |
| |
| func (r Request) getEOD() bool { |
| r.stateLock.RLock() |
| defer r.stateLock.RUnlock() |
| return r.state.endofdir |
| } |
| |
| // Close reader/writer if possible |
| func (r Request) close() { |
| rd := r.getReader() |
| if c, ok := rd.(io.Closer); ok { |
| c.Close() |
| } |
| wt := r.getWriter() |
| if c, ok := wt.(io.Closer); ok { |
| c.Close() |
| } |
| } |
| |
| // push packet_data into fifo |
| func (r Request) pushPacket(pd packet_data) { |
| r.packets <- pd |
| } |
| |
| // pop packet_data into fifo |
| func (r *Request) popPacket() packet_data { |
| return <-r.packets |
| } |
| |
| // called from worker to handle packet/request |
| func (r Request) handle(handlers Handlers) (responsePacket, error) { |
| var err error |
| var rpkt responsePacket |
| switch r.Method { |
| case "Get": |
| rpkt, err = fileget(handlers.FileGet, r) |
| case "Put": // add "Append" to this to handle append only file writes |
| rpkt, err = fileput(handlers.FilePut, r) |
| case "SetStat", "Rename", "Rmdir", "Mkdir", "Symlink", "Remove": |
| rpkt, err = filecmd(handlers.FileCmd, r) |
| case "List", "Stat", "Readlink": |
| rpkt, err = fileinfo(handlers.FileInfo, r) |
| } |
| return rpkt, err |
| } |
| |
| // wrap FileReader handler |
| func fileget(h FileReader, r Request) (responsePacket, error) { |
| var err error |
| reader := r.getReader() |
| if reader == nil { |
| reader, err = h.Fileread(r) |
| if err != nil { |
| return nil, err |
| } |
| r.setState(reader) |
| } |
| |
| pd := r.popPacket() |
| data := make([]byte, clamp(pd.length, maxTxPacket)) |
| n, err := reader.ReadAt(data, pd.offset) |
| if err != nil && (err != io.EOF || n == 0) { |
| return nil, err |
| } |
| return &sshFxpDataPacket{ |
| ID: pd.id, |
| Length: uint32(n), |
| Data: data[:n], |
| }, nil |
| } |
| |
| // wrap FileWriter handler |
| func fileput(h FileWriter, r Request) (responsePacket, error) { |
| var err error |
| writer := r.getWriter() |
| if writer == nil { |
| writer, err = h.Filewrite(r) |
| if err != nil { |
| return nil, err |
| } |
| r.setState(writer) |
| } |
| |
| pd := r.popPacket() |
| _, err = writer.WriteAt(pd.data, pd.offset) |
| if err != nil { |
| return nil, err |
| } |
| return &sshFxpStatusPacket{ |
| ID: pd.id, |
| StatusError: StatusError{ |
| Code: ssh_FX_OK, |
| }}, nil |
| } |
| |
| // wrap FileCmder handler |
| func filecmd(h FileCmder, r Request) (responsePacket, error) { |
| err := h.Filecmd(r) |
| if err != nil { |
| return nil, err |
| } |
| return &sshFxpStatusPacket{ |
| ID: r.pkt_id, |
| StatusError: StatusError{ |
| Code: ssh_FX_OK, |
| }}, nil |
| } |
| |
| // wrap FileInfoer handler |
| func fileinfo(h FileInfoer, r Request) (responsePacket, error) { |
| if r.getEOD() { |
| return nil, io.EOF |
| } |
| finfo, err := h.Fileinfo(r) |
| if err != nil { |
| return nil, err |
| } |
| |
| switch r.Method { |
| case "List": |
| pd := r.popPacket() |
| dirname := path.Base(r.Filepath) |
| ret := &sshFxpNamePacket{ID: pd.id} |
| for _, fi := range finfo { |
| ret.NameAttrs = append(ret.NameAttrs, sshFxpNameAttr{ |
| Name: fi.Name(), |
| LongName: runLs(dirname, fi), |
| Attrs: []interface{}{fi}, |
| }) |
| } |
| r.setState(true) |
| return ret, nil |
| case "Stat": |
| if len(finfo) == 0 { |
| err = &os.PathError{Op: "stat", Path: r.Filepath, |
| Err: syscall.ENOENT} |
| return nil, err |
| } |
| return &sshFxpStatResponse{ |
| ID: r.pkt_id, |
| info: finfo[0], |
| }, nil |
| case "Readlink": |
| if len(finfo) == 0 { |
| err = &os.PathError{Op: "readlink", Path: r.Filepath, |
| Err: syscall.ENOENT} |
| return nil, err |
| } |
| filename := finfo[0].Name() |
| return &sshFxpNamePacket{ |
| ID: r.pkt_id, |
| NameAttrs: []sshFxpNameAttr{{ |
| Name: filename, |
| LongName: filename, |
| Attrs: emptyFileStat, |
| }}, |
| }, nil |
| } |
| return nil, err |
| } |
| |
| // file data for additional read/write packets |
| func (r *Request) update(p hasHandle) error { |
| pd := packet_data{id: p.id()} |
| switch p := p.(type) { |
| case *sshFxpReadPacket: |
| r.Method = "Get" |
| pd.length = p.Len |
| pd.offset = int64(p.Offset) |
| case *sshFxpWritePacket: |
| r.Method = "Put" |
| pd.data = p.Data |
| pd.length = p.Length |
| pd.offset = int64(p.Offset) |
| case *sshFxpReaddirPacket: |
| r.Method = "List" |
| case *sshFxpFsetstatPacket: |
| r.Method = "Setstat" |
| r.Attrs = p.Attrs.([]byte) |
| case *sshFxpFstatPacket: |
| r.Method = "Stat" |
| default: |
| return errors.Errorf("unexpected packet type %T", p) |
| } |
| r.pushPacket(pd) |
| return nil |
| } |
| |
| // init attributes of request object from packet data |
| func requestMethod(p hasPath) (method string) { |
| switch p.(type) { |
| case *sshFxpOpenPacket, *sshFxpOpendirPacket: |
| method = "Open" |
| case *sshFxpSetstatPacket: |
| method = "Setstat" |
| case *sshFxpRenamePacket: |
| method = "Rename" |
| case *sshFxpSymlinkPacket: |
| method = "Symlink" |
| case *sshFxpRemovePacket: |
| method = "Remove" |
| case *sshFxpStatPacket, *sshFxpLstatPacket: |
| method = "Stat" |
| case *sshFxpRmdirPacket: |
| method = "Rmdir" |
| case *sshFxpReadlinkPacket: |
| method = "Readlink" |
| case *sshFxpMkdirPacket: |
| method = "Mkdir" |
| } |
| return method |
| } |