diff --git a/go.mod b/go.mod index 79596aa3a..750fe93f1 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/djherbis/times v1.0.1 github.com/dropbox/dropbox-sdk-go-unofficial v4.1.0+incompatible github.com/go-ini/ini v1.37.0 // indirect + github.com/goftp/server v0.0.0-20180914132916-1fd52c8552f1 github.com/golang/lint v0.0.0-20180702182130-06c8688daad7 // indirect github.com/golang/protobuf v1.1.0 // indirect github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 // indirect diff --git a/go.sum b/go.sum index 0975da4b2..337931e28 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,8 @@ github.com/dropbox/dropbox-sdk-go-unofficial v4.1.0+incompatible h1:ZFvUIiBbGhDY github.com/dropbox/dropbox-sdk-go-unofficial v4.1.0+incompatible/go.mod h1:lr+LhMM3F6Y3lW1T9j2U5l7QeuWm87N9+PPXo3yH4qY= github.com/go-ini/ini v1.37.0 h1:/FpMfveJbc7ExTTDgT5nL9Vw+aZdst/c2dOxC931U+M= github.com/go-ini/ini v1.37.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/goftp/server v0.0.0-20180914132916-1fd52c8552f1 h1:WjgeEHEDLGx56ndxS6FYi6qFjZGajSVHPuEPdpJ60cI= +github.com/goftp/server v0.0.0-20180914132916-1fd52c8552f1/go.mod h1:k/SS6VWkxY7dHPhoMQ8IdRu8L4lQtmGbhyXGg+vCnXE= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7 h1:2hRPrmiwPrp3fQX967rNJIhQPtiGXdlQWAxKbKw3VHA= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/protobuf v1.1.0 h1:0iH4Ffd/meGoXqF2lSAhZHt8X+cPgkfn/cb6Cce5Vpc= diff --git a/vendor/github.com/goftp/server/.gitignore b/vendor/github.com/goftp/server/.gitignore new file mode 100644 index 000000000..fae1bc987 --- /dev/null +++ b/vendor/github.com/goftp/server/.gitignore @@ -0,0 +1,2 @@ +testdata +coverage.txt \ No newline at end of file diff --git a/vendor/github.com/goftp/server/LICENSE b/vendor/github.com/goftp/server/LICENSE new file mode 100644 index 000000000..091e8c013 --- /dev/null +++ b/vendor/github.com/goftp/server/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2018 Goftp Authors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/goftp/server/README.md b/vendor/github.com/goftp/server/README.md new file mode 100644 index 000000000..792aaf7d2 --- /dev/null +++ b/vendor/github.com/goftp/server/README.md @@ -0,0 +1,92 @@ +# server + +[![CircleCI](https://circleci.com/gh/goftp/server.svg?style=shield)](https://circleci.com/gh/goftp/server) +[![](https://goreportcard.com/badge/github.com/goftp/server)](https://goreportcard.com/report/github.com/goftp/server) +[![codecov](https://codecov.io/gh/goftp/server/branch/master/graph/badge.svg)](https://codecov.io/gh/goftp/server) + +A FTP server framework forked from [github.com/yob/graval](http://github.com/yob/graval) and changed a lot. + +Full documentation for the package is available on [godoc](http://godoc.org/github.com/goftp/server) + +## Version + + v0.2.3 + +## Installation + + go get github.com/goftp/server + +## Usage + +To boot a FTP server you will need to provide a driver that speaks to +your persistence layer - the required driver contract is in [the +documentation](http://godoc.org/github.com/goftp/server). + +Look at the [file driver](https://github.com/goftp/file-driver) to see +an example of how to build a backend. + +There is a [sample ftp server](/exampleftpd) as a demo. You can build it with this +command: + + go install github.com/goftp/server/exampleftpd + +Then run it if you have add $GOPATH to your $PATH: + + exampleftpd -root /tmp + +And finally, connect to the server with any FTP client and the following +details: + + host: 127.0.0.1 + port: 2121 + username: admin + password: 123456 + +This uses the file driver mentioned above to serve files. + +## Contributors + +see [https://github.com/goftp/server/graphs/contributors](https://github.com/goftp/server/graphs/contributors) + +## Warning + +FTP is an incredibly insecure protocol. Be careful about forcing users to authenticate +with an username or password that are important. + +## License + +This library is distributed under the terms of the MIT License. See the included file for +more detail. + +## Contributing + +All suggestions and patches welcome, preferably via a git repository I can pull from. +If this library proves useful to you, please let me know. + +## Further Reading + +There are a range of RFCs that together specify the FTP protocol. In chronological +order, the more useful ones are: + +* [http://tools.ietf.org/rfc/rfc959.txt](http://tools.ietf.org/rfc/rfc959.txt) +* [http://tools.ietf.org/rfc/rfc1123.txt](http://tools.ietf.org/rfc/rfc1123.txt) +* [http://tools.ietf.org/rfc/rfc2228.txt](http://tools.ietf.org/rfc/rfc2228.txt) +* [http://tools.ietf.org/rfc/rfc2389.txt](http://tools.ietf.org/rfc/rfc2389.txt) +* [http://tools.ietf.org/rfc/rfc2428.txt](http://tools.ietf.org/rfc/rfc2428.txt) +* [http://tools.ietf.org/rfc/rfc3659.txt](http://tools.ietf.org/rfc/rfc3659.txt) +* [http://tools.ietf.org/rfc/rfc4217.txt](http://tools.ietf.org/rfc/rfc4217.txt) + +For an english summary that's somewhat more legible than the RFCs, and provides +some commentary on what features are actually useful or relevant 24 years after +RFC959 was published: + +* [http://cr.yp.to/ftp.html](http://cr.yp.to/ftp.html) + +For a history lesson, check out Appendix III of RCF959. It lists the preceding +(obsolete) RFC documents that relate to file transfers, including the ye old +RFC114 from 1971, "A File Transfer Protocol" + +This library is heavily based on [em-ftpd](https://github.com/yob/em-ftpd), an FTPd +framework with similar design goals within the ruby and EventMachine ecosystems. It +worked well enough, but you know, callbacks and event loops make me something +something. diff --git a/vendor/github.com/goftp/server/auth.go b/vendor/github.com/goftp/server/auth.go new file mode 100644 index 000000000..159b5131c --- /dev/null +++ b/vendor/github.com/goftp/server/auth.go @@ -0,0 +1,28 @@ +// 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 + +// Auth is an interface to auth your ftp user login. +type Auth interface { + CheckPasswd(string, string) (bool, error) +} + +var ( + _ Auth = &SimpleAuth{} +) + +// SimpleAuth implements Auth interface to provide a memory user login auth +type SimpleAuth struct { + Name string + Password string +} + +// CheckPasswd will check user's password +func (a *SimpleAuth) CheckPasswd(name, pass string) (bool, error) { + if name != a.Name || pass != a.Password { + return false, nil + } + return true, nil +} diff --git a/vendor/github.com/goftp/server/circle.yml b/vendor/github.com/goftp/server/circle.yml new file mode 100644 index 000000000..63c11ef8c --- /dev/null +++ b/vendor/github.com/goftp/server/circle.yml @@ -0,0 +1,14 @@ +dependencies: + override: + - mkdir -p ~/.go_workspace/src/github.com/goftp + - ln -s ${HOME}/${CIRCLE_PROJECT_REPONAME} ${HOME}/.go_workspace/src/github.com/goftp/${CIRCLE_PROJECT_REPONAME} + # './...' is a relative pattern which means all subdirectories + - go get -t -d -v ./... + - go build -v + +test: + override: + # './...' is a relative pattern which means all subdirectories + - go test -v -race -coverprofile=coverage.txt -covermode=atomic + post: + - bash <(curl -s https://codecov.io/bash) diff --git a/vendor/github.com/goftp/server/cmd.go b/vendor/github.com/goftp/server/cmd.go new file mode 100644 index 000000000..6cc622706 --- /dev/null +++ b/vendor/github.com/goftp/server/cmd.go @@ -0,0 +1,1138 @@ +// 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 ( + "fmt" + "log" + "strconv" + "strings" +) + +type Command interface { + IsExtend() bool + RequireParam() bool + RequireAuth() bool + Execute(*Conn, string) +} + +type commandMap map[string]Command + +var ( + commands = commandMap{ + "ADAT": commandAdat{}, + "ALLO": commandAllo{}, + "APPE": commandAppe{}, + "AUTH": commandAuth{}, + "CDUP": commandCdup{}, + "CWD": commandCwd{}, + "CCC": commandCcc{}, + "CONF": commandConf{}, + "DELE": commandDele{}, + "ENC": commandEnc{}, + "EPRT": commandEprt{}, + "EPSV": commandEpsv{}, + "FEAT": commandFeat{}, + "LIST": commandList{}, + "NLST": commandNlst{}, + "MDTM": commandMdtm{}, + "MIC": commandMic{}, + "MKD": commandMkd{}, + "MODE": commandMode{}, + "NOOP": commandNoop{}, + "OPTS": commandOpts{}, + "PASS": commandPass{}, + "PASV": commandPasv{}, + "PBSZ": commandPbsz{}, + "PORT": commandPort{}, + "PROT": commandProt{}, + "PWD": commandPwd{}, + "QUIT": commandQuit{}, + "RETR": commandRetr{}, + "REST": commandRest{}, + "RNFR": commandRnfr{}, + "RNTO": commandRnto{}, + "RMD": commandRmd{}, + "SIZE": commandSize{}, + "STOR": commandStor{}, + "STRU": commandStru{}, + "SYST": commandSyst{}, + "TYPE": commandType{}, + "USER": commandUser{}, + "XCUP": commandCdup{}, + "XCWD": commandCwd{}, + "XPWD": commandPwd{}, + "XRMD": commandRmd{}, + } +) + +// commandAllo responds to the ALLO FTP command. +// +// This is essentially a ping from the client so we just respond with an +// basic OK message. +type commandAllo struct{} + +func (cmd commandAllo) IsExtend() bool { + return false +} + +func (cmd commandAllo) RequireParam() bool { + return false +} + +func (cmd commandAllo) RequireAuth() bool { + return false +} + +func (cmd commandAllo) Execute(conn *Conn, param string) { + conn.writeMessage(202, "Obsolete") +} + +type commandAppe struct{} + +func (cmd commandAppe) IsExtend() bool { + return false +} + +func (cmd commandAppe) RequireParam() bool { + return false +} + +func (cmd commandAppe) RequireAuth() bool { + return true +} + +func (cmd commandAppe) Execute(conn *Conn, param string) { + conn.appendData = true + conn.writeMessage(202, "Obsolete") +} + +type commandOpts struct{} + +func (cmd commandOpts) IsExtend() bool { + return false +} + +func (cmd commandOpts) RequireParam() bool { + return false +} + +func (cmd commandOpts) RequireAuth() bool { + return false +} + +func (cmd commandOpts) Execute(conn *Conn, param string) { + parts := strings.Fields(param) + if len(parts) != 2 { + conn.writeMessage(550, "Unknow params") + return + } + if strings.ToUpper(parts[0]) != "UTF8" { + conn.writeMessage(550, "Unknow params") + return + } + + if strings.ToUpper(parts[1]) == "ON" { + conn.writeMessage(200, "UTF8 mode enabled") + } else { + conn.writeMessage(550, "Unsupported non-utf8 mode") + } +} + +type commandFeat struct{} + +func (cmd commandFeat) IsExtend() bool { + return false +} + +func (cmd commandFeat) RequireParam() bool { + return false +} + +func (cmd commandFeat) RequireAuth() bool { + return false +} + +var ( + feats = "Extensions supported:\n%s" + featCmds = " UTF8\n" +) + +func init() { + for k, v := range commands { + if v.IsExtend() { + featCmds = featCmds + " " + k + "\n" + } + } +} + +func (cmd commandFeat) Execute(conn *Conn, param string) { + conn.writeMessageMultiline(211, conn.server.feats) +} + +// cmdCdup responds to the CDUP FTP command. +// +// Allows the client change their current directory to the parent. +type commandCdup struct{} + +func (cmd commandCdup) IsExtend() bool { + return false +} + +func (cmd commandCdup) RequireParam() bool { + return false +} + +func (cmd commandCdup) RequireAuth() bool { + return true +} + +func (cmd commandCdup) Execute(conn *Conn, param string) { + otherCmd := &commandCwd{} + otherCmd.Execute(conn, "..") +} + +// commandCwd responds to the CWD FTP command. It allows the client to change the +// current working directory. +type commandCwd struct{} + +func (cmd commandCwd) IsExtend() bool { + return false +} + +func (cmd commandCwd) RequireParam() bool { + return true +} + +func (cmd commandCwd) RequireAuth() bool { + return true +} + +func (cmd commandCwd) Execute(conn *Conn, param string) { + path := conn.buildPath(param) + err := conn.driver.ChangeDir(path) + if err == nil { + conn.namePrefix = path + conn.writeMessage(250, "Directory changed to "+path) + } else { + conn.writeMessage(550, fmt.Sprint("Directory change to ", path, " failed: ", err)) + } +} + +// commandDele responds to the DELE FTP command. It allows the client to delete +// a file +type commandDele struct{} + +func (cmd commandDele) IsExtend() bool { + return false +} + +func (cmd commandDele) RequireParam() bool { + return true +} + +func (cmd commandDele) RequireAuth() bool { + return true +} + +func (cmd commandDele) Execute(conn *Conn, param string) { + path := conn.buildPath(param) + err := conn.driver.DeleteFile(path) + if err == nil { + conn.writeMessage(250, "File deleted") + } else { + conn.writeMessage(550, fmt.Sprint("File delete failed: ", err)) + } +} + +// commandEprt responds to the EPRT FTP command. It allows the client to +// request an active data socket with more options than the original PORT +// command. It mainly adds ipv6 support. +type commandEprt struct{} + +func (cmd commandEprt) IsExtend() bool { + return true +} + +func (cmd commandEprt) RequireParam() bool { + return true +} + +func (cmd commandEprt) RequireAuth() bool { + return true +} + +func (cmd commandEprt) Execute(conn *Conn, param string) { + delim := string(param[0:1]) + parts := strings.Split(param, delim) + addressFamily, err := strconv.Atoi(parts[1]) + host := parts[2] + port, err := strconv.Atoi(parts[3]) + if addressFamily != 1 && addressFamily != 2 { + conn.writeMessage(522, "Network protocol not supported, use (1,2)") + return + } + socket, err := newActiveSocket(host, port, conn.logger, conn.sessionID) + if err != nil { + conn.writeMessage(425, "Data connection failed") + return + } + conn.dataConn = socket + conn.writeMessage(200, "Connection established ("+strconv.Itoa(port)+")") +} + +// commandEpsv responds to the EPSV FTP command. It allows the client to +// request a passive data socket with more options than the original PASV +// command. It mainly adds ipv6 support, although we don't support that yet. +type commandEpsv struct{} + +func (cmd commandEpsv) IsExtend() bool { + return true +} + +func (cmd commandEpsv) RequireParam() bool { + return false +} + +func (cmd commandEpsv) RequireAuth() bool { + return true +} + +func (cmd commandEpsv) Execute(conn *Conn, param string) { + addr := conn.passiveListenIP() + lastIdx := strings.LastIndex(addr, ":") + if lastIdx <= 0 { + conn.writeMessage(425, "Data connection failed") + return + } + + socket, err := newPassiveSocket(addr[:lastIdx], conn.PassivePort, conn.logger, conn.sessionID, conn.tlsConfig) + if err != nil { + log.Println(err) + conn.writeMessage(425, "Data connection failed") + return + } + conn.dataConn = socket + msg := fmt.Sprintf("Entering Extended Passive Mode (|||%d|)", socket.Port()) + conn.writeMessage(229, msg) +} + +// commandList responds to the LIST FTP command. It allows the client to retreive +// a detailed listing of the contents of a directory. +type commandList struct{} + +func (cmd commandList) IsExtend() bool { + return false +} + +func (cmd commandList) RequireParam() bool { + return false +} + +func (cmd commandList) RequireAuth() bool { + return true +} + +func (cmd commandList) Execute(conn *Conn, param string) { + path := conn.buildPath(parseListParam(param)) + info, err := conn.driver.Stat(path) + if err != nil { + conn.writeMessage(550, err.Error()) + return + } + + if info == nil { + conn.logger.Printf(conn.sessionID, "%s: no such file or directory.\n", path) + return + } + var files []FileInfo + if info.IsDir() { + err = conn.driver.ListDir(path, func(f FileInfo) error { + files = append(files, f) + return nil + }) + if err != nil { + conn.writeMessage(550, err.Error()) + return + } + } else { + files = append(files, info) + } + + conn.writeMessage(150, "Opening ASCII mode data connection for file list") + conn.sendOutofbandData(listFormatter(files).Detailed()) +} + +func parseListParam(param string) (path string) { + if len(param) == 0 { + path = param + } else { + fields := strings.Fields(param) + i := 0 + for _, field := range fields { + if !strings.HasPrefix(field, "-") { + break + } + i = strings.LastIndex(param, " "+field) + len(field) + 1 + } + path = strings.TrimLeft(param[i:], " ") //Get all the path even with space inside + } + return path +} + +// commandNlst responds to the NLST FTP command. It allows the client to +// retreive a list of filenames in the current directory. +type commandNlst struct{} + +func (cmd commandNlst) IsExtend() bool { + return false +} + +func (cmd commandNlst) RequireParam() bool { + return false +} + +func (cmd commandNlst) RequireAuth() bool { + return true +} + +func (cmd commandNlst) Execute(conn *Conn, param string) { + path := conn.buildPath(parseListParam(param)) + info, err := conn.driver.Stat(path) + if err != nil { + conn.writeMessage(550, err.Error()) + return + } + if !info.IsDir() { + conn.writeMessage(550, param+" is not a directory") + return + } + + var files []FileInfo + err = conn.driver.ListDir(path, func(f FileInfo) error { + files = append(files, f) + return nil + }) + if err != nil { + conn.writeMessage(550, err.Error()) + return + } + conn.writeMessage(150, "Opening ASCII mode data connection for file list") + conn.sendOutofbandData(listFormatter(files).Short()) +} + +// commandMdtm responds to the MDTM FTP command. It allows the client to +// retreive the last modified time of a file. +type commandMdtm struct{} + +func (cmd commandMdtm) IsExtend() bool { + return false +} + +func (cmd commandMdtm) RequireParam() bool { + return true +} + +func (cmd commandMdtm) RequireAuth() bool { + return true +} + +func (cmd commandMdtm) Execute(conn *Conn, param string) { + path := conn.buildPath(param) + stat, err := conn.driver.Stat(path) + if err == nil { + conn.writeMessage(213, stat.ModTime().Format("20060102150405")) + } else { + conn.writeMessage(450, "File not available") + } +} + +// commandMkd responds to the MKD FTP command. It allows the client to create +// a new directory +type commandMkd struct{} + +func (cmd commandMkd) IsExtend() bool { + return false +} + +func (cmd commandMkd) RequireParam() bool { + return true +} + +func (cmd commandMkd) RequireAuth() bool { + return true +} + +func (cmd commandMkd) Execute(conn *Conn, param string) { + path := conn.buildPath(param) + err := conn.driver.MakeDir(path) + if err == nil { + conn.writeMessage(257, "Directory created") + } else { + conn.writeMessage(550, fmt.Sprint("Action not taken: ", err)) + } +} + +// cmdMode responds to the MODE FTP command. +// +// the original FTP spec had various options for hosts to negotiate how data +// would be sent over the data socket, In reality these days (S)tream mode +// is all that is used for the mode - data is just streamed down the data +// socket unchanged. +type commandMode struct{} + +func (cmd commandMode) IsExtend() bool { + return false +} + +func (cmd commandMode) RequireParam() bool { + return true +} + +func (cmd commandMode) RequireAuth() bool { + return true +} + +func (cmd commandMode) Execute(conn *Conn, param string) { + if strings.ToUpper(param) == "S" { + conn.writeMessage(200, "OK") + } else { + conn.writeMessage(504, "MODE is an obsolete command") + } +} + +// cmdNoop responds to the NOOP FTP command. +// +// This is essentially a ping from the client so we just respond with an +// basic 200 message. +type commandNoop struct{} + +func (cmd commandNoop) IsExtend() bool { + return false +} + +func (cmd commandNoop) RequireParam() bool { + return false +} + +func (cmd commandNoop) RequireAuth() bool { + return false +} + +func (cmd commandNoop) Execute(conn *Conn, param string) { + conn.writeMessage(200, "OK") +} + +// commandPass respond to the PASS FTP command by asking the driver if the +// supplied username and password are valid +type commandPass struct{} + +func (cmd commandPass) IsExtend() bool { + return false +} + +func (cmd commandPass) RequireParam() bool { + return true +} + +func (cmd commandPass) RequireAuth() bool { + return false +} + +func (cmd commandPass) Execute(conn *Conn, param string) { + ok, err := conn.server.Auth.CheckPasswd(conn.reqUser, param) + if err != nil { + conn.writeMessage(550, "Checking password error") + return + } + + if ok { + conn.user = conn.reqUser + conn.reqUser = "" + conn.writeMessage(230, "Password ok, continue") + } else { + conn.writeMessage(530, "Incorrect password, not logged in") + } +} + +// commandPasv responds to the PASV FTP command. +// +// The client is requesting us to open a new TCP listing socket and wait for them +// to connect to it. +type commandPasv struct{} + +func (cmd commandPasv) IsExtend() bool { + return false +} + +func (cmd commandPasv) RequireParam() bool { + return false +} + +func (cmd commandPasv) RequireAuth() bool { + return true +} + +func (cmd commandPasv) Execute(conn *Conn, param string) { + listenIP := conn.passiveListenIP() + lastIdx := strings.LastIndex(listenIP, ":") + if lastIdx <= 0 { + conn.writeMessage(425, "Data connection failed") + return + } + socket, err := newPassiveSocket(listenIP[:lastIdx], conn.PassivePort, conn.logger, conn.sessionID, conn.tlsConfig) + if err != nil { + conn.writeMessage(425, "Data connection failed") + return + } + conn.dataConn = socket + p1 := socket.Port() / 256 + p2 := socket.Port() - (p1 * 256) + quads := strings.Split(listenIP[:lastIdx], ".") + target := fmt.Sprintf("(%s,%s,%s,%s,%d,%d)", quads[0], quads[1], quads[2], quads[3], p1, p2) + msg := "Entering Passive Mode " + target + conn.writeMessage(227, msg) +} + +// commandPort responds to the PORT FTP command. +// +// The client has opened a listening socket for sending out of band data and +// is requesting that we connect to it +type commandPort struct{} + +func (cmd commandPort) IsExtend() bool { + return false +} + +func (cmd commandPort) RequireParam() bool { + return true +} + +func (cmd commandPort) RequireAuth() bool { + return true +} + +func (cmd commandPort) Execute(conn *Conn, param string) { + nums := strings.Split(param, ",") + portOne, _ := strconv.Atoi(nums[4]) + portTwo, _ := strconv.Atoi(nums[5]) + port := (portOne * 256) + portTwo + host := nums[0] + "." + nums[1] + "." + nums[2] + "." + nums[3] + socket, err := newActiveSocket(host, port, conn.logger, conn.sessionID) + if err != nil { + conn.writeMessage(425, "Data connection failed") + return + } + conn.dataConn = socket + conn.writeMessage(200, "Connection established ("+strconv.Itoa(port)+")") +} + +// commandPwd responds to the PWD FTP command. +// +// Tells the client what the current working directory is. +type commandPwd struct{} + +func (cmd commandPwd) IsExtend() bool { + return false +} + +func (cmd commandPwd) RequireParam() bool { + return false +} + +func (cmd commandPwd) RequireAuth() bool { + return true +} + +func (cmd commandPwd) Execute(conn *Conn, param string) { + conn.writeMessage(257, "\""+conn.namePrefix+"\" is the current directory") +} + +// CommandQuit responds to the QUIT FTP command. The client has requested the +// connection be closed. +type commandQuit struct{} + +func (cmd commandQuit) IsExtend() bool { + return false +} + +func (cmd commandQuit) RequireParam() bool { + return false +} + +func (cmd commandQuit) RequireAuth() bool { + return false +} + +func (cmd commandQuit) Execute(conn *Conn, param string) { + conn.writeMessage(221, "Goodbye") + conn.Close() +} + +// commandRetr responds to the RETR FTP command. It allows the client to +// download a file. +type commandRetr struct{} + +func (cmd commandRetr) IsExtend() bool { + return false +} + +func (cmd commandRetr) RequireParam() bool { + return true +} + +func (cmd commandRetr) RequireAuth() bool { + return true +} + +func (cmd commandRetr) Execute(conn *Conn, param string) { + path := conn.buildPath(param) + defer func() { + conn.lastFilePos = 0 + conn.appendData = false + }() + bytes, data, err := conn.driver.GetFile(path, conn.lastFilePos) + if err == nil { + defer data.Close() + conn.writeMessage(150, fmt.Sprintf("Data transfer starting %v bytes", bytes)) + err = conn.sendOutofBandDataWriter(data) + } else { + conn.writeMessage(551, "File not available") + } +} + +type commandRest struct{} + +func (cmd commandRest) IsExtend() bool { + return false +} + +func (cmd commandRest) RequireParam() bool { + return true +} + +func (cmd commandRest) RequireAuth() bool { + return true +} + +func (cmd commandRest) Execute(conn *Conn, param string) { + var err error + conn.lastFilePos, err = strconv.ParseInt(param, 10, 64) + if err != nil { + conn.writeMessage(551, "File not available") + return + } + + conn.appendData = true + + conn.writeMessage(350, fmt.Sprint("Start transfer from ", conn.lastFilePos)) +} + +// commandRnfr responds to the RNFR FTP command. It's the first of two commands +// required for a client to rename a file. +type commandRnfr struct{} + +func (cmd commandRnfr) IsExtend() bool { + return false +} + +func (cmd commandRnfr) RequireParam() bool { + return true +} + +func (cmd commandRnfr) RequireAuth() bool { + return true +} + +func (cmd commandRnfr) Execute(conn *Conn, param string) { + conn.renameFrom = conn.buildPath(param) + conn.writeMessage(350, "Requested file action pending further information.") +} + +// cmdRnto responds to the RNTO FTP command. It's the second of two commands +// required for a client to rename a file. +type commandRnto struct{} + +func (cmd commandRnto) IsExtend() bool { + return false +} + +func (cmd commandRnto) RequireParam() bool { + return true +} + +func (cmd commandRnto) RequireAuth() bool { + return true +} + +func (cmd commandRnto) Execute(conn *Conn, param string) { + toPath := conn.buildPath(param) + err := conn.driver.Rename(conn.renameFrom, toPath) + defer func() { + conn.renameFrom = "" + }() + + if err == nil { + conn.writeMessage(250, "File renamed") + } else { + conn.writeMessage(550, fmt.Sprint("Action not taken: ", err)) + } +} + +// cmdRmd responds to the RMD FTP command. It allows the client to delete a +// directory. +type commandRmd struct{} + +func (cmd commandRmd) IsExtend() bool { + return false +} + +func (cmd commandRmd) RequireParam() bool { + return true +} + +func (cmd commandRmd) RequireAuth() bool { + return true +} + +func (cmd commandRmd) Execute(conn *Conn, param string) { + path := conn.buildPath(param) + err := conn.driver.DeleteDir(path) + if err == nil { + conn.writeMessage(250, "Directory deleted") + } else { + conn.writeMessage(550, fmt.Sprint("Directory delete failed: ", err)) + } +} + +type commandAdat struct{} + +func (cmd commandAdat) IsExtend() bool { + return false +} + +func (cmd commandAdat) RequireParam() bool { + return true +} + +func (cmd commandAdat) RequireAuth() bool { + return true +} + +func (cmd commandAdat) Execute(conn *Conn, param string) { + conn.writeMessage(550, "Action not taken") +} + +type commandAuth struct{} + +func (cmd commandAuth) IsExtend() bool { + return false +} + +func (cmd commandAuth) RequireParam() bool { + return true +} + +func (cmd commandAuth) RequireAuth() bool { + return false +} + +func (cmd commandAuth) Execute(conn *Conn, param string) { + if param == "TLS" && conn.tlsConfig != nil { + conn.writeMessage(234, "AUTH command OK") + err := conn.upgradeToTLS() + if err != nil { + conn.logger.Printf("Error upgrading connection to TLS %v", err.Error()) + } + } else { + conn.writeMessage(550, "Action not taken") + } +} + +type commandCcc struct{} + +func (cmd commandCcc) IsExtend() bool { + return false +} + +func (cmd commandCcc) RequireParam() bool { + return true +} + +func (cmd commandCcc) RequireAuth() bool { + return true +} + +func (cmd commandCcc) Execute(conn *Conn, param string) { + conn.writeMessage(550, "Action not taken") +} + +type commandEnc struct{} + +func (cmd commandEnc) IsExtend() bool { + return false +} + +func (cmd commandEnc) RequireParam() bool { + return true +} + +func (cmd commandEnc) RequireAuth() bool { + return true +} + +func (cmd commandEnc) Execute(conn *Conn, param string) { + conn.writeMessage(550, "Action not taken") +} + +type commandMic struct{} + +func (cmd commandMic) IsExtend() bool { + return false +} + +func (cmd commandMic) RequireParam() bool { + return true +} + +func (cmd commandMic) RequireAuth() bool { + return true +} + +func (cmd commandMic) Execute(conn *Conn, param string) { + conn.writeMessage(550, "Action not taken") +} + +type commandPbsz struct{} + +func (cmd commandPbsz) IsExtend() bool { + return false +} + +func (cmd commandPbsz) RequireParam() bool { + return true +} + +func (cmd commandPbsz) RequireAuth() bool { + return false +} + +func (cmd commandPbsz) Execute(conn *Conn, param string) { + if conn.tls && param == "0" { + conn.writeMessage(200, "OK") + } else { + conn.writeMessage(550, "Action not taken") + } +} + +type commandProt struct{} + +func (cmd commandProt) IsExtend() bool { + return false +} + +func (cmd commandProt) RequireParam() bool { + return true +} + +func (cmd commandProt) RequireAuth() bool { + return false +} + +func (cmd commandProt) Execute(conn *Conn, param string) { + if conn.tls && param == "P" { + conn.writeMessage(200, "OK") + } else if conn.tls { + conn.writeMessage(536, "Only P level is supported") + } else { + conn.writeMessage(550, "Action not taken") + } +} + +type commandConf struct{} + +func (cmd commandConf) IsExtend() bool { + return false +} + +func (cmd commandConf) RequireParam() bool { + return true +} + +func (cmd commandConf) RequireAuth() bool { + return true +} + +func (cmd commandConf) Execute(conn *Conn, param string) { + conn.writeMessage(550, "Action not taken") +} + +// commandSize responds to the SIZE FTP command. It returns the size of the +// requested path in bytes. +type commandSize struct{} + +func (cmd commandSize) IsExtend() bool { + return false +} + +func (cmd commandSize) RequireParam() bool { + return true +} + +func (cmd commandSize) RequireAuth() bool { + return true +} + +func (cmd commandSize) Execute(conn *Conn, param string) { + path := conn.buildPath(param) + stat, err := conn.driver.Stat(path) + if err != nil { + log.Printf("Size: error(%s)", err) + conn.writeMessage(450, fmt.Sprint("path", path, "not found")) + } else { + conn.writeMessage(213, strconv.Itoa(int(stat.Size()))) + } +} + +// commandStor responds to the STOR FTP command. It allows the user to upload a +// new file. +type commandStor struct{} + +func (cmd commandStor) IsExtend() bool { + return false +} + +func (cmd commandStor) RequireParam() bool { + return true +} + +func (cmd commandStor) RequireAuth() bool { + return true +} + +func (cmd commandStor) Execute(conn *Conn, param string) { + targetPath := conn.buildPath(param) + conn.writeMessage(150, "Data transfer starting") + + defer func() { + conn.appendData = false + }() + + bytes, err := conn.driver.PutFile(targetPath, conn.dataConn, conn.appendData) + if err == nil { + msg := "OK, received " + strconv.Itoa(int(bytes)) + " bytes" + conn.writeMessage(226, msg) + } else { + conn.writeMessage(450, fmt.Sprint("error during transfer: ", err)) + } +} + +// commandStru responds to the STRU FTP command. +// +// like the MODE and TYPE commands, stru[cture] dates back to a time when the +// FTP protocol was more aware of the content of the files it was transferring, +// and would sometimes be expected to translate things like EOL markers on the +// fly. +// +// These days files are sent unmodified, and F(ile) mode is the only one we +// really need to support. +type commandStru struct{} + +func (cmd commandStru) IsExtend() bool { + return false +} + +func (cmd commandStru) RequireParam() bool { + return true +} + +func (cmd commandStru) RequireAuth() bool { + return true +} + +func (cmd commandStru) Execute(conn *Conn, param string) { + if strings.ToUpper(param) == "F" { + conn.writeMessage(200, "OK") + } else { + conn.writeMessage(504, "STRU is an obsolete command") + } +} + +// commandSyst responds to the SYST FTP command by providing a canned response. +type commandSyst struct{} + +func (cmd commandSyst) IsExtend() bool { + return false +} + +func (cmd commandSyst) RequireParam() bool { + return false +} + +func (cmd commandSyst) RequireAuth() bool { + return true +} + +func (cmd commandSyst) Execute(conn *Conn, param string) { + conn.writeMessage(215, "UNIX Type: L8") +} + +// commandType responds to the TYPE FTP command. +// +// like the MODE and STRU commands, TYPE dates back to a time when the FTP +// protocol was more aware of the content of the files it was transferring, and +// would sometimes be expected to translate things like EOL markers on the fly. +// +// Valid options were A(SCII), I(mage), E(BCDIC) or LN (for local type). Since +// we plan to just accept bytes from the client unchanged, I think Image mode is +// adequate. The RFC requires we accept ASCII mode however, so accept it, but +// ignore it. +type commandType struct{} + +func (cmd commandType) IsExtend() bool { + return false +} + +func (cmd commandType) RequireParam() bool { + return false +} + +func (cmd commandType) RequireAuth() bool { + return true +} + +func (cmd commandType) Execute(conn *Conn, param string) { + if strings.ToUpper(param) == "A" { + conn.writeMessage(200, "Type set to ASCII") + } else if strings.ToUpper(param) == "I" { + conn.writeMessage(200, "Type set to binary") + } else { + conn.writeMessage(500, "Invalid type") + } +} + +// commandUser responds to the USER FTP command by asking for the password +type commandUser struct{} + +func (cmd commandUser) IsExtend() bool { + return false +} + +func (cmd commandUser) RequireParam() bool { + return true +} + +func (cmd commandUser) RequireAuth() bool { + return false +} + +func (cmd commandUser) Execute(conn *Conn, param string) { + conn.reqUser = param + if conn.tls || conn.tlsConfig == nil { + conn.writeMessage(331, "User name ok, password required") + } else { + conn.writeMessage(534, "Unsecured login not allowed. AUTH TLS required") + } +} diff --git a/vendor/github.com/goftp/server/conn.go b/vendor/github.com/goftp/server/conn.go new file mode 100644 index 000000000..4d5cace5f --- /dev/null +++ b/vendor/github.com/goftp/server/conn.go @@ -0,0 +1,252 @@ +// 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" + "crypto/rand" + "crypto/sha256" + "crypto/tls" + "encoding/hex" + "fmt" + "io" + "log" + mrand "math/rand" + "net" + "path/filepath" + "strconv" + "strings" +) + +const ( + defaultWelcomeMessage = "Welcome to the Go FTP 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 + namePrefix string + reqUser string + user string + renameFrom string + lastFilePos int64 + appendData bool + closed bool + tls bool +} + +func (conn *Conn) LoginUser() string { + return conn.user +} + +func (conn *Conn) IsLogin() bool { + return len(conn.user) > 0 +} + +func (conn *Conn) PublicIp() string { + return conn.server.PublicIp +} + +func (conn *Conn) passiveListenIP() string { + if len(conn.PublicIp()) > 0 { + return conn.PublicIp() + } + return conn.conn.LocalAddr().String() +} + +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 + if conn.dataConn != nil { + conn.dataConn.Close() + conn.dataConn = nil + } +} + +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) { + 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.namePrefix + "/" + filename) + } else { + fullPath = filepath.Clean(conn.namePrefix) + } + 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 +} diff --git a/vendor/github.com/goftp/server/doc.go b/vendor/github.com/goftp/server/doc.go new file mode 100644 index 000000000..22bb2f87c --- /dev/null +++ b/vendor/github.com/goftp/server/doc.go @@ -0,0 +1,14 @@ +// 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. + +/* +http://tools.ietf.org/html/rfc959 + +http://www.faqs.org/rfcs/rfc2389.html +http://www.faqs.org/rfcs/rfc959.html + +http://tools.ietf.org/html/rfc2428 +*/ + +package server diff --git a/vendor/github.com/goftp/server/driver.go b/vendor/github.com/goftp/server/driver.go new file mode 100644 index 000000000..376be52ce --- /dev/null +++ b/vendor/github.com/goftp/server/driver.go @@ -0,0 +1,61 @@ +// 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 "io" + +// DriverFactory is a driver factory to create driver. For each client that connects to the server, a new FTPDriver is required. +// Create an implementation if this interface and provide it to FTPServer. +type DriverFactory interface { + NewDriver() (Driver, error) +} + +// Driver is an interface that you will create an implementation that speaks to your +// chosen persistence layer. graval will create a new instance of your +// driver for each client that connects and delegate to it as required. +type Driver interface { + // Init init + Init(*Conn) + + // params - a file path + // returns - a time indicating when the requested path was last modified + // - an error if the file doesn't exist or the user lacks + // permissions + Stat(string) (FileInfo, error) + + // params - path + // returns - true if the current user is permitted to change to the + // requested path + ChangeDir(string) error + + // params - path, function on file or subdir found + // returns - error + // path + ListDir(string, func(FileInfo) error) error + + // params - path + // returns - nil if the directory was deleted or any error encountered + DeleteDir(string) error + + // params - path + // returns - nil if the file was deleted or any error encountered + DeleteFile(string) error + + // params - from_path, to_path + // returns - nil if the file was renamed or any error encountered + Rename(string, string) error + + // params - path + // returns - nil if the new directory was created or any error encountered + MakeDir(string) error + + // params - path + // returns - a string containing the file data to send to the client + GetFile(string, int64) (int64, io.ReadCloser, error) + + // params - destination path, an io.Reader containing the file data + // returns - the number of bytes writen and the first error encountered while writing, if any. + PutFile(string, io.Reader, bool) (int64, error) +} diff --git a/vendor/github.com/goftp/server/fileinfo.go b/vendor/github.com/goftp/server/fileinfo.go new file mode 100644 index 000000000..26dafd0a0 --- /dev/null +++ b/vendor/github.com/goftp/server/fileinfo.go @@ -0,0 +1,14 @@ +// 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 "os" + +type FileInfo interface { + os.FileInfo + + Owner() string + Group() string +} diff --git a/vendor/github.com/goftp/server/listformatter.go b/vendor/github.com/goftp/server/listformatter.go new file mode 100644 index 000000000..b4b3d7739 --- /dev/null +++ b/vendor/github.com/goftp/server/listformatter.go @@ -0,0 +1,49 @@ +// 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 ( + "bytes" + "fmt" + "strconv" + "strings" +) + +type listFormatter []FileInfo + +// Short returns a string that lists the collection of files by name only, +// one per line +func (formatter listFormatter) Short() []byte { + var buf bytes.Buffer + for _, file := range formatter { + fmt.Fprintf(&buf, "%s\r\n", file.Name()) + } + return buf.Bytes() +} + +// Detailed returns a string that lists the collection of files with extra +// detail, one per line +func (formatter listFormatter) Detailed() []byte { + var buf bytes.Buffer + for _, file := range formatter { + fmt.Fprintf(&buf, file.Mode().String()) + fmt.Fprintf(&buf, " 1 %s %s ", file.Owner(), file.Group()) + fmt.Fprintf(&buf, lpad(strconv.FormatInt(file.Size(), 10), 12)) + fmt.Fprintf(&buf, file.ModTime().Format(" Jan _2 15:04 ")) + fmt.Fprintf(&buf, "%s\r\n", file.Name()) + } + return buf.Bytes() +} + +func lpad(input string, length int) (result string) { + if len(input) < length { + result = strings.Repeat(" ", length-len(input)) + input + } else if len(input) == length { + result = input + } else { + result = input[0:length] + } + return +} diff --git a/vendor/github.com/goftp/server/logger.go b/vendor/github.com/goftp/server/logger.go new file mode 100644 index 000000000..3413585eb --- /dev/null +++ b/vendor/github.com/goftp/server/logger.go @@ -0,0 +1,48 @@ +// 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 ( + "fmt" + "log" +) + +type Logger interface { + Print(sessionId string, message interface{}) + Printf(sessionId string, format string, v ...interface{}) + PrintCommand(sessionId string, command string, params string) + PrintResponse(sessionId string, code int, message string) +} + +// Use an instance of this to log in a standard format +type StdLogger struct{} + +func (logger *StdLogger) Print(sessionId string, message interface{}) { + log.Printf("%s %s", sessionId, message) +} + +func (logger *StdLogger) Printf(sessionId string, format string, v ...interface{}) { + logger.Print(sessionId, fmt.Sprintf(format, v...)) +} + +func (logger *StdLogger) PrintCommand(sessionId string, command string, params string) { + if command == "PASS" { + log.Printf("%s > PASS ****", sessionId) + } else { + log.Printf("%s > %s %s", sessionId, command, params) + } +} + +func (logger *StdLogger) PrintResponse(sessionId string, code int, message string) { + log.Printf("%s < %d %s", sessionId, code, message) +} + +// Silent logger, produces no output +type DiscardLogger struct{} + +func (logger *DiscardLogger) Print(sessionId string, message interface{}) {} +func (logger *DiscardLogger) Printf(sessionId string, format string, v ...interface{}) {} +func (logger *DiscardLogger) PrintCommand(sessionId string, command string, params string) {} +func (logger *DiscardLogger) PrintResponse(sessionId string, code int, message string) {} diff --git a/vendor/github.com/goftp/server/perm.go b/vendor/github.com/goftp/server/perm.go new file mode 100644 index 000000000..abcb70a9a --- /dev/null +++ b/vendor/github.com/goftp/server/perm.go @@ -0,0 +1,52 @@ +// 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 "os" + +type Perm interface { + GetOwner(string) (string, error) + GetGroup(string) (string, error) + GetMode(string) (os.FileMode, error) + + ChOwner(string, string) error + ChGroup(string, string) error + ChMode(string, os.FileMode) error +} + +type SimplePerm struct { + owner, group string +} + +func NewSimplePerm(owner, group string) *SimplePerm { + return &SimplePerm{ + owner: owner, + group: group, + } +} + +func (s *SimplePerm) GetOwner(string) (string, error) { + return s.owner, nil +} + +func (s *SimplePerm) GetGroup(string) (string, error) { + return s.group, nil +} + +func (s *SimplePerm) GetMode(string) (os.FileMode, error) { + return os.ModePerm, nil +} + +func (s *SimplePerm) ChOwner(string, string) error { + return nil +} + +func (s *SimplePerm) ChGroup(string, string) error { + return nil +} + +func (s *SimplePerm) ChMode(string, os.FileMode) error { + return nil +} diff --git a/vendor/github.com/goftp/server/server.go b/vendor/github.com/goftp/server/server.go new file mode 100644 index 000000000..980bc7e1b --- /dev/null +++ b/vendor/github.com/goftp/server/server.go @@ -0,0 +1,278 @@ +// 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" + "context" + "crypto/tls" + "errors" + "fmt" + "net" + "strconv" +) + +// Version returns the library version +func Version() string { + return "0.3.0" +} + +// ServerOpts contains parameters for server.NewServer() +type ServerOpts struct { + // The factory that will be used to create a new FTPDriver instance for + // each client connection. This is a mandatory option. + Factory DriverFactory + + Auth Auth + + // Server Name, Default is Go Ftp Server + Name string + + // The hostname that the FTP server should listen on. Optional, defaults to + // "::", which means all hostnames on ipv4 and ipv6. + Hostname string + + // Public IP of the server + PublicIp string + + // Passive ports + PassivePorts string + + // The port that the FTP should listen on. Optional, defaults to 3000. In + // a production environment you will probably want to change this to 21. + Port int + + // use tls, default is false + TLS bool + + // if tls used, cert file is required + CertFile string + + // if tls used, key file is required + KeyFile string + + // If ture TLS is used in RFC4217 mode + ExplicitFTPS bool + + WelcomeMessage string + + // A logger implementation, if nil the StdLogger is used + Logger Logger +} + +// Server is the root of your FTP application. You should instantiate one +// of these and call ListenAndServe() to start accepting client connections. +// +// Always use the NewServer() method to create a new Server. +type Server struct { + *ServerOpts + listenTo string + logger Logger + listener net.Listener + tlsConfig *tls.Config + ctx context.Context + cancel context.CancelFunc + feats string +} + +// ErrServerClosed is returned by ListenAndServe() or Serve() when a shutdown +// was requested. +var ErrServerClosed = errors.New("ftp: Server closed") + +// serverOptsWithDefaults copies an ServerOpts struct into a new struct, +// then adds any default values that are missing and returns the new data. +func serverOptsWithDefaults(opts *ServerOpts) *ServerOpts { + var newOpts ServerOpts + if opts == nil { + opts = &ServerOpts{} + } + if opts.Hostname == "" { + newOpts.Hostname = "::" + } else { + newOpts.Hostname = opts.Hostname + } + if opts.Port == 0 { + newOpts.Port = 3000 + } else { + newOpts.Port = opts.Port + } + newOpts.Factory = opts.Factory + if opts.Name == "" { + newOpts.Name = "Go FTP Server" + } else { + newOpts.Name = opts.Name + } + + if opts.WelcomeMessage == "" { + newOpts.WelcomeMessage = defaultWelcomeMessage + } else { + newOpts.WelcomeMessage = opts.WelcomeMessage + } + + if opts.Auth != nil { + newOpts.Auth = opts.Auth + } + + newOpts.Logger = &StdLogger{} + if opts.Logger != nil { + newOpts.Logger = opts.Logger + } + + newOpts.TLS = opts.TLS + newOpts.KeyFile = opts.KeyFile + newOpts.CertFile = opts.CertFile + newOpts.ExplicitFTPS = opts.ExplicitFTPS + + newOpts.PublicIp = opts.PublicIp + newOpts.PassivePorts = opts.PassivePorts + + return &newOpts +} + +// NewServer initialises a new FTP server. Configuration options are provided +// via an instance of ServerOpts. Calling this function in your code will +// probably look something like this: +// +// factory := &MyDriverFactory{} +// server := server.NewServer(&server.ServerOpts{ Factory: factory }) +// +// or: +// +// factory := &MyDriverFactory{} +// opts := &server.ServerOpts{ +// Factory: factory, +// Port: 2000, +// Hostname: "127.0.0.1", +// } +// server := server.NewServer(opts) +// +func NewServer(opts *ServerOpts) *Server { + opts = serverOptsWithDefaults(opts) + s := new(Server) + s.ServerOpts = opts + s.listenTo = net.JoinHostPort(opts.Hostname, strconv.Itoa(opts.Port)) + s.logger = opts.Logger + return s +} + +// NewConn constructs a new object that will handle the FTP protocol over +// an active net.TCPConn. The TCP connection should already be open before +// it is handed to this functions. driver is an instance of FTPDriver that +// will handle all auth and persistence details. +func (server *Server) newConn(tcpConn net.Conn, driver Driver) *Conn { + c := new(Conn) + c.namePrefix = "/" + c.conn = tcpConn + c.controlReader = bufio.NewReader(tcpConn) + c.controlWriter = bufio.NewWriter(tcpConn) + c.driver = driver + c.auth = server.Auth + c.server = server + c.sessionID = newSessionID() + c.logger = server.logger + c.tlsConfig = server.tlsConfig + + driver.Init(c) + return c +} + +func simpleTLSConfig(certFile, keyFile string) (*tls.Config, error) { + config := &tls.Config{} + if config.NextProtos == nil { + config.NextProtos = []string{"ftp"} + } + + var err error + config.Certificates = make([]tls.Certificate, 1) + config.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return nil, err + } + return config, nil +} + +// ListenAndServe asks a new Server to begin accepting client connections. It +// accepts no arguments - all configuration is provided via the NewServer +// function. +// +// If the server fails to start for any reason, an error will be returned. Common +// errors are trying to bind to a privileged port or something else is already +// listening on the same port. +// +func (server *Server) ListenAndServe() error { + var listener net.Listener + var err error + var curFeats = featCmds + + if server.ServerOpts.TLS { + server.tlsConfig, err = simpleTLSConfig(server.CertFile, server.KeyFile) + if err != nil { + return err + } + + curFeats += " AUTH TLS\n PBSZ\n PROT\n" + + if server.ServerOpts.ExplicitFTPS { + listener, err = net.Listen("tcp", server.listenTo) + } else { + listener, err = tls.Listen("tcp", server.listenTo, server.tlsConfig) + } + } else { + listener, err = net.Listen("tcp", server.listenTo) + } + if err != nil { + return err + } + server.feats = fmt.Sprintf(feats, curFeats) + + sessionID := "" + server.logger.Printf(sessionID, "%s listening on %d", server.Name, server.Port) + + return server.Serve(listener) +} + +// Serve accepts connections on a given net.Listener and handles each +// request in a new goroutine. +// +func (server *Server) Serve(l net.Listener) error { + server.listener = l + server.ctx, server.cancel = context.WithCancel(context.Background()) + sessionID := "" + for { + tcpConn, err := server.listener.Accept() + if err != nil { + select { + case <-server.ctx.Done(): + return ErrServerClosed + default: + } + server.logger.Printf(sessionID, "listening error: %v", err) + if ne, ok := err.(net.Error); ok && ne.Temporary() { + continue + } + return err + } + driver, err := server.Factory.NewDriver() + if err != nil { + server.logger.Printf(sessionID, "Error creating driver, aborting client connection: %v", err) + tcpConn.Close() + } else { + ftpConn := server.newConn(tcpConn, driver) + go ftpConn.Serve() + } + } +} + +// Shutdown will gracefully stop a server. Already connected clients will retain their connections +func (server *Server) Shutdown() error { + if server.cancel != nil { + server.cancel() + } + if server.listener != nil { + return server.listener.Close() + } + // server wasnt even started + return nil +} diff --git a/vendor/github.com/goftp/server/socket.go b/vendor/github.com/goftp/server/socket.go new file mode 100644 index 000000000..3795e27ce --- /dev/null +++ b/vendor/github.com/goftp/server/socket.go @@ -0,0 +1,245 @@ +// 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 ( + "crypto/tls" + "io" + "net" + "os" + "runtime" + "strconv" + "strings" + "sync" + "syscall" +) + +// DataSocket describes a data socket is used to send non-control data between the client and +// server. +type DataSocket interface { + Host() string + + Port() int + + // the standard io.Reader interface + Read(p []byte) (n int, err error) + + // the standard io.ReaderFrom interface + ReadFrom(r io.Reader) (int64, error) + + // the standard io.Writer interface + Write(p []byte) (n int, err error) + + // the standard io.Closer interface + Close() error +} + +type ftpActiveSocket struct { + conn *net.TCPConn + host string + port int + logger Logger +} + +func newActiveSocket(remote string, port int, logger Logger, sessionID string) (DataSocket, error) { + connectTo := net.JoinHostPort(remote, strconv.Itoa(port)) + + logger.Print(sessionID, "Opening active data connection to "+connectTo) + + raddr, err := net.ResolveTCPAddr("tcp", connectTo) + + if err != nil { + logger.Print(sessionID, err) + return nil, err + } + + tcpConn, err := net.DialTCP("tcp", nil, raddr) + + if err != nil { + logger.Print(sessionID, err) + return nil, err + } + + socket := new(ftpActiveSocket) + socket.conn = tcpConn + socket.host = remote + socket.port = port + socket.logger = logger + + return socket, nil +} + +func (socket *ftpActiveSocket) Host() string { + return socket.host +} + +func (socket *ftpActiveSocket) Port() int { + return socket.port +} + +func (socket *ftpActiveSocket) Read(p []byte) (n int, err error) { + return socket.conn.Read(p) +} + +func (socket *ftpActiveSocket) ReadFrom(r io.Reader) (int64, error) { + return socket.conn.ReadFrom(r) +} + +func (socket *ftpActiveSocket) Write(p []byte) (n int, err error) { + return socket.conn.Write(p) +} + +func (socket *ftpActiveSocket) Close() error { + return socket.conn.Close() +} + +type ftpPassiveSocket struct { + conn net.Conn + port int + host string + ingress chan []byte + egress chan []byte + logger Logger + lock sync.Mutex // protects conn and err + err error + tlsConfig *tls.Config +} + +// Detect if an error is "bind: address already in use" +// +// Originally from https://stackoverflow.com/a/52152912/164234 +func isErrorAddressAlreadyInUse(err error) bool { + errOpError, ok := err.(*net.OpError) + if !ok { + return false + } + errSyscallError, ok := errOpError.Err.(*os.SyscallError) + if !ok { + return false + } + errErrno, ok := errSyscallError.Err.(syscall.Errno) + if !ok { + return false + } + if errErrno == syscall.EADDRINUSE { + return true + } + const WSAEADDRINUSE = 10048 + if runtime.GOOS == "windows" && errErrno == WSAEADDRINUSE { + return true + } + return false +} + +func newPassiveSocket(host string, port func() int, logger Logger, sessionID string, tlsConfig *tls.Config) (DataSocket, error) { + socket := new(ftpPassiveSocket) + socket.ingress = make(chan []byte) + socket.egress = make(chan []byte) + socket.logger = logger + socket.host = host + socket.tlsConfig = tlsConfig + const retries = 10 + var err error + for i := 1; i <= retries; i++ { + socket.port = port() + err = socket.GoListenAndServe(sessionID) + if err != nil && socket.port != 0 && isErrorAddressAlreadyInUse(err) { + // choose a different port on error already in use + continue + } + break + } + return socket, err +} + +func (socket *ftpPassiveSocket) Host() string { + return socket.host +} + +func (socket *ftpPassiveSocket) Port() int { + return socket.port +} + +func (socket *ftpPassiveSocket) Read(p []byte) (n int, err error) { + socket.lock.Lock() + defer socket.lock.Unlock() + if socket.err != nil { + return 0, socket.err + } + return socket.conn.Read(p) +} + +func (socket *ftpPassiveSocket) ReadFrom(r io.Reader) (int64, error) { + socket.lock.Lock() + defer socket.lock.Unlock() + if socket.err != nil { + return 0, socket.err + } + + // For normal TCPConn, this will use sendfile syscall; if not, + // it will just downgrade to normal read/write procedure + return io.Copy(socket.conn, r) +} + +func (socket *ftpPassiveSocket) Write(p []byte) (n int, err error) { + socket.lock.Lock() + defer socket.lock.Unlock() + if socket.err != nil { + return 0, socket.err + } + return socket.conn.Write(p) +} + +func (socket *ftpPassiveSocket) Close() error { + socket.lock.Lock() + defer socket.lock.Unlock() + if socket.conn != nil { + return socket.conn.Close() + } + return nil +} + +func (socket *ftpPassiveSocket) GoListenAndServe(sessionID string) (err error) { + laddr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort("", strconv.Itoa(socket.port))) + if err != nil { + socket.logger.Print(sessionID, err) + return + } + + var listener net.Listener + listener, err = net.ListenTCP("tcp", laddr) + if err != nil { + socket.logger.Print(sessionID, err) + return + } + + add := listener.Addr() + parts := strings.Split(add.String(), ":") + port, err := strconv.Atoi(parts[len(parts)-1]) + if err != nil { + socket.logger.Print(sessionID, err) + return + } + + socket.port = port + if socket.tlsConfig != nil { + listener = tls.NewListener(listener, socket.tlsConfig) + } + + socket.lock.Lock() + go func() { + defer socket.lock.Unlock() + + conn, err := listener.Accept() + if err != nil { + socket.err = err + return + } + socket.err = nil + socket.conn = conn + _ = listener.Close() + }() + return nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 6493a86a7..444feaaa3 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -74,6 +74,8 @@ github.com/dropbox/dropbox-sdk-go-unofficial/dropbox/team_policies github.com/dropbox/dropbox-sdk-go-unofficial/dropbox/users_common # github.com/go-ini/ini v1.37.0 github.com/go-ini/ini +# github.com/goftp/server v0.0.0-20180914132916-1fd52c8552f1 +github.com/goftp/server # github.com/golang/protobuf v1.1.0 github.com/golang/protobuf/proto # github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135