goftp-server/conn.go
2020-03-31 20:45:04 +10:00

308 lines
7.9 KiB
Go

// Copyright 2018 The goftp Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package server
import (
"bufio"
"bytes"
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"fmt"
"io"
"log"
mrand "math/rand"
"net"
"path/filepath"
"runtime"
"strconv"
"strings"
)
const (
defaultWelcomeMessage = "Welcome to the Go FTP Server"
)
// Conn represents a connection between ftp client and the server
type Conn struct {
conn net.Conn
controlReader *bufio.Reader
controlWriter *bufio.Writer
dataConn DataSocket
driver Driver
auth Auth
logger Logger
server *Server
tlsConfig *tls.Config
sessionID string
curDir string
reqUser string
user string
renameFrom string
lastFilePos int64
appendData bool
closed bool
tls bool
}
// RemoteAddr returns the remote ftp client's address
func (conn *Conn) RemoteAddr() net.Addr {
return conn.conn.RemoteAddr()
}
// LoginUser returns the login user name if login
func (conn *Conn) LoginUser() string {
return conn.user
}
// IsLogin returns if user has login
func (conn *Conn) IsLogin() bool {
return len(conn.user) > 0
}
// PublicIP returns the public ip of the server
func (conn *Conn) PublicIP() string {
return conn.server.PublicIP
}
func (conn *Conn) passiveListenIP() string {
var listenIP string
if len(conn.PublicIP()) > 0 {
listenIP = conn.PublicIP()
} else {
listenIP = conn.conn.LocalAddr().(*net.TCPAddr).IP.String()
}
if listenIP == "::1" {
return listenIP
}
lastIdx := strings.LastIndex(listenIP, ":")
if lastIdx <= 0 {
return listenIP
}
return listenIP[:lastIdx]
}
// PassivePort returns the port which could be used by passive mode.
func (conn *Conn) PassivePort() int {
if len(conn.server.PassivePorts) > 0 {
portRange := strings.Split(conn.server.PassivePorts, "-")
if len(portRange) != 2 {
log.Println("empty port")
return 0
}
minPort, _ := strconv.Atoi(strings.TrimSpace(portRange[0]))
maxPort, _ := strconv.Atoi(strings.TrimSpace(portRange[1]))
return minPort + mrand.Intn(maxPort-minPort)
}
// let system automatically chose one port
return 0
}
// returns a random 20 char string that can be used as a unique session ID
func newSessionID() string {
hash := sha256.New()
_, err := io.CopyN(hash, rand.Reader, 50)
if err != nil {
return "????????????????????"
}
md := hash.Sum(nil)
mdStr := hex.EncodeToString(md)
return mdStr[0:20]
}
// Serve starts an endless loop that reads FTP commands from the client and
// responds appropriately. terminated is a channel that will receive a true
// message when the connection closes. This loop will be running inside a
// goroutine, so use this channel to be notified when the connection can be
// cleaned up.
func (conn *Conn) Serve() {
conn.logger.Print(conn.sessionID, "Connection Established")
// send welcome
conn.writeMessage(220, conn.server.WelcomeMessage)
// read commands
for {
line, err := conn.controlReader.ReadString('\n')
if err != nil {
if err != io.EOF {
conn.logger.Print(conn.sessionID, fmt.Sprint("read error:", err))
}
break
}
conn.receiveLine(line)
// QUIT command closes connection, break to avoid error on reading from
// closed socket
if conn.closed == true {
break
}
}
conn.Close()
conn.logger.Print(conn.sessionID, "Connection Terminated")
}
// Close will manually close this connection, even if the client isn't ready.
func (conn *Conn) Close() {
conn.conn.Close()
conn.closed = true
conn.reqUser = ""
conn.user = ""
if conn.dataConn != nil {
conn.dataConn.Close()
conn.dataConn = nil
}
}
// Driver will expose conn to the driver
func (conn *Conn) Driver() Driver {
return conn.driver
}
func (conn *Conn) upgradeToTLS() error {
conn.logger.Print(conn.sessionID, "Upgrading connectiion to TLS")
tlsConn := tls.Server(conn.conn, conn.tlsConfig)
err := tlsConn.Handshake()
if err == nil {
conn.conn = tlsConn
conn.controlReader = bufio.NewReader(tlsConn)
conn.controlWriter = bufio.NewWriter(tlsConn)
conn.tls = true
}
return err
}
// receiveLine accepts a single line FTP command and co-ordinates an
// appropriate response.
func (conn *Conn) receiveLine(line string) {
defer func() {
if e := recover(); e != nil {
var buf bytes.Buffer
fmt.Fprintf(&buf, "Handler crashed with error: %v", e)
for i := 1; ; i++ {
_, file, line, ok := runtime.Caller(i)
if !ok {
break
} else {
fmt.Fprintf(&buf, "\n")
}
fmt.Fprintf(&buf, "%v:%v", file, line)
}
conn.logger.Print(conn.sessionID, buf.String())
}
}()
command, param := conn.parseLine(line)
conn.logger.PrintCommand(conn.sessionID, command, param)
cmdObj := commands[strings.ToUpper(command)]
if cmdObj == nil {
conn.writeMessage(500, "Command not found")
return
}
if cmdObj.RequireParam() && param == "" {
conn.writeMessage(553, "action aborted, required param missing")
} else if cmdObj.RequireAuth() && conn.user == "" {
conn.writeMessage(530, "not logged in")
} else {
cmdObj.Execute(conn, param)
}
}
func (conn *Conn) parseLine(line string) (string, string) {
params := strings.SplitN(strings.Trim(line, "\r\n"), " ", 2)
if len(params) == 1 {
return params[0], ""
}
return params[0], strings.TrimSpace(params[1])
}
// writeMessage will send a standard FTP response back to the client.
func (conn *Conn) writeMessage(code int, message string) (wrote int, err error) {
conn.logger.PrintResponse(conn.sessionID, code, message)
line := fmt.Sprintf("%d %s\r\n", code, message)
wrote, err = conn.controlWriter.WriteString(line)
conn.controlWriter.Flush()
return
}
// writeMessage will send a standard FTP response back to the client.
func (conn *Conn) writeMessageMultiline(code int, message string) (wrote int, err error) {
conn.logger.PrintResponse(conn.sessionID, code, message)
line := fmt.Sprintf("%d-%s\r\n%d END\r\n", code, message, code)
wrote, err = conn.controlWriter.WriteString(line)
conn.controlWriter.Flush()
return
}
// buildPath takes a client supplied path or filename and generates a safe
// absolute path within their account sandbox.
//
// buildpath("/")
// => "/"
// buildpath("one.txt")
// => "/one.txt"
// buildpath("/files/two.txt")
// => "/files/two.txt"
// buildpath("files/two.txt")
// => "/files/two.txt"
// buildpath("/../../../../etc/passwd")
// => "/etc/passwd"
//
// The driver implementation is responsible for deciding how to treat this path.
// Obviously they MUST NOT just read the path off disk. The probably want to
// prefix the path with something to scope the users access to a sandbox.
func (conn *Conn) buildPath(filename string) (fullPath string) {
if len(filename) > 0 && filename[0:1] == "/" {
fullPath = filepath.Clean(filename)
} else if len(filename) > 0 && filename != "-a" {
fullPath = filepath.Clean(conn.curDir + "/" + filename)
} else {
fullPath = filepath.Clean(conn.curDir)
}
fullPath = strings.Replace(fullPath, "//", "/", -1)
fullPath = strings.Replace(fullPath, string(filepath.Separator), "/", -1)
return
}
// sendOutofbandData will send a string to the client via the currently open
// data socket. Assumes the socket is open and ready to be used.
func (conn *Conn) sendOutofbandData(data []byte) {
bytes := len(data)
if conn.dataConn != nil {
conn.dataConn.Write(data)
conn.dataConn.Close()
conn.dataConn = nil
}
message := "Closing data connection, sent " + strconv.Itoa(bytes) + " bytes"
conn.writeMessage(226, message)
}
func (conn *Conn) sendOutofBandDataWriter(data io.ReadCloser) error {
conn.lastFilePos = 0
bytes, err := io.Copy(conn.dataConn, data)
if err != nil {
conn.dataConn.Close()
conn.dataConn = nil
return err
}
message := "Closing data connection, sent " + strconv.Itoa(int(bytes)) + " bytes"
conn.writeMessage(226, message)
conn.dataConn.Close()
conn.dataConn = nil
return nil
}
func (conn *Conn) changeCurDir(path string) error {
conn.curDir = path
return nil
}