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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user