Go程序設計語言練習題-第8章(8.2)

ex8.2:要求實現一個ftp服務器,支持cd,ls,put,get等命令,目前實現了用戶身份簡單確認,獲取家目錄後能夠進行cd,ls,mkdir以及上傳和下載文件。linux

TODO:windows

1)未實現輸入密碼時不回顯(相似C裏的getpass函數);數組

2)不支持文件夾的上傳與下載;服務器

3)未實現與linux用戶權限管理保持一致。app

目錄結構:tcp

----go工做目錄/gobook/ch8/ex8.2
--------client
------------ftp
----------------ftp.go
------------client.go
--------ftp ------------ftp.go
--------server ------------ftp ----------------ftp.go ------------server.go

 

代碼實現:ide

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))
}
View Code

client/client.goui

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:\t[command] [args]\ncd [path]\n")
}

func handleCommand(ftpCon *client.FtpClient, command string, args []string) (err error) {
    cmdid, ok := ftp.Commands[command]
    if !ok {
        return errors.New("unsupported command\n")
    }
    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("\n%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("\n%s:", ftpCon.Cwd)
        return nil
    }
    if cmdid == ftp.Commands["exit"] {
        ftpCon.Exit = true
        fmt.Printf("%s\n", ftp.Sbyte2str(res))
        return nil
    }

    fmt.Printf("%s\n%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)
        }
    }

}
View Code

client/ftp/ftp.gospa

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
}
View Code

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)
    }
}
View Code

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\n", len(finfos))
        for _, info := range finfos {
            res = res + fmt.Sprintf("%.30s\t%.10d\t%s\n", info.Name(), info.Size(), info.ModTime())
        }
        err = ftpCon.Write(ftp.Str2sbyte(res))
    } else {
        res := fmt.Sprintf("%.30s\t%.10d\t%s\n", 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
}
View Code
相關文章
相關標籤/搜索