mirror of https://github.com/pkg/sftp.git
				
				
				
			
		
			
				
	
	
		
			305 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			305 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Go
		
	
	
	
| 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, syscall.EBADF
 | |
| 		}
 | |
| 		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, syscall.EBADF
 | |
| 		}
 | |
| 		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
 | |
| }
 |