commit ef149fb0b873f081dac2431f7e04a6c37bfbfca8 Author: Unbewohnte Date: Sun Jun 6 14:19:57 2021 +0300 First working version diff --git a/FTU b/FTU new file mode 100755 index 0000000..9df16ed Binary files /dev/null and b/FTU differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a08fc5c --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright © 2021 Unbewohne | Nikolay Kasyanov + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..93d6576 --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# FTU (FileTransferringUtility) +## Send files through the Net ! + +--- + +## What is that ? +This application is like an FTP server, but overcomplicated, probably overengineered monstrosity. (basically, a file server, but P2P and only one file is shared). + + +--- + +## Why ? +Learning + +--- + +## How does this work ? +In order to transfer one file on one computer to another - they need to establish a connection. + +In order to establish a connection - there needs to be a 1) server (the owner of the file), waiting for connections, and a 2) client, who will try to connect to a server. If the requirements are met - a client will connect to a server and the packet exchange will begin. + +The server and the client needs to communicate with packets according to certain rules, given by a [protocol](https://github.com/unbewohnte/FTU/protocol/). + +In my implementation there is only one basic packet template with fixed fields. The packets are divided into several groups by its headers, this way my basic packet`s template can be used in many ways, without need of creating a brand-new packet with a different kind of a template. + +Thus, with a connection and a way of communication, the server will send a handshake packet to a client that describes a filename and its size. The client will have the choice of accepting or rejecting the packet. If rejected - the connection will be closed and the program will exit. If accepted - the file will be transfered via packets. + +--- + +## Known issues|problems|lack of features|reasons why it`s bad +1. **VERY** slow +2. **VERY** expensive on resources +3. If `MAXFILEDATASIZE` is bigger than appr. 1024 - the packets on the other end will not be unmarshalled due to error ?? +4. Lack of proper error-handling +5. Lack of information about the process of transferring (ETA, lost packets, etc.) +6. No way to verify if the transferred file is not corrupted +7. No encryption + +## Good points +1. It... works ? + +--- + +## IMPORTANT NOTE +This is NOT intended to be a serious application. I'm learning and this is a product of my curiosity. If you're a beginner too, please don't try to find something useful in my code, I am not an expert. + +Also, this utility only works if both the server and the client have a port-forwarding enabled and configured. Fortunatelly, locally it works without any port-forwarding. + +--- + +## Inspired by [croc](https://github.com/schollz/croc) + +--- + +## License +MIT \ No newline at end of file diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..f0121cd --- /dev/null +++ b/client/client.go @@ -0,0 +1,197 @@ +package client + +import ( + "fmt" + "net" + "os" + "path/filepath" + "strings" + + "github.com/Unbewohnte/FTU/protocol" +) + +// Representation of a tcp client +type Client struct { + DownloadsFolder string + Connection net.Conn + IncomingPackets chan protocol.Packet + Stopped bool + ReadyToReceive bool + PacketCounter uint64 +} + +// Creates a new client with default fields +func NewClient(downloadsFolder string) *Client { + os.MkdirAll(downloadsFolder, os.ModePerm) + + info, err := os.Stat(downloadsFolder) + if err != nil { + panic(err) + } + if !info.IsDir() { + panic("Downloads folder is not a directory") + } + + incomingPacketsChan := make(chan protocol.Packet, 5) + + var PacketCounter uint64 = 0 + fmt.Println("Created a new client") + return &Client{ + DownloadsFolder: downloadsFolder, + Connection: nil, + IncomingPackets: incomingPacketsChan, + Stopped: false, + ReadyToReceive: false, + PacketCounter: PacketCounter, + } +} + +// Closes the connection +func (c *Client) Disconnect() { + c.Connection.Close() +} + +// Closes the connection, warns the server and exits the mainloop +func (c *Client) Stop() { + disconnectionPacket := protocol.Packet{ + Header: protocol.HeaderDisconnecting, + } + protocol.SendPacket(c.Connection, disconnectionPacket) + c.Stopped = true + c.Disconnect() +} + +// Connects to a given address over tcp. Sets a connection to a client +func (c *Client) Connect(addr string) error { + fmt.Printf("Trying to connect to %s...\n", addr) + connection, err := net.Dial("tcp", addr) + if err != nil { + return fmt.Errorf("could not connect to %s: %s", addr, err) + } + c.Connection = connection + fmt.Println("Connected to ", c.Connection.RemoteAddr()) + + return nil +} + +// Handles the fileinfo packet. The choice of acceptance is given to the user +func (c *Client) HandleFileOffer(fileinfoPacket protocol.Packet) error { + + // inform the user about the file + fmt.Printf("Incoming fileinfo packet:\nFilename: %s\nFilesize: %.3fMB\nAccept ? [Y/N]: ", + fileinfoPacket.Filename, float32(fileinfoPacket.Filesize)/1024/1024) + + // get and process the input + var input string + fmt.Scanln(&input) + input = strings.TrimSpace(input) + input = strings.ToLower(input) + + // reject the file + if input != "y" { + rejectionPacket := protocol.Packet{ + Header: protocol.HeaderReject, + } + err := protocol.SendPacket(c.Connection, rejectionPacket) + if err != nil { + return fmt.Errorf("could not send a rejection packet: %s", err) + } + + c.ReadyToReceive = false + + return nil + } + + // accept the file + acceptancePacket := protocol.Packet{ + Header: protocol.HeaderAccept, + } + err := protocol.SendPacket(c.Connection, acceptancePacket) + if err != nil { + return fmt.Errorf("could not send an acceptance packet: %s", err) + } + + // can and ready to receive file packets + c.ReadyToReceive = true + + return nil +} + +// Handles the download by writing incoming bytes into the file +func (c *Client) WritePieceOfFile(filePacket protocol.Packet) error { + c.ReadyToReceive = false + + // open|create a file with the same name as the filepacket`s file name + file, err := os.OpenFile(filepath.Join(c.DownloadsFolder, filePacket.Filename), os.O_CREATE|os.O_APPEND|os.O_WRONLY, os.ModePerm) + if err != nil { + return err + } + // just write the filedata + file.Write(filePacket.FileData) + file.Close() + c.PacketCounter++ + + c.ReadyToReceive = true + + return nil +} + +// Listens in an endless loop; reads incoming packages and puts them into channel +func (c *Client) ReceivePackets() { + for { + incomingPacket := protocol.ReadFromConn(c.Connection) + isvalid, _ := protocol.IsValidPacket(incomingPacket) + if !isvalid { + continue + } + c.IncomingPackets <- incomingPacket + } +} + +// The "head" of the client. Similarly as in server`s logic "glues" everything together. +// Current structure allows the client to receive any type of packet +// in any order and react correspondingly +func (c *Client) MainLoop() { + go c.ReceivePackets() + + for { + if c.Stopped { + // exit the mainloop + break + } + // 1) send -> 2) handle received if necessary + + // send a packet telling server to send another piece of file + if c.ReadyToReceive { + readyPacket := protocol.Packet{ + Header: protocol.HeaderReady, + } + protocol.SendPacket(c.Connection, readyPacket) + c.ReadyToReceive = false + } + + // no incoming packets ? Skipping the packet handling part + if len(c.IncomingPackets) == 0 { + continue + } + + // take the packet and handle depending on the header + incomingPacket := <-c.IncomingPackets + + // handling each packet header differently + switch incomingPacket.Header { + + case protocol.HeaderFileInfo: + go c.HandleFileOffer(incomingPacket) + + case protocol.HeaderFileData: + go c.WritePieceOfFile(incomingPacket) + + case protocol.HeaderDisconnecting: + // the server is ded, no need to stay alive as well + fmt.Println("Done. Got ", c.PacketCounter, " packets in total") + c.Stopped = true + c.Stop() + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b22f201 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/Unbewohnte/FTU + +go 1.16 diff --git a/main.go b/main.go new file mode 100644 index 0000000..d8b0067 --- /dev/null +++ b/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/Unbewohnte/FTU/client" + "github.com/Unbewohnte/FTU/server" +) + +// flags +var PORT *int = flag.Int("port", 8080, "Specifies a port for a server") +var SERVERADDR *string = flag.String("addr", "", "Specifies an IP for connection") +var ISSERVER *bool = flag.Bool("server", false, "Server") +var DOWNLOADSFOLDER *string = flag.String("downloadto", "", "Specifies where the client will store downloaded file") +var SHAREDFILE *string = flag.String("sharefile", "", "Specifies what file server will serve") + +// helpMessage +var HELPMSG string = ` +"-port", default: 8080, Specifies a port for a server +"-addr", default: "", Specifies an IP for connection +"-server", default: false, Share file or connect and receive one ? +"-downloadto", default: "", Specifies where the client will store downloaded file +"-sharefile", default: "", Specifies what file server will share` + +// Input-validation +func checkFlags() { + if *ISSERVER { + if strings.TrimSpace(*SHAREDFILE) == "" { + fmt.Println("No file specified !\n", HELPMSG) + os.Exit(1) + } + if *PORT <= 0 { + fmt.Println("Invalid port !\n", HELPMSG) + os.Exit(1) + } + } else if !*ISSERVER { + if strings.TrimSpace(*SERVERADDR) == "" { + fmt.Println("Invalid IP address !\n", HELPMSG) + os.Exit(1) + } + if strings.TrimSpace(*DOWNLOADSFOLDER) == "" { + *DOWNLOADSFOLDER = "./downloads/" + fmt.Println("Empty downloads folder. Changed to ./downloads/") + } + } +} + +// parse flags, validate given values +func init() { + flag.Parse() + checkFlags() +} + +func main() { + if *ISSERVER { + // 1) create server -> 2) wait for a client ->| + // 3) send handshake packet -> 4) if accepted - upload file + server := server.NewServer(*PORT, *SHAREDFILE) + server.WaitForConnection() + server.MainLoop() + + } else { + // 1) create client -> 2) try to connect to a server -> 3) wait for a handshake ->| + // 4) accept or refuse -> 5) download|don`t_download file + client := client.NewClient(*DOWNLOADSFOLDER) + client.Connect(fmt.Sprintf("%s:%d", *SERVERADDR, *PORT)) + client.MainLoop() + } +} diff --git a/protocol/protocol.go b/protocol/protocol.go new file mode 100644 index 0000000..3900ece --- /dev/null +++ b/protocol/protocol.go @@ -0,0 +1,155 @@ +package protocol + +import ( + "encoding/json" + "fmt" + "net" + "strconv" + "strings" +) + +// a package that describes how server and client should communicate + +const MAXPACKETSIZE int = 2048 // whole packet +const MAXFILEDATASIZE int = 512 // only `FileData` | MUST be less than `MAXPACKETSIZE` +const PACKETSIZEDELIMETER string = "|" + +// Headers +type Header string + +const HeaderFileData Header = "FILEDATA" +const HeaderFileInfo Header = "FILEINFO" +const HeaderReject Header = "FILE_REJECT" +const HeaderAccept Header = "FILE_ACCEPT" +const HeaderReady Header = "READY" +const HeaderDisconnecting Header = "BYE!" + +// Packet structure. +// A Packet without a header is an invalid packet +type Packet struct { + Header Header `json:"Header"` + Filename string `json:"Filename"` + Filesize uint64 `json:"Filesize"` + FileData []byte `json:"Filedata"` +} + +// converts valid packet bytes into `Packet` struct +func ReadPacketBytes(packetBytes []byte) Packet { + // makes sure that the packet is ALWAYS less or equal to the maximum packet size + // this allows not to use any client or server checks + + //fmt.Println("READING packet: ", string(packetBytes)) + + var packet Packet + err := json.Unmarshal(packetBytes, &packet) + if err != nil { + fmt.Printf("Could not unmarshal the packet: %s\n", err) + return Packet{} + } + return packet +} + +// Converts `Packet` struct into []byte +func EncodePacket(packet Packet) []byte { + packetBytes, err := json.Marshal(packet) + if err != nil { + return []byte("") + } + return packetBytes +} + +// Measures the packet length +func MeasurePacket(packet Packet) uint64 { + packetBytes := EncodePacket(packet) + return uint64(len(packetBytes)) +} + +// Checks if given packet is valid, returns a boolean and an explanation message +func IsValidPacket(packet Packet) (bool, string) { + if MeasurePacket(packet) > uint64(MAXPACKETSIZE) { + return false, "Exceeded MAXPACKETSIZE" + } + if len(packet.FileData) > MAXFILEDATASIZE { + return false, "Exceeded MAXFILEDATASIZE" + } + + if strings.TrimSpace(string(packet.Header)) == "" { + return false, "Blank header" + } + return true, "" +} + +// Sends a given packet to connection using a special sending format +// ALL packets MUST be sent by this method +func SendPacket(connection net.Conn, packet Packet) error { + isvalid, msg := IsValidPacket(packet) + if !isvalid { + return fmt.Errorf("this packet is invalid !: %v; The error: %v", packet, msg) + } + + packetSize := MeasurePacket(packet) + + // write packetsize between delimeters (ie: |727|{"HEADER":"PING"...}) + connection.Write([]byte(fmt.Sprintf("%s%d%s", PACKETSIZEDELIMETER, packetSize, PACKETSIZEDELIMETER))) + + // write an actual packet + connection.Write(EncodePacket(packet)) + + //fmt.Println("Sending packet: ", string(EncodePacket(packet)), " Length: ", packetSize) + return nil +} + +// Reads a packet from a connection by retrieving the packet length. Only once +// ASSUMING THAT THE PACKETS ARE SENT BY `SendPacket` method !!!! +func ReadFromConn(connection net.Conn) Packet { + var gotPacketSize bool = false + var delimeterCounter int = 0 + + var packetSizeStr string = "" + var packetSize int = 0 + for { + // still need to get a packetsize + if !gotPacketSize { + // reading byte-by-byte + buffer := make([]byte, 1) + connection.Read(buffer) + + // found a delimeter + if string(buffer) == PACKETSIZEDELIMETER { + delimeterCounter++ + + // the first delimeter is found, skipping the rest of the code + if delimeterCounter == 1 { + continue + } + } + + // found the first delimeter, skip was performed, now reading an actual packetsize + if delimeterCounter == 1 { + packetSizeStr += string(buffer) + } else if delimeterCounter == 2 { + // found the last delimeter, thus already read the whole packetsize + packetSize, _ = strconv.Atoi(packetSizeStr) + gotPacketSize = true + } + // skipping the rest of the code because we don`t know the packet size yet + continue + } + // have a packetsize, now reading the whole packet + + //fmt.Println("Got a packetsize!: ", packetSize) + packetBuffer := make([]byte, packetSize) + connection.Read(packetBuffer) + + packet := ReadPacketBytes(packetBuffer) + + isvalid, _ := IsValidPacket(packet) + if isvalid { + return packet + } + + break + } + + return Packet{} +} diff --git a/server/file.go b/server/file.go new file mode 100644 index 0000000..396ef13 --- /dev/null +++ b/server/file.go @@ -0,0 +1,37 @@ +package server + +import ( + "fmt" + "os" +) + +// Struct that represents the served file. Used internally in the server +type File struct { + path string + Filename string + Filesize uint64 + SentBytes uint64 + LeftBytes uint64 + SentPackets uint64 + Handler *os.File +} + +// Prepares a file for serving. Used for preparing info before sending a handshake +func getFile(path string) (*File, error) { + info, err := os.Stat(path) + if err != nil { + return nil, fmt.Errorf("could not get a fileinfo: %s", err) + } + handler, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("couldn`t be able to open the file: %s", err) + } + return &File{ + path: path, + Filename: info.Name(), + Filesize: uint64(info.Size()), + SentBytes: 0, + LeftBytes: uint64(info.Size()), + Handler: handler, + }, nil +} diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..64d93f4 --- /dev/null +++ b/server/server.go @@ -0,0 +1,236 @@ +package server + +import ( + "fmt" + "io" + "net" + "net/http" + + "github.com/Unbewohnte/FTU/protocol" +) + +// gets a local ip. Borrowed from StackOverflow, thank you, whoever I brought it from +func GetLocalIP() (string, error) { + conn, err := net.Dial("udp", "8.8.8.8:80") + if err != nil { + return "", err + } + defer conn.Close() + + localAddr := conn.LocalAddr().(*net.UDPAddr) + + return string(localAddr.IP), nil +} + +// gets a remote ip. Borrowed from StackOverflow, thank you, whoever I brought it from +func GetRemoteIP() (string, error) { + resp, err := http.Get("https://api.ipify.org?format=text") + if err != nil { + return "", fmt.Errorf("could not make a request to get your remote IP: %s", err) + } + defer resp.Body.Close() + ip, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("could not read a response: %s", err) + } + return string(ip), nil +} + +// The main server struct +type Server struct { + Port int + FileToTransfer *File + Listener net.Listener + Connection net.Conn + IncomingPackets chan protocol.Packet + CanTransfer bool + Stopped bool +} + +// Creates a new server with default fields +func NewServer(port int, filepath string) *Server { + fileToTransfer, err := getFile(filepath) + if err != nil { + panic(err) + } + + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) + if err != nil { + panic(err) + } + incomingPacketsChan := make(chan protocol.Packet, 5) + + remoteIP, err := GetRemoteIP() + if err != nil { + panic(err) + } + localIP, err := GetLocalIP() + if err != nil { + panic(err) + } + + fmt.Printf("Created a new server at %s:%d (remote)\n%s:%d (local)\n", remoteIP, port, localIP, port) + return &Server{ + Port: port, + FileToTransfer: fileToTransfer, + Listener: listener, + Connection: nil, + IncomingPackets: incomingPacketsChan, + Stopped: false, + } +} + +// Closes the connection, warns about it the client and exits the mainloop +func (s *Server) Stop() { + disconnectionPacket := protocol.Packet{ + Header: protocol.HeaderDisconnecting, + } + err := protocol.SendPacket(s.Connection, disconnectionPacket) + if err != nil { + panic(fmt.Sprintf("could not send a disconnection packet: %s", err)) + } + + s.Stopped = true + s.Disconnect() +} + +// Closes current connection +func (s *Server) Disconnect() { + s.Connection.Close() +} + +// Accepts one connection +func (s *Server) WaitForConnection() error { + connection, err := s.Listener.Accept() + if err != nil { + return fmt.Errorf("could not accept a connection: %s", err) + } + s.Connection = connection + fmt.Println("New connection from ", s.Connection.RemoteAddr()) + + return nil +} + +// Closes the listener. Used only when there is still no connection from `AcceptConnections` +func (s *Server) StopListening() { + s.Listener.Close() +} + +// Sends a packet with all information about a file to current connection +func (s *Server) SendOffer() error { + err := protocol.SendPacket(s.Connection, protocol.Packet{ + Header: protocol.HeaderFileInfo, + Filename: s.FileToTransfer.Filename, + Filesize: s.FileToTransfer.Filesize, + }) + if err != nil { + return fmt.Errorf("could not send an information about the file: %s", err) + } + + return nil +} + +// Sends one file packet to the client +func (s *Server) SendPiece() error { + // if no data to send - exit + if s.FileToTransfer.LeftBytes == 0 { + fmt.Printf("Done. Sent %d file packets\n", s.FileToTransfer.SentPackets) + s.Stop() + } + + fileBytes := make([]byte, protocol.MAXFILEDATASIZE) + // if there is less data to send than the limit - create a buffer of needed size + if s.FileToTransfer.LeftBytes < uint64(protocol.MAXFILEDATASIZE) { + fileBytes = make([]byte, protocol.MAXFILEDATASIZE-(protocol.MAXFILEDATASIZE-int(s.FileToTransfer.LeftBytes))) + } + + // reading bytes from the point where we left + read, err := s.FileToTransfer.Handler.ReadAt(fileBytes, int64(s.FileToTransfer.SentBytes)) + if err != nil { + return fmt.Errorf("could not read from a file: %s", err) + } + + // constructing a file packet and sending it + fileDataPacket := protocol.Packet{ + Header: protocol.HeaderFileData, + Filename: s.FileToTransfer.Filename, + Filesize: s.FileToTransfer.Filesize, + FileData: fileBytes, + } + + err = protocol.SendPacket(s.Connection, fileDataPacket) + if err != nil { + return fmt.Errorf("could not send a file packet : %s", err) + } + + // doing a "logging" for the next time + s.FileToTransfer.LeftBytes -= uint64(read) + s.FileToTransfer.SentBytes += uint64(read) + s.FileToTransfer.SentPackets++ + + return nil +} + +// Listens in an endless loop; reads incoming packages and puts them into channel +func (s *Server) ReceivePackets() { + for { + incomingPacket := protocol.ReadFromConn(s.Connection) + isvalid, _ := protocol.IsValidPacket(incomingPacket) + if !isvalid { + continue + } + s.IncomingPackets <- incomingPacket + } +} + +// The "head" of the server. "Glues" all things together. +// Current structure allows the server to receive any type of packet +// in any order and react correspondingly +func (s *Server) MainLoop() { + + go s.ReceivePackets() + + // send an information about the shared file to the client + s.SendOffer() + + for { + if s.Stopped { + // exit the mainloop + break + } + // send, then process received packets + + // no incoming packets ? Skipping the packet handling part + if len(s.IncomingPackets) == 0 { + continue + } + + incomingPacket := <-s.IncomingPackets + + // handling each packet header differently + switch incomingPacket.Header { + + case protocol.HeaderAccept: + fmt.Printf("Client has accepted the transfer !\n") + // allowed to send file packets + s.CanTransfer = true + + case protocol.HeaderReject: + fmt.Println("Client has rejected the transfer") + s.Stop() + + // client is ready to receive the next file packet + case protocol.HeaderReady: + if !s.CanTransfer { + break + } + err := s.SendPiece() + if err != nil { + fmt.Println(err) + } + + case protocol.HeaderDisconnecting: + s.Stop() + } + } +}