First commit.

This commit is contained in:
jlaffaye 2011-05-07 00:29:10 +01:00
commit bcc332e95c
5 changed files with 395 additions and 0 deletions

13
LICENSE Normal file
View 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
View 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
View 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
View 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
View 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: "",
}