ex8.2:要求实现一个ftp服务器,支持cd,ls,put,get等命令,目前实现了用户身份简单确认,获取家目录后可以进行cd,ls,mkdir以及上传和下载文件。
TODO:
1)未实现输入密码时不回显(类似C里的getpass函数);
2)不支持文件夹的上传与下载;
3)未实现与linux用户权限管理保持一致。
目录结构:
----go工作目录/gobook/ch8/ex8.2
--------client
------------ftp
----------------ftp.go
------------client.go
--------ftp
------------ftp.go
--------server
------------ftp
----------------ftp.go
------------server.go
代码实现:
ftp/ftp.go
package ftp import ( "encoding/binary" "net" "unsafe" ) var Commands = map[string]uint8{ "cd": uint8(1), "ls": uint8(2), "exit": uint8(3), "mkdir": uint8(4), "put": uint8(5), "get": uint8(6), } type FtpConn struct { Con net.Conn Cwd string Home string Exit bool } func (ftpCon *FtpConn) Write(content []byte) error { var length uint32 length = uint32(len(content)) if length == 0 { return binary.Write(ftpCon.Con, binary.LittleEndian, &length) } length = length + uint32(binary.Size(length)) err := binary.Write(ftpCon.Con, binary.LittleEndian, &length) if err != nil { return err } err = binary.Write(ftpCon.Con, binary.LittleEndian, content) if err != nil { return err } return nil } // string转[]byte // 利用string本来的底层数组 func Str2sbyte(s string) (b []byte) { *(*string)(unsafe.Pointer(&b)) = s *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&b)) + 2*unsafe.Sizeof(&b))) = len(s) return } // []byte转string // 利用[]byte本来的底层数组 func Sbyte2str(b []byte) string { return *(*string)(unsafe.Pointer(&b)) }
client/client.go
package main import ( "bufio" "encoding/binary" "errors" "fmt" "log" "net" "os" "strings" "gobook/ch8/ex8.2/client/ftp" "gobook/ch8/ex8.2/ftp" ) func printHelp() { log.Println("Help: [command] [args] cd [path] ") } func handleCommand(ftpCon *client.FtpClient, command string, args []string) (err error) { cmdid, ok := ftp.Commands[command] if !ok { return errors.New("unsupported command ") } err = ftpCon.WriteCommand(cmdid, args) if err != nil { return err } if cmdid == ftp.Commands["get"] { err = ftpCon.HandleGet(args[0]) if err != nil { return err } } var length uint32 err = binary.Read(ftpCon.Con, binary.LittleEndian, &length) if err != nil { return err } if length == 0 { fmt.Printf(" %s:", ftpCon.Cwd) return nil } res := make([]byte, length-uint32(binary.Size(length))) err = binary.Read(ftpCon.Con, binary.LittleEndian, res) if err != nil { return err } if cmdid == ftp.Commands["cd"] { ftpCon.Cwd = ftp.Sbyte2str(res) fmt.Printf(" %s:", ftpCon.Cwd) return nil } if cmdid == ftp.Commands["exit"] { ftpCon.Exit = true fmt.Printf("%s ", ftp.Sbyte2str(res)) return nil } fmt.Printf("%s %s:", ftp.Sbyte2str(res), ftpCon.Cwd) return } func main() { // 获取用户身份信息与ftp服务器host信息 if len(os.Args) < 2 { fmt.Println("无法通过身份认证") return } arg := os.Args[1] if !strings.Contains(arg, "@") { fmt.Println("无法通过身份认证") return } args := strings.Split(arg, "@") user := args[0] host := args[1] fmt.Print("Password:") var pwd string input := bufio.NewScanner(os.Stdin) if input.Scan() { pwd = input.Text() } // 连接到ftp服务器 con, err := net.Dial("tcp", host) if err != nil { fmt.Println(err) return } defer con.Close() ftpCon := ftp.FtpConn{ Con: con, } ftpClient := client.FtpClient{ ftpCon, } // 身份验证 err = ftpClient.Write(ftp.Str2sbyte(user)) if err != nil { fmt.Println(err) return } err = ftpClient.Write(ftp.Str2sbyte(pwd)) if err != nil { fmt.Println(err) return } var res uint32 err = binary.Read(con, binary.LittleEndian, &res) if err != nil { fmt.Println(err) return } if res == 0 { fmt.Println("认证失败") return } cwd := make([]byte, res) err = binary.Read(con, binary.LittleEndian, cwd) if err != nil { fmt.Println(err) return } ftpClient.Cwd = ftp.Sbyte2str(cwd) ftpClient.Home = ftpCon.Cwd fmt.Println(ftpClient.Cwd, ":") // 监听命令行输入 for input.Scan() && !ftpClient.Exit { argstr := input.Text() args := strings.Split(strings.TrimSpace(argstr), " ") if len(args) == 0 { printHelp() continue } command := args[0] if len(args) > 1 { args = args[1:] } else { args = nil } err = handleCommand(&ftpClient, command, args) if err != nil { log.Println(err) } } }
client/ftp/ftp.go
package client import ( "bufio" "encoding/binary" "errors" "io" "os" "path" "strings" "gobook/ch8/ex8.2/ftp" ) type FtpClient struct { ftp.FtpConn } func (ftpCon *FtpClient) WriteCommand(cmdid uint8, args []string) error { if cmdid == ftp.Commands["put"] { return ftpCon.WritePut(cmdid, args[0]) } var length uint32 argstr := strings.Join(args, "") length = uint32(binary.Size(length)+binary.Size(cmdid)) + uint32(len(argstr)) err := binary.Write(ftpCon.Con, binary.LittleEndian, length) if err != nil { return err } err = binary.Write(ftpCon.Con, binary.LittleEndian, cmdid) if err != nil { return err } err = binary.Write(ftpCon.Con, binary.LittleEndian, ftp.Str2sbyte(argstr)) if err != nil { return err } return nil } func (ftpCon *FtpClient) WritePut(cmdid uint8, filePath string) error { filePath = strings.Replace(filePath, "\", "/", -1) f, err := os.Open(filePath) if err != nil { return err } defer f.Close() // 发送命令与文件名 var length uint32 fileName := ftp.Str2sbyte(path.Base(filePath)) length = uint32(binary.Size(length)+binary.Size(cmdid)) + uint32(len(fileName)) err = binary.Write(ftpCon.Con, binary.LittleEndian, length) if err != nil { return err } err = binary.Write(ftpCon.Con, binary.LittleEndian, cmdid) if err != nil { return err } err = binary.Write(ftpCon.Con, binary.LittleEndian, fileName) if err != nil { return err } // 发送文件长度 fileInfo, err := f.Stat() if err != nil { return err } if fileInfo.IsDir() { return errors.New("put 命令不支持发送文件夹,请尝试putdir命令") } else { err = binary.Write(ftpCon.Con, binary.LittleEndian, fileInfo.Size()) if err != nil { return err } } // 发送文件内容 buf := make([]byte, 4096) bufReader := bufio.NewReader(f) for { n, err := bufReader.Read(buf) if err != nil { if err == io.EOF { break } return err } err = binary.Write(ftpCon.Con, binary.LittleEndian, buf[0:n]) if err != nil { return err } } return nil } func (ftpCon *FtpClient) HandleGet(filePath string) error { fileName := path.Base(filePath) f, err := os.Create(fileName) if err != nil { if os.IsExist(err) { err = f.Truncate(0) if err != nil { return err } } else { return err } } defer f.Close() var length int64 err = binary.Read(ftpCon.Con, binary.LittleEndian, &length) if err != nil { return err } var total, bufSize int64 if length > 4096 { bufSize = 4096 } else { bufSize = length } buf := make([]byte, bufSize) for total < length { err = binary.Read(ftpCon.Con, binary.LittleEndian, buf) if err != nil { return err } n, err := f.Write(buf) if err != nil { return err } total += int64(n) if length-total < bufSize { buf = buf[0 : length-total] } } return nil }
server/server.go
// ftp server package main import ( "encoding/binary" "log" "net" "gobook/ch8/ex8.2/ftp" "gobook/ch8/ex8.2/server/ftp" ) func handleFunc(con net.Conn) { defer con.Close() // 身份验证 // 读取用户名 var length uint32 err := binary.Read(con, binary.LittleEndian, &length) if err != nil { err = binary.Write(con, binary.LittleEndian, uint32(0)) if err != nil { log.Println(err) } return } user := make([]byte, length-uint32(binary.Size(length))) err = binary.Read(con, binary.LittleEndian, user) if err != nil { err = binary.Write(con, binary.LittleEndian, uint32(0)) if err != nil { log.Println(err) } return } // 读取密码 err = binary.Read(con, binary.LittleEndian, &length) if err != nil { err = binary.Write(con, binary.LittleEndian, uint32(0)) if err != nil { log.Println(err) } return } pwd := make([]byte, length-uint32(binary.Size(length))) err = binary.Read(con, binary.LittleEndian, pwd) if err != nil { err = binary.Write(con, binary.LittleEndian, uint32(0)) if err != nil { log.Println(err) } return } // 验证用户名密码获取家目录 validated, cwd := server.Validate(ftp.Sbyte2str(user), ftp.Sbyte2str(pwd)) if !validated { err = binary.Write(con, binary.LittleEndian, uint32(0)) if err != nil { log.Println(err) } return } home := ftp.Str2sbyte(cwd) err = binary.Write(con, binary.LittleEndian, uint32(binary.Size(home))) if err != nil { log.Println(err) return } err = binary.Write(con, binary.LittleEndian, home) if err != nil { log.Println(err) return } ftpCon := ftp.FtpConn{ Con: con, Home: cwd, Cwd: cwd, } ftpServer := server.FtpServer{ ftpCon, } // 循环监听命令请求 for !ftpServer.Exit { var length uint32 err = binary.Read(con, binary.LittleEndian, &length) if err != nil { log.Println(err) return } var cmdid uint8 err = binary.Read(con, binary.LittleEndian, &cmdid) if err != nil { log.Println(err) return } args := make([]byte, length-uint32(binary.Size(cmdid))-uint32(binary.Size(length))) err = binary.Read(con, binary.LittleEndian, args) if err != nil { log.Println(err) return } switch cmdid { case ftp.Commands["cd"]: err = ftpServer.HandleCd(args) case ftp.Commands["ls"]: err = ftpServer.HandleLs(args) case ftp.Commands["exit"]: err = ftpServer.HandleExit(args) case ftp.Commands["mkdir"]: err = ftpServer.HandleMkdir(args) case ftp.Commands["put"]: err = ftpServer.HandlePut(args) case ftp.Commands["get"]: err = ftpServer.HandleGet(args) default: err = ftpServer.Write([]byte("no command handler.")) } if err != nil { log.Println(err) } } } func main() { listener, err := net.Listen("tcp", "localhost:5900") if err != nil { log.Fatal(err) } for { con, err := listener.Accept() if err != nil { log.Println(err) continue } go handleFunc(con) } }
server/ftp/ftp.go
package server import ( "bufio" "encoding/binary" "fmt" "io" "log" "os" "path" "runtime" "strings" "sync" "gobook/ch8/ex8.2/ftp" ) var Commands = map[string]uint8{ "cd": uint8(1), "ls": uint8(2), "exit": uint8(3), "mkdir": uint8(4), "put": uint8(5), "get": uint8(6), } var DefaultDir = map[string]string{ "windows": "C:/Users/Kylin/workspace/go/src/gobook/ch8", "unix": "home/www", } type userInfo struct { name string pwd string home string } var lock sync.Once // 初始化users一次 var users []userInfo func init() { lock.Do(initUsers) } type FtpServer struct { ftp.FtpConn } func (ftpCon *FtpServer) HandleCd(args []byte) error { cwd := ftp.Sbyte2str(args) if strings.HasPrefix(cwd, "/") { cwd = path.Join(ftpCon.Cwd, cwd) } f, err := os.Open(cwd) if err != nil { ftpCon.Write(ftp.Str2sbyte(err.Error())) return nil } defer f.Close() finfo, err := f.Stat() if err != nil { ftpCon.Write(ftp.Str2sbyte(err.Error())) return nil } if !finfo.IsDir() { ftpCon.Write(ftp.Str2sbyte("cd parameter must be directory.")) return nil } ftpCon.Cwd = cwd return ftpCon.Write(ftp.Str2sbyte(cwd)) } func (ftpCon *FtpServer) HandleLs(args []byte) error { cwd := ftp.Sbyte2str(args) if strings.HasPrefix(cwd, "/") { cwd = path.Join(ftpCon.Cwd, cwd) } f, err := os.Open(cwd) if err != nil { ftpCon.Write(ftp.Str2sbyte(err.Error())) return nil } finfo, err := f.Stat() if err != nil { ftpCon.Write(ftp.Str2sbyte(err.Error())) return nil } if finfo.IsDir() { finfos, err := f.Readdir(0) if err != nil { ftpCon.Write(ftp.Str2sbyte(err.Error())) } var res string res = fmt.Sprintf("Total:%d ", len(finfos)) for _, info := range finfos { res = res + fmt.Sprintf("%.30s %.10d %s ", info.Name(), info.Size(), info.ModTime()) } err = ftpCon.Write(ftp.Str2sbyte(res)) } else { res := fmt.Sprintf("%.30s %.10d %s ", finfo.Name(), finfo.Size(), finfo.ModTime()) err = ftpCon.Write(ftp.Str2sbyte(res)) } if err != nil { err = ftpCon.Write(ftp.Str2sbyte(err.Error())) } return err } func (ftpCon *FtpServer) HandleExit(args []byte) error { ftpCon.Exit = true ftpCon.Write(ftp.Str2sbyte("Byebye.")) return nil } func (ftpCon *FtpServer) HandleMkdir(args []byte) error { dir := ftp.Sbyte2str(args) if strings.HasPrefix(dir, "/") { dir = path.Join(ftpCon.Home, dir) } else { dir = path.Join(ftpCon.Cwd, dir) } err := os.Mkdir(dir, os.ModePerm) if err != nil { return err } return ftpCon.Write(ftp.Str2sbyte("Ok")) } func (ftpCon *FtpServer) HandlePut(args []byte) error { fileName := ftp.Sbyte2str(args) f, err := os.Create(path.Join(ftpCon.Cwd, fileName)) if err != nil { return err } defer f.Close() var length int64 err = binary.Read(ftpCon.Con, binary.LittleEndian, &length) if err != nil { return err } var total, bufSize int64 if length > 4096 { bufSize = 4096 } else { bufSize = length } buf := make([]byte, bufSize) for total < length { err = binary.Read(ftpCon.Con, binary.LittleEndian, buf) if err != nil { return err } n, err := f.Write(buf) if err != nil { return err } total += int64(n) if (length - total) < bufSize { buf = buf[0 : length-total] } } ftpCon.Write(ftp.Str2sbyte("Ok.")) return nil } func (ftpCon *FtpServer) HandleGet(args []byte) error { filePath := ftp.Sbyte2str(args) if strings.HasPrefix(filePath, "/") { filePath = path.Join(ftpCon.Home, filePath) } else { filePath = path.Join(ftpCon.Cwd, filePath) } f, err := os.Open(filePath) if err != nil { return err } defer f.Close() finfo, err := f.Stat() if err != nil { return err } // TODO 暂不支持下载文件夹 if finfo.IsDir() { return binary.Write(ftpCon.Con, binary.LittleEndian, int64(0)) } err = binary.Write(ftpCon.Con, binary.LittleEndian, finfo.Size()) if err != nil { return err } bufReader := bufio.NewReader(f) buf := make([]byte, 4096) for { n, err := bufReader.Read(buf) if err != nil { if err == io.EOF { break } else { return err } } err = binary.Write(ftpCon.Con, binary.LittleEndian, buf[0:n]) if err != nil { return err } } ftpCon.Write(ftp.Str2sbyte("Ok.")) return nil } func initUsers() { cwd, ok := DefaultDir[runtime.GOOS] if !ok { log.Fatal("Unsupported system.") } // TODO 打开相对路径的问题 f, err := os.Open("C:/Users/Kylin/workspace/go/src/gobook/ch8/ex8.2/server/ftp/users") if err != nil { log.Fatal("failed to load users' information.", err) } defer f.Close() scanner := bufio.NewScanner(f) for scanner.Scan() { line := scanner.Text() userinfo := strings.Split(line, ";;") if len(userinfo) < 3 { continue } home := path.Join(cwd, userinfo[2]) f, err := os.Open(home) if err != nil && os.IsNotExist(err) { err = os.Mkdir(home, os.ModePerm) if err != nil { log.Fatal("failed to make directory", home) } } else { f.Close() } users = append(users, userInfo{userinfo[0], userinfo[1], home}) } } // 验证用户名和密码,返回验证结果true/false和验证通过后的用户家目录 func Validate(name string, pwd string) (pass bool, home string) { if len(users) <= 0 { return } for _, info := range users { if info.name == name && info.pwd == pwd { return true, info.home } } return }