First commit.
This commit is contained in:
commit
bcc332e95c
13
LICENSE
Normal file
13
LICENSE
Normal file
@ -0,0 +1,13 @@
|
||||
Copyright (c) 2011, Julien Laffaye <jlaffaye@FreeBSD.org>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
8
Makefile
Normal file
8
Makefile
Normal file
@ -0,0 +1,8 @@
|
||||
include ${GOROOT}/src/Make.inc
|
||||
|
||||
TARG= ftp
|
||||
GOFILES=\
|
||||
ftp.go\
|
||||
status.go
|
||||
|
||||
include ${GOROOT}/src/Make.pkg
|
224
ftp.go
Normal file
224
ftp.go
Normal file
@ -0,0 +1,224 @@
|
||||
package ftp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"os"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
EntryTypeFile = iota
|
||||
EntryTypeFolder
|
||||
EntryTypeLink
|
||||
)
|
||||
|
||||
type ServerCon struct {
|
||||
conn net.Conn
|
||||
bio *bufio.Reader
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
conn net.Conn
|
||||
c *ServerCon
|
||||
}
|
||||
|
||||
type Entry struct {
|
||||
Name string
|
||||
EntryType int
|
||||
Size uint64
|
||||
}
|
||||
|
||||
// Check if the last status code is equal to the given code
|
||||
// If it is the case, err is nil
|
||||
// Returns the status line for further processing
|
||||
func (c *ServerCon) checkStatus(expected int) (line string, err os.Error) {
|
||||
line, err = c.bio.ReadString('\n')
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
code, err := strconv.Atoi(line[:3]) // A status is 3 digits
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if code != expected {
|
||||
err = os.NewError(fmt.Sprintf("%d %s", code, statusText[code]))
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Like send() but with formating.
|
||||
func (c *ServerCon) sendf(str string, a ...interface{}) (os.Error) {
|
||||
return c.send([]byte(fmt.Sprintf(str, a...)))
|
||||
}
|
||||
|
||||
// Send a raw command on the connection.
|
||||
func (c *ServerCon) send(data []byte) (os.Error) {
|
||||
_, err := c.conn.Write(data)
|
||||
return err
|
||||
}
|
||||
|
||||
// Connect to a ftp server and returns a ServerCon handler.
|
||||
func Connect(host, user, password string) (*ServerCon, os.Error) {
|
||||
conn, err := net.Dial("tcp", host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := &ServerCon{conn, bufio.NewReader(conn)}
|
||||
|
||||
_, err = c.checkStatus(StatusReady)
|
||||
if err != nil {
|
||||
c.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.sendf("USER %v\r\n", user)
|
||||
_, err = c.checkStatus(StatusUserOK)
|
||||
if err != nil {
|
||||
c.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.sendf("PASS %v\r\n", password)
|
||||
_, err = c.checkStatus(StatusLoggedIn)
|
||||
if err != nil {
|
||||
c.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Like Connect() but with anonymous credentials.
|
||||
func ConnectAnonymous(host string) (*ServerCon, os.Error) {
|
||||
return Connect(host, "anonymous", "anonymous")
|
||||
}
|
||||
|
||||
// Enter extended passive mode
|
||||
func (c *ServerCon) epsv() (port int, err os.Error) {
|
||||
c.send([]byte("EPSV\r\n"))
|
||||
line, err := c.checkStatus(StatusExtendedPassiveMode)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
start := strings.Index(line, "|||")
|
||||
end := strings.LastIndex(line, "|")
|
||||
if start == -1 || end == -1 {
|
||||
err = os.NewError("Invalid EPSV response format")
|
||||
return
|
||||
}
|
||||
port, err = strconv.Atoi(line[start+3 : end])
|
||||
return
|
||||
}
|
||||
|
||||
// Open a new data connection using extended passive mode
|
||||
func (c *ServerCon) openDataConnection() (r *Response, err os.Error) {
|
||||
port, err := c.epsv()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Build the new net address string
|
||||
a := strings.Split(c.conn.RemoteAddr().String(), ":", 2)
|
||||
addr := fmt.Sprintf("%v:%v", a[0], port)
|
||||
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
r = &Response{conn, c}
|
||||
return
|
||||
}
|
||||
|
||||
func parseListLine(line string) (*Entry, os.Error) {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 9 {
|
||||
return nil, os.NewError("Unsupported LIST line")
|
||||
}
|
||||
|
||||
e := &Entry{}
|
||||
switch fields[0][0] {
|
||||
case '-':
|
||||
e.EntryType = EntryTypeFile
|
||||
case 'd':
|
||||
e.EntryType = EntryTypeFolder
|
||||
case 'l':
|
||||
e.EntryType = EntryTypeLink
|
||||
default:
|
||||
return nil, os.NewError("Unknown entry type")
|
||||
}
|
||||
|
||||
e.Name = strings.Join(fields[8:], " ")
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func (c *ServerCon) List() (entries []*Entry, err os.Error) {
|
||||
r, err := c.openDataConnection()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
c.send([]byte("LIST\r\n"))
|
||||
_, err = c.checkStatus(StatusAboutToSend)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
bio := bufio.NewReader(r)
|
||||
for {
|
||||
line, e := bio.ReadString('\n')
|
||||
if e == os.EOF {
|
||||
break
|
||||
} else if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
entry, err := parseListLine(line)
|
||||
if err == nil {
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *ServerCon) ChangeDir(path string) (err os.Error) {
|
||||
c.sendf("CWD %s\r\n", path);
|
||||
_, err = c.checkStatus(StatusRequestedFileActionOK)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *ServerCon) Get(path string) (r *Response, err os.Error) {
|
||||
r, err = c.openDataConnection()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.sendf("RETR %s\r\n", path)
|
||||
_, err = c.checkStatus(StatusAboutToSend)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *ServerCon) Close() {
|
||||
c.send([]byte("QUIT\r\n"))
|
||||
c.conn.Close()
|
||||
}
|
||||
|
||||
func (r *Response) Read(buf []byte) (int, os.Error) {
|
||||
n, err := r.conn.Read(buf)
|
||||
if err == os.EOF {
|
||||
_, err2 := r.c.checkStatus(StatusClosingDataConnection)
|
||||
if err2 != nil {
|
||||
err = err2
|
||||
}
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (r *Response) Close() os.Error {
|
||||
return r.conn.Close()
|
||||
}
|
50
parse_test.go
Normal file
50
parse_test.go
Normal file
@ -0,0 +1,50 @@
|
||||
package ftp
|
||||
|
||||
import "testing"
|
||||
|
||||
type line struct {
|
||||
line string
|
||||
name string
|
||||
entryType int
|
||||
}
|
||||
|
||||
var listTests = []line {
|
||||
// UNIX ls -l style
|
||||
line{"drwxr-xr-x 3 110 1002 3 Dec 02 2009 pub", "pub", EntryTypeFolder},
|
||||
line{"drwxr-xr-x 3 110 1002 3 Dec 02 2009 p u b", "p u b", EntryTypeFolder},
|
||||
line{"-rwxr-xr-x 3 110 1002 1234567 Dec 02 2009 fileName", "fileName", EntryTypeFile},
|
||||
line{"lrwxrwxrwx 1 root other 7 Jan 25 00:17 bin -> usr/bin", "bin -> usr/bin", EntryTypeLink},
|
||||
// Microsoft's FTP servers for Windows
|
||||
line{"---------- 1 owner group 1803128 Jul 10 10:18 ls-lR.Z", "ls-lR.Z", EntryTypeFile},
|
||||
line{"d--------- 1 owner group 0 May 9 19:45 Softlib", "Softlib", EntryTypeFolder},
|
||||
// WFTPD for MSDOS
|
||||
line{"-rwxrwxrwx 1 noone nogroup 322 Aug 19 1996 message.ftp", "message.ftp", EntryTypeFile},
|
||||
}
|
||||
|
||||
// Not supported, at least we should properly return failure
|
||||
var listTestsFail = []line {
|
||||
line{"d [R----F--] supervisor 512 Jan 16 18:53 login", "login", EntryTypeFolder},
|
||||
line{"- [R----F--] rhesus 214059 Oct 20 15:27 cx.exe", "cx.exe", EntryTypeFile},
|
||||
}
|
||||
|
||||
func TestParseListLine(t *testing.T) {
|
||||
for _, lt := range listTests {
|
||||
entry, err := parseListLine(lt.line)
|
||||
if err != nil {
|
||||
t.Errorf("parseListLine(%v) returned err = %v", lt.line, err)
|
||||
continue
|
||||
}
|
||||
if entry.Name != lt.name {
|
||||
t.Errorf("parseListLine(%v).Name = '%v', want '%v'", lt.line, entry.Name, lt.name)
|
||||
}
|
||||
if entry.EntryType != lt.entryType {
|
||||
t.Errorf("parseListLine(%v).EntryType = %v, want %v", lt.line, entry.EntryType, lt.entryType,)
|
||||
}
|
||||
}
|
||||
for _, lt := range listTestsFail {
|
||||
_, err := parseListLine(lt.line)
|
||||
if err == nil {
|
||||
t.Errorf("parseListLine(%v) expected to fail", lt.line)
|
||||
}
|
||||
}
|
||||
}
|
100
status.go
Normal file
100
status.go
Normal file
@ -0,0 +1,100 @@
|
||||
package ftp
|
||||
|
||||
const (
|
||||
StatusInitiating = 100
|
||||
StatusRestartMarker = 110
|
||||
StatusReadyMinute = 120
|
||||
StatusAboutToSend = 150
|
||||
|
||||
StatusCommandOK = 200
|
||||
StatusCommandNotImplemented = 202
|
||||
StatusSystem = 211
|
||||
StatusDirectory = 212
|
||||
StatusFile = 213
|
||||
StatusHelp = 214
|
||||
StatusName = 215
|
||||
StatusReady = 220
|
||||
StatusClosing = 221
|
||||
StatusDataConnectionOpen = 225
|
||||
StatusClosingDataConnection = 226
|
||||
StatusPassiveMode = 227
|
||||
StatusLongPassiveMode = 228
|
||||
StatusExtendedPassiveMode = 229
|
||||
StatusLoggedIn = 230
|
||||
StatusLoggedOut = 231
|
||||
StatusLogoutAck = 232
|
||||
StatusRequestedFileActionOK = 250
|
||||
StatusPathCreated = 257
|
||||
|
||||
StatusUserOK = 331
|
||||
StatusLoginNeedAccount = 332
|
||||
Status350 = 350
|
||||
|
||||
StatusNotAvailable = 421
|
||||
StatusCanNotOpenDataConnection = 425
|
||||
StatusTransfertAborted = 426
|
||||
StatusInvalidCredentials = 430
|
||||
StatusHostUnavailable = 434
|
||||
StatusFileActionIgnored = 450
|
||||
StatusActionAborted = 451
|
||||
Status452 = 452
|
||||
|
||||
StatusBadCommand = 500
|
||||
StatusBadArguments = 501
|
||||
StatusNotImplemented = 502
|
||||
StatusBadSequence = 503
|
||||
StatusNotImplementedParameter = 504
|
||||
StatusNotLoggedIn = 530
|
||||
StatusStorNeedAccount = 532
|
||||
StatusFileUnavailable = 550
|
||||
StatusPageTypeUnknown = 551
|
||||
StatusExceededStorage = 552
|
||||
StatusBadFileName = 553
|
||||
)
|
||||
|
||||
var statusText = map[int]string{
|
||||
StatusCommandOK: "Command okay",
|
||||
StatusCommandNotImplemented: "Command not implemented, superfluous at this site",
|
||||
StatusSystem: "System status, or system help reply",
|
||||
StatusDirectory: "Directory status",
|
||||
StatusFile: "File status",
|
||||
StatusHelp: "Help message",
|
||||
StatusName: "",
|
||||
StatusReady: "Service ready for new user",
|
||||
StatusClosing: "Service closing control connection",
|
||||
StatusDataConnectionOpen: "Data connection open; no transfer in progress",
|
||||
StatusClosingDataConnection: "Closing data connection. Requested file action successful",
|
||||
StatusPassiveMode: "Entering Passive Mode",
|
||||
StatusLongPassiveMode: "Entering Long Passive Mode",
|
||||
StatusExtendedPassiveMode: "Entering Extended Passive Mode",
|
||||
StatusLoggedIn: "User logged in, proceed",
|
||||
StatusLoggedOut: "User logged out; service terminated",
|
||||
StatusLogoutAck: "Logout command noted, will complete when transfer done",
|
||||
StatusRequestedFileActionOK: "Requested file action okay, completed",
|
||||
StatusPathCreated: "Path created",
|
||||
|
||||
StatusUserOK: "",
|
||||
StatusLoginNeedAccount: "",
|
||||
Status350: "",
|
||||
|
||||
StatusNotAvailable: "",
|
||||
StatusCanNotOpenDataConnection: "",
|
||||
StatusTransfertAborted: "",
|
||||
StatusInvalidCredentials: "",
|
||||
StatusHostUnavailable: "",
|
||||
StatusFileActionIgnored: "",
|
||||
StatusActionAborted: "",
|
||||
Status452: "",
|
||||
|
||||
StatusBadCommand: "",
|
||||
StatusBadArguments: "",
|
||||
StatusNotImplemented: "",
|
||||
StatusBadSequence: "",
|
||||
StatusNotImplementedParameter: "",
|
||||
StatusNotLoggedIn: "",
|
||||
StatusStorNeedAccount: "",
|
||||
StatusFileUnavailable: "",
|
||||
StatusPageTypeUnknown: "",
|
||||
StatusExceededStorage: "",
|
||||
StatusBadFileName: "",
|
||||
}
|
Loading…
Reference in New Issue
Block a user