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