diff --git a/LICENSE b/LICENSE index a08fc5c..c82adc3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright © 2021 Unbewohne | Nikolay Kasyanov +Copyright © 2021 Unbewohne | Kasyanov Nikolay Alexeevich 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: diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6986e0f --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +.DEFAULT_GOAL := all + +SRC_DIR := src/ +EXE_NAME := ftu +INSTALLATION_DIR := /usr/local/bin/ + +all: + cd $(SRC_DIR) && go build && mv $(EXE_NAME) .. + +race: + cd $(SRC_DIR) && go build -race && mv $(EXE_NAME) .. + +install: all + cp $(EXE_NAME) $(INSTALLATION_DIR) + +clean: + rm $(EXE_NAME) \ No newline at end of file diff --git a/README.md b/README.md index c0dec36..14a103a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# FTU (FileTransferringUtility) +# ftu (FileTransferringUtility) ## Send files through the Net ! --- @@ -25,65 +25,59 @@ Thus, with a connection and a way of communication, the sender will send some pa --- -## ● Known issues|problems|lack of features|reasons why it`s bad -- ~~**VERY** slow~~ -- ~~**VERY**~~ expensive on resources -- ~~Lack of proper error-handling~~ -- ~~Lack of information about the process of transferring~~ -- ~~No way to verify if the transferred file is not corrupted~~ -- ~~No encryption~~ -- ~~No tests~~ -- ~~No interrupt signal handling~~ - -## ● Good points -- It works. - ---- - ## ● Installation ### ● From release (Pre-compiled) - Proceed to [releases page](https://github.com/Unbewohnte/ftu/releases) - Choose a version/architecture you have and download an archive - Unpack an archive +- If on GNU/Linux - run `sudo make install` ### ● From source (Compile it yourself) (You need [Go](https://golang.org/dl/) and [git](https://git-scm.com/) to be installed on your machine) - `git clone https://github.com/Unbewohnte/ftu.git` - `cd` into the folder -- `go build` - to simply compile for your OS/ARCHITECTURE || `CGO_ENABLED=0 go build` - to compile a static executable - -### ● Final steps (optional) -- `cd` into folder if you`re not there already -- `chmod +x install.sh` - make installation script executable -- `sudo ./install.sh` +- `make` +- If on GNU/Linux - run `sudo make install` Now you have ftu installed ! --- ## ● Usage -`ftu [FLAGS_HERE]` +`ftu -h` - to print a usage message -### ● Flags -`ftu --help` - to get all flags` description +`ftu [FLAGS]` -- `-port` (int) - specifies a working port (if sending - listens on this port, else - tries to connect to this port); -- `-addr` (string) - specifies an address to connect to; -- `-sharefile` (string) - specifies path to a file you want to share, if given a valid path - sender will offer to download this file to receiver; -- `-downloadto` (string) - specifies path to a folder where the receiver wants to store downloaded file; +### ● FLAGs +- -p [Uinteger_here] for port +- -r [true|false] for recursive sending of a directory +- -a [ip_address|domain_name] address to connect to (cannot be used with -s) +- -d [path_to_directory] where the files will be downloaded to (cannot be used with -s) +- -s [path_to_file|directory] to send it (cannot be used with -a) +- -l for license text ### ● Examples -- `ftu -sharefile="/home/some_path_here/FILETOSHARE.zip"` - creates a server that will share `FILETOSHARE.zip` on port `8080` -- `ftu -sharefile="/home/some_path_here/FILETOSHARE.zip" - port=727` - same as before, but on port `727` -- `ftu -downloadto="/home/some_path_here/Downloads/" -addr="192.168.1.104"` - creates a client (receiver) that will try to connect to `192.168.1.104` (local device) on port `8080` and if successful - downloads a file to given path -- `ftu -downloadto="/home/some_path_here/Downloads/" -addr=145.125.53.212 -port=8888` - same as before, but will try to connect to `145.125.53.212` on port `8888` +`ftu -p 89898 -s /home/user/Downloads/someVideo.mp4` +creates a node on a non-default port 89898 that will send "someVideo.mp4" to the other node that connects to you + +`ftu -p 7277 -a 192.168.1.104 -d .` +creates a node that will connect to 192.168.1.104:7277 and download served file|directory to the working directory + +`ftu -p 7277 -a 192.168.1.104 -d /home/user/Downloads/` +creates a node that will connect to 192.168.1.104:7277 and download served file|directory to "/home/user/Downloads/" + +`ftu -s /home/user/homework` +creates a node that will send every file in the directory + +`ftu -r -s /home/user/homework/` +creates a node that will send every file in the directory !RECUSRIVELY! --- ## ● Testing -In 'ftu' directory: +In 'src' directory: - `go test ./...` - to test everything - `go test -v ./...` - to test everything, with additional information @@ -106,7 +100,5 @@ Also, this utility only works if the server side has a port-forwarding|virtual s MIT ## ● TODO -- multiple filepaths as args, not as a flag -- send all files in a directory -- send all files in a directory recursively -- ip address as an arg, not as a flag \ No newline at end of file +- Send directory +- Wire back encryption \ No newline at end of file diff --git a/checksum/checksum.go b/checksum/checksum.go deleted file mode 100644 index 609d84d..0000000 --- a/checksum/checksum.go +++ /dev/null @@ -1,106 +0,0 @@ -package checksum - -import ( - "crypto/sha256" - "fmt" - "io" - "os" -) - -const CHECKSUMLEN uint = 32 - -type CheckSum [CHECKSUMLEN]byte - -// returns a checksum of given file. NOTE, that it creates checksum -// not of a full file (from all file bytes), but from separate byte blocks. -// This is done as an optimisation because the file can be very large in size. -// The general idea: -// BOF... CHUNK -> STEP -> CHUNK... EOF -// checksum := sha256.Sum256(ALLCHUNKS) -// GetPartialCheckSum is default method used to get a file checksum by sender and receiver -func GetPartialCheckSum(file *os.File) (CheckSum, error) { - // "capturing" CHUNKSIZE bytes and then skipping STEP bytes before the next chunk until the last one - const CHUNKS uint = 100 - const CHUNKSIZE uint = 100 - const STEP uint = 250 - - fileStats, err := file.Stat() - if err != nil { - return [CHECKSUMLEN]byte{}, fmt.Errorf("could not get the stats: %s", err) - } - - fileSize := fileStats.Size() - - if fileSize < int64(CHUNKS*CHUNKSIZE+STEP*(CHUNKS-1)) { - // file is too small to chop it in chunks, so just doing full checksum - - checksum, err := getFullCheckSum(file) - if err != nil { - return [CHECKSUMLEN]byte{}, err - } - return checksum, nil - } - - var capturedChunks string - var read uint64 = 0 - for i := 0; uint(i) < CHUNKS; i++ { - buffer := make([]byte, CHUNKSIZE) - r, _ := file.ReadAt(buffer, int64(read)) - - capturedChunks += string(buffer) - - read += uint64(r) - read += uint64(STEP) - } - - checksum := sha256.Sum256([]byte(capturedChunks)) - return checksum, nil -} - -// Returns a sha256 checksum of given file -func getFullCheckSum(file *os.File) (CheckSum, error) { - filebytes, err := io.ReadAll(file) - if err != nil { - return [CHECKSUMLEN]byte{}, fmt.Errorf("could not read the file: %s", err) - } - checksum := sha256.Sum256(filebytes) - - return checksum, nil -} - -// Simply compares 2 given checksums. If they are equal - returns true -func AreEqual(checksum1, checksum2 CheckSum) bool { - var i int = 0 - for _, checksum1Byte := range checksum1 { - checksum2Byte := checksum2[i] - if checksum1Byte != checksum2Byte { - return false - } - i++ - } - return true -} - -// Tries to convert given bytes into CheckSum type -func BytesToChecksum(bytes []byte) (CheckSum, error) { - if uint(len(bytes)) > CHECKSUMLEN { - return CheckSum{}, fmt.Errorf("provided bytes` length is bigger than the checksum`s") - } else if uint(len(bytes)) < CHECKSUMLEN { - return CheckSum{}, fmt.Errorf("provided bytes` length is smaller than needed") - } - - var checksum [CHECKSUMLEN]byte - for index, b := range bytes { - checksum[index] = b - } - return CheckSum(checksum), nil -} - -// Converts given checksum into []byte -func ChecksumToBytes(checksum CheckSum) []byte { - var checksumBytes []byte - for _, b := range checksum { - checksumBytes = append(checksumBytes, b) - } - return checksumBytes -} diff --git a/checksum/checksum_test.go b/checksum/checksum_test.go deleted file mode 100644 index 8bacdde..0000000 --- a/checksum/checksum_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package checksum - -import ( - "testing" -) - -func TestBytesToChecksum(t *testing.T) { - invalidChecksumBytes := []byte("LESSTHAN32") - _, err := BytesToChecksum(invalidChecksumBytes) - if err == nil { - t.Error("BytesToChecksum failed: expected an error") - } - - invalidChecksumBytes = []byte("BIGGERTHAN32_IFJOWIJFOIHJGLVKNS'O[DFJQWG[OJHNE[OJGNJOREG") - _, err = BytesToChecksum(invalidChecksumBytes) - if err == nil { - t.Error("BytesToChecksum failed: expected an error") - } - - validChecksumBytes := []byte{5, 194, 47, 217, 251, 195, 69, 230, 216, 121, 253, 38, - 116, 68, 152, 68, 103, 226, 16, 58, 235, 47, 6, 55, 27, 20, 83, 152, 89, 38, 59, 29} - _, err = BytesToChecksum(validChecksumBytes) - if err != nil { - t.Errorf("BytesToChecksum failed: not expected an error, got : %s; length of given bytes: %d", err, len(validChecksumBytes)) - } -} - -func TestChecksumToBytes(t *testing.T) { - validChecksumBytes := []byte{5, 194, 47, 217, 251, 195, 69, 230, 216, 121, 253, 38, - 116, 68, 152, 68, 103, 226, 16, 58, 235, 47, 6, 55, 27, 20, 83, 152, 89, 38, 59, 29} - - var validChecksum CheckSum = CheckSum{5, 194, 47, 217, 251, 195, 69, 230, 216, 121, 253, 38, - 116, 68, 152, 68, 103, 226, 16, 58, 235, 47, 6, 55, 27, 20, 83, 152, 89, 38, 59, 29} - - result := ChecksumToBytes(validChecksum) - - for index, b := range result { - if b != validChecksumBytes[index] { - t.Errorf("ChecksumToBytes failed, invalid result") - } - } -} diff --git a/install.sh b/install.sh deleted file mode 100755 index 9ad9567..0000000 --- a/install.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -EXECUTABLE_NAME=ftu -DESTDIR=/usr/local/bin/ - -# if ftu is in the same directory - copy it to $DESTDIR -if [ -e $EXECUTABLE_NAME ] -then - cp $EXECUTABLE_NAME $DESTDIR -else - echo "No '${EXECUTABLE_NAME}' in current directory !" -fi diff --git a/main.go b/main.go deleted file mode 100644 index 6a04345..0000000 --- a/main.go +++ /dev/null @@ -1,80 +0,0 @@ -package main - -import ( - _ "embed" - "flag" - "fmt" - "os" - "strings" - - "github.com/Unbewohnte/ftu/receiver" - "github.com/Unbewohnte/ftu/sender" -) - -// flags -var ( - PORT *int = flag.Int("port", 7270, "Specifies a port to work with") - SENDERADDR *string = flag.String("addr", "", "Specifies an address to connect to") - DOWNLOADSFOLDER *string = flag.String("downloadto", ".", "Specifies where the receiver will store downloaded file") - SHAREDFILE *string = flag.String("sharefile", "", "Specifies what file sender will send") - PRINTLICENSE *bool = flag.Bool("license", false, "Prints a license text") - - SENDING bool - - //go:embed LICENSE - licenseText string -) - -// parse flags, validate given values -func init() { - flag.Parse() - - if *PRINTLICENSE { - fmt.Println(licenseText) - os.Exit(0) - } - - // port validation - if *PORT < 0 { - fmt.Println("Invalid port !") - os.Exit(-1) - } - - // sending or receiving - if strings.TrimSpace(*SHAREDFILE) != "" { - SENDING = true - } else if strings.TrimSpace(*SENDERADDR) != "" { - SENDING = false - } - - // check for default values in vital flags in case they were not provided - if strings.TrimSpace(*SENDERADDR) == "" && strings.TrimSpace(*SHAREDFILE) == "" { - flag.PrintDefaults() - os.Exit(-1) - } else if !SENDING && strings.TrimSpace(*SENDERADDR) == "" { - fmt.Println("No specified sender`s address") - os.Exit(-1) - } else if SENDING && strings.TrimSpace(*SHAREDFILE) == "" { - fmt.Println("No specified file") - os.Exit(-1) - } -} - -func main() { - if SENDING { - // 1) create sender -> 2) wait for a connection ->| - // 3) send info about the file -> 4) if accepted - upload file - sender := sender.NewSender(*PORT, *SHAREDFILE) - sender.WaitForConnection() - sender.HandleInterrupt() - sender.MainLoop() - - } else { - // 1) create receiver -> 2) try to connect to a sender -> 3) wait for an info on the file ->| - // 4) accept or refuse -> 5) download|don`t_download file - receiver := receiver.NewReceiver(*DOWNLOADSFOLDER) - receiver.Connect(fmt.Sprintf("%s:%d", *SENDERADDR, *PORT)) - receiver.HandleInterrupt() - receiver.MainLoop() - } -} diff --git a/node/node.go b/node/node.go deleted file mode 100644 index 2293141..0000000 --- a/node/node.go +++ /dev/null @@ -1,56 +0,0 @@ -package node - -import ( - "net" - - "github.com/Unbewohnte/ftu/protocol" -) - -type NodeInnerStates struct { - Connected bool - InTransfer bool - IsWaiting bool - Stopped bool -} - -type Security struct { - EncryptionKey []byte -} - -// Server and a client in one type ! -type Node struct { - conn net.Conn - packetPipe chan []protocol.Packet - State *NodeInnerStates - Security *Security -} - -// Creates a new either a server-side or client-side node -func NewNode(options *NodeOptions) (*Node, error) { - - node := Node{} - return &node, nil -} - -func (node *Node) Connect(addr string, port uint) error { - return nil -} - -func (node *Node) Disconnect() error { - if node.State.Connected { - err := node.conn.Close() - if err != nil { - return err - } - } - return nil -} - -func (node *Node) Send(packet protocol.Packet) error { - return nil -} - -func (node *Node) Listen() error { - return nil -} - diff --git a/node/noder.go b/node/noder.go deleted file mode 100644 index 781cb87..0000000 --- a/node/noder.go +++ /dev/null @@ -1,11 +0,0 @@ -package node - -import "github.com/Unbewohnte/ftu/protocol" - -// Implementation for sender and receiver nodes. [I`ll Probably remove it later. I don`t see the use-cases rn] -type Noder interface { - Connect(addr string, port uint) error - Disconnect() error - Listen(packetPipe chan protocol.Packet) - Send(packet protocol.Packet) error -} diff --git a/node/options.go b/node/options.go deleted file mode 100644 index d7cda55..0000000 --- a/node/options.go +++ /dev/null @@ -1,23 +0,0 @@ -package node - -import ( - "github.com/Unbewohnte/ftu/fs" - "github.com/Unbewohnte/ftu/protocol" -) - -type ServerSideNodeOptions struct { - ServingDirectory *fs.Directory // Can be set to nil - ServingFile *fs.File // Can be set to nil -} - -type ClientSideNodeOptions struct { - DownloadsFolder *fs.Directory // Must be set during the Node creation, even if it will be changed afterwards -} - -// Options to configure the node -type NodeOptions struct { - WorkingPort uint - PacketPipe chan protocol.Packet - ServerSide *ServerSideNodeOptions - ClientSide *ClientSideNodeOptions -} diff --git a/protocol/headers.go b/protocol/headers.go deleted file mode 100644 index 35954c1..0000000 --- a/protocol/headers.go +++ /dev/null @@ -1,83 +0,0 @@ -// This file describes various headers of the protocol and how to use them -package protocol - -type Header string - -// Headers - -//// In the following below examples "|" is PACKETSIZEDELIMETER and "~" is HEADERDELIMETER - -// ENCRKEY. -// The FIRST header to be sent. Sent immediately after the connection has been established -// by sender. Body contains randomly generated by sender aes encryption key. -// ie: |40|ENCRKEY~SUPER_SECURE_ENCRYPTION_KEY_YESS -const HeaderEncryptionKey Header = "ENCRKEY" - -// FILENAME. -// This header is sent only by sender. The packet with this header -// must contain a name of the transported file in BODY. -// ie: |18|FILENAME~image.png -const HeaderFilename Header = "FILENAME" - -// FILESIZE. -// This header is sent only by sender. The packet with this header -// must contain a size of the transported file in its BODY. -// ie: |15|FILESIZE~512442 -const HeaderFileSize Header = "FILESIZE" - -// CHECKSUM. -// Just like before, this header must be sent in a packet only by sender, -// BODY must contain a checksum of the transported file. -// ie: |74|CHECKSUM~1673f585148148d0c105af0d55646d6cbbf37e33a7366d3b72d8c5caca13434a -const HeaderChecksum Header = "CHECKSUM" - -// DOYOACCEPT. -// Sent by sender after all the information about the transfered file has been sent. -// Receiving a packet with this header means that there will be no more additional information about the -// file and the sender is waiting for response (acceptance or rejection of the file). -// ie: |13|DOYOUACCEPT?~ -const HeaderAcceptance Header = "DOYOUACCEPT?" - -// FILEBYTES. -// Sent only by sender. The packet`s body must contain -// a portion of transported file`s bytes. -// ie: |70|FILEBYTES~fj2pgfjek;hjg02yg082qyuhg83hvuahjvlhsaoughuihgp9earhguhergh\n -const HeaderFileBytes Header = "FILEBYTES" - -// FILEREJECT. -// Sent only by receiver if the user has decided to not download the file. -// The BODY may or may not be empty (preferably empty, of course), in any way, it will not be -// used in any way. -// ie: |11|FILEREJECT~ -const HeaderReject Header = "FILEREJECT" - -// FILEACCEPT. -// The opposite of the previous FILEREJECT. Send by receiver when -// the user has agreed to download the file. -// ie: |11|FILEACCEPT~ -const HeaderAccept Header = "FILEACCEPT" - -// DONE. -// Sent by sender. Warns the receiver that the file transfer has been done and -// there is no more information to give. -// ie: |5|DONE~ -// Usually after the packet with this header has been sent, the receiver will send -// another packet back with header BYE!, telling that it`s going to disconnect -const HeaderDone Header = "DONE" - -// READY. -// Sent by receiver when it hass read and processed the last -// FILEBYTES packet. The sender does not allowed to "spam" FILEBYTES -// packets without the permission of receiver. -// ie: |7|READY!~ -const HeaderReady Header = "READY" - -// BYE!. -// Packet with this header can be sent both by receiver and sender. -// It`s used when the sender or the receiver are going to disconnect -// and will not be able to communicate. -// (Usually it`s when the error has happened, OR, in a good situation, after the DONE header -// has been sent by sender, warning receiver that there is no data to send) -// The BODY is better to be empty. -// ie: |5|BYE!~ -const HeaderDisconnecting Header = "BYE!" diff --git a/protocol/packet.go b/protocol/packet.go deleted file mode 100644 index fd77912..0000000 --- a/protocol/packet.go +++ /dev/null @@ -1,173 +0,0 @@ -// This file describes the general packet structure and provides methods to work with them before|after the transportation - -// Examples of packets, ready for transportation in pseudo-code: -// []byte(|34|FILEDATA~fe2[gkr3j930f]fwpglkrt[o]) -// []byte(|57|FILENAME~theBestFileNameEver_Existed_in'''theUniverse.txt) -// general structure: -// PACKETSIZEDELIMETER packetsize PACKETSIZEDELIMETER packet.Header HEADERDELIMETER packet.Body (without spaces between) -package protocol - -import ( - "bytes" - "fmt" - "net" - "strconv" - - "github.com/Unbewohnte/ftu/encryption" -) - -// Internal representation of packet before|after the transportation -type Packet struct { - Header Header - Body []byte -} - -// Returns a size of the given packet as if it would be sent and presented in bytes. -// ie: FILESIZE~[49 49 56 55 56 53 50 49 54] -// DOES COUNT THE PACKETSIZEDELIMETER -func MeasurePacketSize(packet Packet) uint64 { - packetBytes := new(bytes.Buffer) - packetBytes.Write([]byte(packet.Header)) - packetBytes.Write([]byte(HEADERDELIMETER)) - packetBytes.Write(packet.Body) - - return uint64(packetBytes.Len()) -} - -// Converts packet bytes into Packet struct -func BytesToPacket(packetbytes []byte) Packet { - var header Header - var body []byte - - for counter, b := range packetbytes { - if string(b) == HEADERDELIMETER { - header = Header(packetbytes[0:counter]) - body = packetbytes[counter+1:] - break - } - } - return Packet{ - Header: header, - Body: body, - } -} - -// Converts given packet struct into ready-to-transfer bytes, constructed by following the protocol -func PacketToBytes(packet Packet) ([]byte, error) { - packetSize := MeasurePacketSize(packet) - - if packetSize > uint64(MAXPACKETSIZE) { - return nil, fmt.Errorf("invalid packet!: EXCEEDED MAX PACKETSIZE") - } - - packetSizeBytes := []byte(strconv.Itoa(int(packetSize))) - - // creating a buffer and writing the whole packet into it - packetBuffer := new(bytes.Buffer) - - // packetsize between delimeters (ie: |17|) - packetBuffer.Write([]byte(PACKETSIZEDELIMETER)) - packetBuffer.Write(packetSizeBytes) - packetBuffer.Write([]byte(PACKETSIZEDELIMETER)) - - // ie: FILENAME~file.txt - packetBuffer.Write([]byte(packet.Header)) - packetBuffer.Write([]byte(HEADERDELIMETER)) - packetBuffer.Write(packet.Body) - - // for debug purposes (ᗜˬᗜ) - // fmt.Printf("SENDING PACKET: %s%s%s%s%s%s\n", - // []byte(PACKETSIZEDELIMETER), packetSizeBytes, []byte(PACKETSIZEDELIMETER), - // []byte(packet.Header), []byte(HEADERDELIMETER), packet.Body) - - return packetBuffer.Bytes(), nil -} - -// Sends given packet to connection, following all the protocol`s rules. -// ALL packets MUST be sent by this method -func SendPacket(connection net.Conn, packetToSend Packet) error { - packetBytes, err := PacketToBytes(packetToSend) - if err != nil { - return fmt.Errorf("could not convert given packet to bytes: %s", err) - } - // write the result (ie: |17|FILENAME~file.png) - connection.Write(packetBytes) - - return nil -} - -// Sends given packet to connection, as the normal `SendPacket` method, but -// encodes given packet`s BODY with AES encryption -func SendEncryptedPacket(connection net.Conn, packetToSend Packet, key []byte) error { - // encrypting packet`s body - encryptedBody, err := encryption.Encrypt(key, packetToSend.Body) - if err != nil { - return fmt.Errorf("could not encrypt packet`s body: %s", err) - } - packetToSend.Body = encryptedBody - - // sending the encrypted packet - err = SendPacket(connection, packetToSend) - if err != nil { - return fmt.Errorf("could not send packet: %s", err) - } - - return nil -} - -// Reads a packet from given connection, returns its bytes. -// ASSUMING THAT THE PACKETS ARE SENT BY `SendPacket` function !!!! -func ReadFromConn(connection net.Conn) ([]byte, error) { - var err error - var delimeterCounter int = 0 - var packetSizeStrBuffer string = "" - var packetSize int = 0 - - for { - buffer := make([]byte, 1) - connection.Read(buffer) - - if string(buffer) == PACKETSIZEDELIMETER { - delimeterCounter++ - - // the first delimeter has been found, skipping the rest of the loop - if delimeterCounter == 1 { - continue - } - } - - // the last delimeter, the next read will be the packet itself, so breaking - if delimeterCounter == 2 { - break - } - - packetSizeStrBuffer += string(buffer) - } - - packetSize, err = strconv.Atoi(packetSizeStrBuffer) - if err != nil { - return nil, fmt.Errorf("could not convert packetsizeStr into int: %s", err) - } - - // have a packetsize, now reading the whole packet - packetBuffer := new(bytes.Buffer) - - // splitting a big-sized packet into chunks and constructing it from pieces - left := packetSize - for { - if left == 0 { - break - } - buff := make([]byte, 8192) - if left < len(buff) { - buff = make([]byte, left) - } - - read, _ := connection.Read(buff) - left -= read - - packetBuffer.Write(buff[:read]) - } - - return packetBuffer.Bytes(), nil -} diff --git a/receiver/file.go b/receiver/file.go deleted file mode 100644 index b1db6c2..0000000 --- a/receiver/file.go +++ /dev/null @@ -1,30 +0,0 @@ -package receiver - -import ( - "fmt" - "os" - - "github.com/Unbewohnte/ftu/checksum" -) - -// Receiver`s file struct. Used internally by receiver -type file struct { - Filename string - Filesize uint64 - CheckSum checksum.CheckSum -} - -// Goes through all files in the downloads directory and compares their -// names with the name of the file that is about to be downloaded -func (r *Receiver) checkIfFileAlreadyExists() (bool, error) { - contents, err := os.ReadDir(r.DownloadsFolder) - if err != nil { - return false, fmt.Errorf("could not get contents of the downloads` directory: %s", err) - } - for _, file := range contents { - if file.Name() == r.FileToDownload.Filename { - return true, nil - } - } - return false, nil -} diff --git a/receiver/receiver.go b/receiver/receiver.go deleted file mode 100644 index ef912e6..0000000 --- a/receiver/receiver.go +++ /dev/null @@ -1,356 +0,0 @@ -package receiver - -import ( - "fmt" - "net" - "os" - "os/signal" - "path/filepath" - "strconv" - "strings" - "time" - - "github.com/Unbewohnte/ftu/checksum" - "github.com/Unbewohnte/ftu/encryption" - "github.com/Unbewohnte/ftu/protocol" -) - -// Representation of a receiver -type Receiver struct { - DownloadsFolder string - Connection net.Conn - IncomingPackets chan protocol.Packet - FileToDownload *file - EncryptionKey []byte - TransferInfo *transferInfo - ReadyToReceive bool // waiting for a new packet - Stopped bool // controlls the mainloop -} - -// Creates a new client with default fields -func NewReceiver(downloadsFolder string) *Receiver { - os.MkdirAll(downloadsFolder, os.ModePerm) - - downloadsFolderInfo, err := os.Stat(downloadsFolder) - if err != nil { - panic(err) - } - if !downloadsFolderInfo.IsDir() { - panic("Downloads folder is not a directory") - } - - incomingPacketsChan := make(chan protocol.Packet, 100) - - fmt.Println("Created a new receiver") - return &Receiver{ - DownloadsFolder: downloadsFolder, - Connection: nil, - IncomingPackets: incomingPacketsChan, - Stopped: false, - ReadyToReceive: false, - FileToDownload: &file{ - Filename: "", - Filesize: 0, - }, - TransferInfo: &transferInfo{ - ReceivedFileBytesPackets: 0, - ApproximateNumOfPackets: 0, - }, - } -} - -// When the interrupt signal is sent - exit cleanly -func (r *Receiver) HandleInterrupt() { - signalChan := make(chan os.Signal, 1) - signal.Notify(signalChan, os.Interrupt) - - go func() { - <-signalChan - r.Stop() - }() -} - -// Closes the connection, warns the sender and exits the mainloop -func (r *Receiver) Stop() { - if r.Connection != nil { - disconnectionPacket := protocol.Packet{ - Header: protocol.HeaderDisconnecting, - } - protocol.SendEncryptedPacket(r.Connection, disconnectionPacket, r.EncryptionKey) - r.Connection.Close() - } - - r.Stopped = true - -} - -// Connects to a given address over tcp. Sets a connection to a corresponding field in receiver -func (r *Receiver) 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) - } - r.Connection = connection - fmt.Println("Connected to ", r.Connection.RemoteAddr()) - - return nil -} - -// Prints known information about the file that is about to be transported. -// Handles the input from the user after the sender sent "DOYOUACCEPT?" packet. -// The choice of acceptance is given to the user -func (r *Receiver) HandleFileOffer() error { - // inform the user about the file - - fmt.Printf(` - Incoming fileinfo packet: - | Filename: %s - | Filesize: %.3fMB - | Checksum: %x - | - | Download ? [Y/N]: `, - r.FileToDownload.Filename, float32(r.FileToDownload.Filesize)/1024/1024, r.FileToDownload.CheckSum, - ) - - // 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.SendEncryptedPacket(r.Connection, rejectionPacket, r.EncryptionKey) - if err != nil { - return fmt.Errorf("could not send a rejection packet: %s", err) - } - - return nil - } - // accept the file - - // check if the file with the same name is present - doesExist, err := r.checkIfFileAlreadyExists() - if err != nil { - return fmt.Errorf("could not check if the file with the same name alredy exists: %s", err) - } - - if doesExist { - fmt.Printf(` - | Looks like that there is a file with the same name in your downloads directory, do you want to overwrite it ? [Y/N]: `) - - fmt.Scanln(&input) - input = strings.TrimSpace(input) - input = strings.ToLower(input) - - if input == "y" { - err = os.Remove(filepath.Join(r.DownloadsFolder, r.FileToDownload.Filename)) - if err != nil { - return fmt.Errorf("could not remove the file: %s", err) - } - } else { - // user did not agree to overwrite, adding checksum to the name - r.FileToDownload.Filename = fmt.Sprint(time.Now().Unix()) + r.FileToDownload.Filename - } - } - - acceptancePacket := protocol.Packet{ - Header: protocol.HeaderAccept, - } - err = protocol.SendEncryptedPacket(r.Connection, acceptancePacket, r.EncryptionKey) - if err != nil { - return fmt.Errorf("could not send an acceptance packet: %s", err) - } - - r.TransferInfo.ApproximateNumOfPackets = uint64(float32(r.FileToDownload.Filesize) / float32(protocol.MAXPACKETSIZE)) - - return nil -} - -// Handles the download by writing incoming bytes into the file -func (r *Receiver) WritePieceOfFile(filePacket protocol.Packet) error { - if filePacket.Header != protocol.HeaderFileBytes { - return fmt.Errorf("packet with given header should not contain filebytes !: %v", filePacket) - } - - // open|create a file with the same name as the filepacket`s file name - file, err := os.OpenFile(filepath.Join(r.DownloadsFolder, r.FileToDownload.Filename), os.O_CREATE|os.O_APPEND|os.O_WRONLY, os.ModePerm) - if err != nil { - return err - } - // just write the bytes - file.Write(filePacket.Body) - file.Close() - r.TransferInfo.ReceivedFileBytesPackets++ - - return nil -} - -// Prints a brief information about the state of the transfer -func (r *Receiver) PrintTransferInfo(pauseDuration time.Duration) { - next := time.Now().UTC() - r.TransferInfo.StartTime = next - for { - if r.TransferInfo.ReceivedFileBytesPackets == 0 { - time.Sleep(time.Second) - continue - } - - now := time.Now().UTC() - - if !now.After(next) { - continue - } - next = now.Add(pauseDuration) - - fmt.Printf(` - | Received packets/Approximate number of packets - | (%d|%d) (%.2f%%/100%%) -`, r.TransferInfo.ReceivedFileBytesPackets, - r.TransferInfo.ApproximateNumOfPackets, - float32(r.TransferInfo.ReceivedFileBytesPackets)/float32(r.TransferInfo.ApproximateNumOfPackets)*100) - - time.Sleep(pauseDuration) - } -} - -// Listens in an endless loop; reads incoming packets, decrypts their BODY and puts into channel -func (r *Receiver) ReceivePackets() { - for { - incomingPacketBytes, err := protocol.ReadFromConn(r.Connection) - if err != nil { - fmt.Printf("Error reading a packet: %s\nExiting...", err) - r.Stop() - os.Exit(-1) - } - - incomingPacket := protocol.BytesToPacket(incomingPacketBytes) - - // if this is the FIRST packet - it has HeaderEncryptionKey, so no need to decrypt - if incomingPacket.Header == protocol.HeaderEncryptionKey { - r.IncomingPackets <- incomingPacket - continue - } - - decryptedBody, err := encryption.Decrypt(r.EncryptionKey, incomingPacket.Body) - if err != nil { - fmt.Printf("Error decrypring incoming packet`s BODY: %s\nExiting...", err) - r.Stop() - os.Exit(-1) - } - - incomingPacket.Body = decryptedBody - - r.IncomingPackets <- incomingPacket - } -} - -// The "head" of the receiver. Similarly as in server`s logic "glues" everything together. -// Current structure allows the receiver to receive any type of packet -// in any order and react correspondingly -func (r *Receiver) MainLoop() { - go r.ReceivePackets() - go r.PrintTransferInfo(time.Second * 3) - - for { - if r.Stopped { - break - } - - if r.ReadyToReceive { - readyPacket := protocol.Packet{ - Header: protocol.HeaderReady, - } - err := protocol.SendEncryptedPacket(r.Connection, readyPacket, r.EncryptionKey) - if err != nil { - fmt.Printf("Could not send the packet: %s\nExiting...", err) - r.Stop() - } - - r.ReadyToReceive = false - } - - // no incoming packets ? Skipping the packet handling part - if len(r.IncomingPackets) == 0 { - continue - } - - incomingPacket := <-r.IncomingPackets - - // handling each packet header differently - switch incomingPacket.Header { - - case protocol.HeaderEncryptionKey: - r.EncryptionKey = incomingPacket.Body - fmt.Println("Got the encryption key: ", string(incomingPacket.Body)) - - case protocol.HeaderFilename: - r.FileToDownload.Filename = string(incomingPacket.Body) - - case protocol.HeaderFileSize: - filesize, err := strconv.Atoi(string(incomingPacket.Body)) - if err != nil { - fmt.Printf("could not convert a filesize: %s\n", err) - r.Stop() - } - r.FileToDownload.Filesize = uint64(filesize) - - case protocol.HeaderChecksum: - checksum, err := checksum.BytesToChecksum(incomingPacket.Body) - if err != nil { - fmt.Printf("could not get file`s checksum: %s\n", err) - r.Stop() - } - r.FileToDownload.CheckSum = checksum - - case protocol.HeaderDone: - if r.FileToDownload.Filename != "" && r.FileToDownload.Filesize != 0 && r.FileToDownload.CheckSum != [32]byte{} { - err := r.HandleFileOffer() - if err != nil { - fmt.Printf("Could not handle a file download confirmation: %s\nExiting...", err) - r.Stop() - } - r.ReadyToReceive = true - } else { - fmt.Println("Not enough data about the file was sent. Exiting...") - r.Stop() - } - - case protocol.HeaderFileBytes: - err := r.WritePieceOfFile(incomingPacket) - if err != nil { - fmt.Printf("Could not write a piece of file: %s\nExiting...", err) - r.Stop() - } - r.ReadyToReceive = true - - case protocol.HeaderDisconnecting: - // the sender has completed its mission, - // checking hashes and exiting - - fmt.Printf("Got %d file packets in total. Took %v\n", r.TransferInfo.ReceivedFileBytesPackets, time.Since(r.TransferInfo.StartTime)) - fmt.Println("Comparing checksums...") - - file, err := os.Open(filepath.Join(r.DownloadsFolder, r.FileToDownload.Filename)) - if err != nil { - fmt.Printf("error while opening downloaded file for checking: %s\n", err) - os.Exit(-1) - } - realCheckSum, err := checksum.GetPartialCheckSum(file) - if err != nil { - fmt.Printf("error perfoming partial checksum: %s\n", err) - os.Exit(-1) - } - - fmt.Printf("\n%x ----- %x\n", r.FileToDownload.CheckSum, realCheckSum) - if !checksum.AreEqual(realCheckSum, r.FileToDownload.CheckSum) { - fmt.Println("Downloaded file is corrupted !") - } - r.Stop() - } - } -} diff --git a/receiver/transferinfo.go b/receiver/transferinfo.go deleted file mode 100644 index 4eae577..0000000 --- a/receiver/transferinfo.go +++ /dev/null @@ -1,9 +0,0 @@ -package receiver - -import "time" - -type transferInfo struct { - ReceivedFileBytesPackets uint64 - ApproximateNumOfPackets uint64 - StartTime time.Time -} diff --git a/sender/file.go b/sender/file.go deleted file mode 100644 index f722b20..0000000 --- a/sender/file.go +++ /dev/null @@ -1,45 +0,0 @@ -package sender - -import ( - "fmt" - "os" - - "github.com/Unbewohnte/ftu/checksum" -) - -// Struct that represents the served file. Used internally in the sender -type file struct { - path string - Filename string - Filesize uint64 - SentBytes uint64 - LeftBytes uint64 - Handler *os.File - CheckSum checksum.CheckSum -} - -// Prepares a file for serving. Used for preparing info before sending a fileinfo packet by sender -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("wasn`t able to open the file: %s", err) - } - checksum, err := checksum.GetPartialCheckSum(handler) - if err != nil { - return nil, fmt.Errorf("could not get a partial file checksum: %s", err) - } - - return &file{ - path: path, - Filename: info.Name(), - Filesize: uint64(info.Size()), - SentBytes: 0, - LeftBytes: uint64(info.Size()), - Handler: handler, - CheckSum: checksum, - }, nil -} diff --git a/sender/ip.go b/sender/ip.go deleted file mode 100644 index 6060f3a..0000000 --- a/sender/ip.go +++ /dev/null @@ -1,35 +0,0 @@ -package sender - -import ( - "fmt" - "io" - "net" - "net/http" -) - -// 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 localAddr.IP.String(), 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 -} diff --git a/sender/sender.go b/sender/sender.go deleted file mode 100644 index b6325b5..0000000 --- a/sender/sender.go +++ /dev/null @@ -1,341 +0,0 @@ -package sender - -import ( - "fmt" - "net" - "os" - "os/signal" - "strconv" - "time" - - "github.com/Unbewohnte/ftu/checksum" - "github.com/Unbewohnte/ftu/encryption" - "github.com/Unbewohnte/ftu/protocol" -) - -// The main sender struct -type Sender struct { - Port int - FileToTransfer *file - Listener net.Listener - Connection net.Conn - IncomingPackets chan protocol.Packet - EncryptionKey []byte - TransferInfo *transferInfo - TransferAllowed bool // the receiver had agreed to receive a file - ReceiverIsReady bool // receiver is waiting for a new packet - Stopped bool // controlls the mainloop -} - -// Creates a new sender with default|necessary fields -func NewSender(port int, filepath string) *Sender { - 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, 100) - - remoteIP, err := GetRemoteIP() - if err != nil { - // don`t panic if couldn`t get remote IP - remoteIP = "" - } - - localIP, err := GetLocalIP() - if err != nil { - panic(err) - } - - // !!! - key := encryption.Generate32AESkey() - fmt.Printf("Generated an encryption key: %s\n", key) - - fmt.Printf("Created a new sender at %s:%d (remote)\n%s:%d (local)\n\n", remoteIP, port, localIP, port) - return &Sender{ - Port: port, - FileToTransfer: fileToTransfer, - Listener: listener, - Connection: nil, - IncomingPackets: incomingPacketsChan, - TransferInfo: &transferInfo{ - SentFileBytesPackets: 0, - ApproximateNumOfPackets: uint64(float32(fileToTransfer.Filesize) / float32(protocol.MAXPACKETSIZE)), - }, - EncryptionKey: key, - TransferAllowed: false, - ReceiverIsReady: false, - Stopped: false, - } -} - -// When the interrupt signal is sent - exit cleanly -func (s *Sender) HandleInterrupt() { - signalChan := make(chan os.Signal, 1) - signal.Notify(signalChan, os.Interrupt) - - go func() { - <-signalChan - s.Stop() - }() -} - -// Closes the connection, warns about it the receiver and exits the mainloop -func (s *Sender) Stop() { - if s.Connection != nil { - disconnectionPacket := protocol.Packet{ - Header: protocol.HeaderDisconnecting, - } - err := protocol.SendEncryptedPacket(s.Connection, disconnectionPacket, s.EncryptionKey) - if err != nil { - panic(fmt.Sprintf("could not send a disconnection packet: %s", err)) - } - - s.Connection.Close() - } - - s.Stopped = true -} - -// Accepts one connection -func (s *Sender) WaitForConnection() { - connection, err := s.Listener.Accept() - if err != nil { - fmt.Printf("Could not accept a connection: %s", err) - os.Exit(-1) - } - s.Connection = connection - fmt.Println("New connection from ", s.Connection.RemoteAddr()) -} - -// Closes the listener. Used only when there is still no connection from `AcceptConnections` -func (s *Sender) StopListening() { - s.Listener.Close() -} - -// Sends generated earlier eas encryption key to receiver -func (s *Sender) SendEncryptionKey() error { - - keyPacket := protocol.Packet{ - Header: protocol.HeaderEncryptionKey, - Body: s.EncryptionKey, - } - err := protocol.SendPacket(s.Connection, keyPacket) - if err != nil { - return fmt.Errorf("could not send a packet: %s", err) - } - - return nil -} - -// Sends multiple packets with all information about the file to receiver -// (filename, filesize, checksum) -func (s *Sender) SendOffer() error { - // filename - filenamePacket := protocol.Packet{ - Header: protocol.HeaderFilename, - Body: []byte(s.FileToTransfer.Filename), - } - err := protocol.SendEncryptedPacket(s.Connection, filenamePacket, s.EncryptionKey) - if err != nil { - return fmt.Errorf("could not send an information about the file: %s", err) - } - - // filesize - filesizePacket := protocol.Packet{ - Header: protocol.HeaderFileSize, - Body: []byte(strconv.Itoa(int(s.FileToTransfer.Filesize))), - } - - err = protocol.SendEncryptedPacket(s.Connection, filesizePacket, s.EncryptionKey) - if err != nil { - return fmt.Errorf("could not send an information about the file: %s", err) - } - - // checksum - checksumPacket := protocol.Packet{ - Header: protocol.HeaderChecksum, - Body: checksum.ChecksumToBytes(s.FileToTransfer.CheckSum), - } - err = protocol.SendEncryptedPacket(s.Connection, checksumPacket, s.EncryptionKey) - if err != nil { - return fmt.Errorf("could not send an information about the file: %s", err) - } - - // indicate that we`ve sent everything we needed to send - donePacket := protocol.Packet{ - Header: protocol.HeaderDone, - } - err = protocol.SendEncryptedPacket(s.Connection, donePacket, s.EncryptionKey) - if err != nil { - return fmt.Errorf("could not send an information about the file: %s", err) - } - return nil -} - -// Sends one packet that contains a piece of file to the receiver -func (s *Sender) SendPiece() error { - // if no data to send - exit - if s.FileToTransfer.LeftBytes == 0 { - fmt.Printf("Done. Sent %d file packets\nTook %v\n", s.TransferInfo.SentFileBytesPackets, time.Since(s.TransferInfo.StartTime)) - s.Stop() - } - - // empty body - fileBytesPacket := protocol.Packet{ - Header: protocol.HeaderFileBytes, - } - - // how many bytes we can send at maximum (including some little space for padding) - maxFileBytes := protocol.MAXPACKETSIZE - (uint(protocol.MeasurePacketSize(fileBytesPacket)) + 90) - - fileBytes := make([]byte, maxFileBytes) - // if there is less data to send than the limit - create a buffer of needed size - if s.FileToTransfer.LeftBytes < uint64(maxFileBytes) { - fileBytes = make([]byte, uint64(maxFileBytes)-(uint64(maxFileBytes)-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) - } - - // filling BODY with bytes - fileBytesPacket.Body = fileBytes - - err = protocol.SendEncryptedPacket(s.Connection, fileBytesPacket, s.EncryptionKey) - if err != nil { - return fmt.Errorf("could not send a file packet : %s", err) - } - - // doing a "logging" for the next piece - s.FileToTransfer.LeftBytes -= uint64(read) - s.FileToTransfer.SentBytes += uint64(read) - s.TransferInfo.SentFileBytesPackets++ - - return nil -} - -// Prints a brief information about the state of the transfer -func (s *Sender) PrintTransferInfo(pauseDuration time.Duration) { - next := time.Now().UTC() - s.TransferInfo.StartTime = next - for { - if !s.TransferAllowed { - time.Sleep(time.Second) - continue - } - - now := time.Now().UTC() - - if !now.After(next) { - continue - } - next = now.Add(pauseDuration) - - fmt.Printf(` - | Sent packets/Approximate number of packets - | (%d|%d) (%.2f%%/100%%) -`, s.TransferInfo.SentFileBytesPackets, - s.TransferInfo.ApproximateNumOfPackets, - float32(s.TransferInfo.SentFileBytesPackets)/float32(s.TransferInfo.ApproximateNumOfPackets)*100) - - time.Sleep(pauseDuration) - } -} - -// Listens in an endless loop; reads incoming packets, decrypts their BODY and puts into channel -func (s *Sender) ReceivePackets() { - for { - incomingPacketBytes, err := protocol.ReadFromConn(s.Connection) - if err != nil { - fmt.Printf("Error reading a packet: %s\nExiting...", err) - s.Stop() - os.Exit(-1) - } - - incomingPacket := protocol.BytesToPacket(incomingPacketBytes) - - decryptedBody, err := encryption.Decrypt(s.EncryptionKey, incomingPacket.Body) - if err != nil { - fmt.Printf("Error decrypting an incoming packet: %s\nExiting...", err) - s.Stop() - os.Exit(-1) - } - - incomingPacket.Body = decryptedBody - - s.IncomingPackets <- incomingPacket - } -} - -// The "head" of the sender. "Glues" all things together. -// Current structure allows the sender to receive any type of packet -// in any order and react correspondingly -func (s *Sender) MainLoop() { - // receive and print in separate goroutines - go s.ReceivePackets() - go s.PrintTransferInfo(time.Second * 3) - - // instantly sending an encryption key, following the protocol`s rule - err := s.SendEncryptionKey() - if err != nil { - fmt.Printf("Could not send an encryption key: %s\nExiting...", err) - s.Stop() - } - - // send an information about the shared file to the receiver - err = s.SendOffer() - if err != nil { - fmt.Printf("Could not send an info about the file: %s\nExiting...", err) - s.Stop() - } - - for { - if s.Stopped { - break - } - - if s.TransferAllowed && s.ReceiverIsReady { - err := s.SendPiece() - if err != nil { - fmt.Printf("could not send a piece of file: %s", err) - s.Stop() - } - s.ReceiverIsReady = false - } - - // 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: - // allowed to send file packets - fmt.Println("The transfer has been accepted !") - s.TransferAllowed = true - - case protocol.HeaderReject: - fmt.Println("The transfer has been rejected") - s.Stop() - - case protocol.HeaderReady: - s.ReceiverIsReady = true - - case protocol.HeaderDisconnecting: - // receiver is dropping the file transfer ? - fmt.Println("Receiver has disconnected") - s.Stop() - } - } -} diff --git a/sender/transferinfo.go b/sender/transferinfo.go deleted file mode 100644 index 0c9db06..0000000 --- a/sender/transferinfo.go +++ /dev/null @@ -1,9 +0,0 @@ -package sender - -import "time" - -type transferInfo struct { - SentFileBytesPackets uint64 - ApproximateNumOfPackets uint64 - StartTime time.Time -} diff --git a/src/LICENSE b/src/LICENSE new file mode 100644 index 0000000..c82adc3 --- /dev/null +++ b/src/LICENSE @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright © 2021 Unbewohne | Kasyanov Nikolay Alexeevich + +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/src/addr/outbound.go b/src/addr/outbound.go new file mode 100644 index 0000000..a69ee25 --- /dev/null +++ b/src/addr/outbound.go @@ -0,0 +1,18 @@ +package addr + +import ( + "net" +) + +// Get local IP address; from https://stackoverflow.com/a/37382208 +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 localAddr.IP.String(), nil +} diff --git a/src/checksum/checksum.go b/src/checksum/checksum.go new file mode 100644 index 0000000..72eeea4 --- /dev/null +++ b/src/checksum/checksum.go @@ -0,0 +1,79 @@ +package checksum + +import ( + "crypto/sha256" + "encoding/hex" + "io" + "os" +) + +// returns a checksum of given file. NOTE, that it creates checksum +// not of a full file (from all file bytes), but from separate byte blocks. +// This is done as an optimisation because the file can be very large in size. +// The general idea: +// BOF... CHUNK -> STEP -> CHUNK... EOF +// checksum := sha256.Sum256(ALLCHUNKS) +// GetPartialCheckSum is default method used to get a file checksum by sender and receiver +func GetPartialCheckSum(file *os.File) (string, error) { + // "capturing" CHUNKSIZE bytes and then skipping STEP bytes before the next chunk until the last one + const CHUNKS uint = 100 + const CHUNKSIZE uint = 100 + const STEP uint = 250 + + fileStats, err := file.Stat() + if err != nil { + return "", err + } + + fileSize := fileStats.Size() + + if fileSize < int64(CHUNKS*CHUNKSIZE+STEP*(CHUNKS-1)) { + // file is too small to chop it in chunks, so just doing full checksum + + checksum, err := getFullCheckSum(file) + if err != nil { + return "", err + } + return checksum, nil + } + + _, err = file.Seek(0, io.SeekStart) + if err != nil { + return "", err + } + + var capturedChunks string + var read uint64 = 0 + for i := 0; uint(i) < CHUNKS; i++ { + buffer := make([]byte, CHUNKSIZE) + r, _ := file.ReadAt(buffer, int64(read)) + + capturedChunks += string(buffer) + + read += uint64(r) + read += uint64(STEP) + } + + checksumBytes := sha256.Sum256([]byte(capturedChunks)) + checksum := hex.EncodeToString(checksumBytes[:]) + + return checksum, nil +} + +// Returns a sha256 checksum of given file +func getFullCheckSum(file *os.File) (string, error) { + _, err := file.Seek(0, io.SeekStart) + if err != nil { + return "", err + } + + filebytes, err := io.ReadAll(file) + if err != nil { + return "", err + } + + checksumBytes := sha256.Sum256(filebytes) + checksum := hex.EncodeToString(checksumBytes[:]) + + return checksum, nil +} diff --git a/src/checksum/checksum_test.go b/src/checksum/checksum_test.go new file mode 100644 index 0000000..738f548 --- /dev/null +++ b/src/checksum/checksum_test.go @@ -0,0 +1,25 @@ +package checksum + +import ( + "os" + "strings" + "testing" +) + +func Test_GetPartialCheckSum(t *testing.T) { + tesfilePath := "../testfiles/testfile.txt" + + file, err := os.Open(tesfilePath) + if err != nil { + t.Fatalf("%s", err) + } + + checksum, err := GetPartialCheckSum(file) + if err != nil { + t.Fatalf("GetPartialCheckSum error: %s", err) + } + + if !strings.EqualFold("fa6d92493ac0c73c9fa85d10c92b41569017454c5b4387d315f3d2c4ad1d6766", checksum) { + t.Fatalf("GetPartialCheckSum error: hashes of a testfile.txt do not match") + } +} diff --git a/encryption/decrypt.go b/src/encryption/decrypt.go similarity index 100% rename from encryption/decrypt.go rename to src/encryption/decrypt.go diff --git a/encryption/encrypt.go b/src/encryption/encrypt.go similarity index 100% rename from encryption/encrypt.go rename to src/encryption/encrypt.go diff --git a/encryption/encryption_test.go b/src/encryption/encryption_test.go similarity index 100% rename from encryption/encryption_test.go rename to src/encryption/encryption_test.go diff --git a/encryption/key.go b/src/encryption/key.go similarity index 100% rename from encryption/key.go rename to src/encryption/key.go diff --git a/fs/dir.go b/src/fsys/dir.go similarity index 89% rename from fs/dir.go rename to src/fsys/dir.go index 8a81b1e..3b7ff86 100644 --- a/fs/dir.go +++ b/src/fsys/dir.go @@ -1,4 +1,4 @@ -package fs +package fsys import ( "fmt" @@ -18,15 +18,11 @@ type Directory struct { var ErrorNotDirectory error = fmt.Errorf("not a directory") func GetDir(path string, recursive bool) (*Directory, error) { - fmt.Println("Provided path ", path) - absPath, err := filepath.Abs(path) if err != nil { return nil, err } - fmt.Println("absolute path ", absPath) - stats, err := os.Stat(absPath) if err != nil { return nil, err @@ -56,8 +52,6 @@ func GetDir(path string, recursive bool) (*Directory, error) { // do the recursive magic innerDirPath := filepath.Join(absPath, entry.Name()) - fmt.Println("inner dir path ", innerDirPath) - innerDir, err := GetDir(innerDirPath, true) if err != nil { return nil, err @@ -70,8 +64,6 @@ func GetDir(path string, recursive bool) (*Directory, error) { } else { innerFilePath := filepath.Join(absPath, entryInfo.Name()) - fmt.Println("inner file path ", innerFilePath) - innerFile, err := GetFile(innerFilePath) if err != nil { return nil, err diff --git a/fs/dir_test.go b/src/fsys/dir_test.go similarity index 66% rename from fs/dir_test.go rename to src/fsys/dir_test.go index d6bc0b5..61a62c0 100644 --- a/fs/dir_test.go +++ b/src/fsys/dir_test.go @@ -1,4 +1,4 @@ -package fs +package fsys import "testing" @@ -19,13 +19,8 @@ func Test_GetDirRecursive(t *testing.T) { t.Fatalf("GetDir error: %s", err) } - expectedAmountOfUpperDirectories := 2 + expectedAmountOfUpperDirectories := 3 if len(dir.Directories) != expectedAmountOfUpperDirectories { t.Fatalf("GetDir error: expected to have %d inner directories; got %d", expectedAmountOfUpperDirectories, len(dir.Directories)) } - - innerDir1 := dir.Directories[0] - if innerDir1 == nil || innerDir1.Name != "testdir" { - t.Fatalf("GetDir error: expected to have the first inner directory to be \"%s\"; got \"%s\"", "testdir", innerDir1.Name) - } } diff --git a/fs/file.go b/src/fsys/file.go similarity index 66% rename from fs/file.go rename to src/fsys/file.go index e58b2e0..5189a8f 100644 --- a/fs/file.go +++ b/src/fsys/file.go @@ -1,4 +1,4 @@ -package fs +package fsys import ( "fmt" @@ -6,13 +6,18 @@ import ( "path/filepath" ) -// A struct that represents the main file information +var FileIDsCounter uint64 = 1 + +// A struct that represents the necessary file information for transportation through node type File struct { + ID uint64 Name string Path string ParentPath string Size uint64 - Handler *os.File + Checksum string // Set manually + Handler *os.File // Set when .Open() is called + SentBytes uint64 // Set manually during transportation } var ErrorNotFile error = fmt.Errorf("not a file") @@ -38,6 +43,7 @@ func GetFile(path string) (*File, error) { } file := File{ + ID: FileIDsCounter, Name: stats.Name(), Path: absPath, ParentPath: filepath.Dir(absPath), @@ -45,12 +51,15 @@ func GetFile(path string) (*File, error) { Handler: nil, } + // increment ids counter so the next file will have a different ID + FileIDsCounter++ + return &file, nil } // Opens file for read/write operations func (file *File) Open() error { - handler, err := os.OpenFile(file.Path, os.O_RDWR, os.ModePerm) + handler, err := os.OpenFile(file.Path, os.O_CREATE|os.O_RDWR, os.ModePerm) if err != nil { return err } diff --git a/fs/file_test.go b/src/fsys/file_test.go similarity index 98% rename from fs/file_test.go rename to src/fsys/file_test.go index 40ffc04..e51eeee 100644 --- a/fs/file_test.go +++ b/src/fsys/file_test.go @@ -1,4 +1,4 @@ -package fs +package fsys import ( "io" diff --git a/go.mod b/src/go.mod similarity index 100% rename from go.mod rename to src/go.mod diff --git a/src/main.go b/src/main.go new file mode 100644 index 0000000..871794e --- /dev/null +++ b/src/main.go @@ -0,0 +1,106 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "os" + + "github.com/Unbewohnte/ftu/node" +) + +// flags +var ( + PORT *uint = flag.Uint("p", 7270, "Specifies a port to work with") + PRINT_LICENSE *bool = flag.Bool("l", false, "Prints a license text") + RECUSRIVE *bool = flag.Bool("r", false, "Recursively send a directory") + ADDRESS *string = flag.String("a", "", "Specifies an address to connect to") + DOWNLOADS_DIR *string = flag.String("d", ".", "Downloads folder") + SEND *string = flag.String("s", "", "Specify a file|directory to send") + + //go:embed LICENSE + licenseText string + + isSending bool +) + +func init() { + flag.Usage = func() { + fmt.Printf("ftu -[FLAG]...\n\n") + + fmt.Printf("[FLAGs]\n\n") + fmt.Printf("| -p [Uinteger_here] for port\n") + fmt.Printf("| -r [true|false] for recursive sending of a directory\n") + fmt.Printf("| -a [ip_address|domain_name] address to connect to (cannot be used with -s)\n") + fmt.Printf("| -d [path_to_directory] where the files will be downloaded to (cannot be used with -s)\n") + fmt.Printf("| -s [path_to_file|directory] to send it (cannot be used with -a)\n") + fmt.Printf("| -l for license text\n\n\n") + + fmt.Printf("[Examples]\n\n") + + fmt.Printf("| ftu -p 89898 -s /home/user/Downloads/someVideo.mp4\n") + fmt.Printf("| creates a node on a non-default port 89898 that will send \"someVideo.mp4\" to the other node that connects to you\n\n") + + fmt.Printf("| ftu -p 7277 -a 192.168.1.104 -d .\n") + fmt.Printf("| creates a node that will connect to 192.168.1.104:7277 and download served file|directory to the working directory\n\n") + + fmt.Printf("| ftu -p 7277 -a 192.168.1.104 -d /home/user/Downloads/\n") + fmt.Printf("| creates a node that will connect to 192.168.1.104:7277 and download served file|directory to \"/home/user/Downloads/\"\n\n") + + fmt.Printf("| ftu -s /home/user/homework\n") + fmt.Printf("| creates a node that will send every file in the directory\n\n") + + fmt.Printf("| ftu -r -s /home/user/homework/\n") + fmt.Printf("| creates a node that will send every file in the directory !RECUSRIVELY!\n\n\n") + + } + flag.Parse() + + if *PRINT_LICENSE { + fmt.Println(licenseText) + os.Exit(0) + } + + // validate flags + if *SEND == "" && *ADDRESS == "" { + fmt.Printf("Neither sending nor receiving flag was specified. Run ftu -h for help") + os.Exit(-1) + } + + if *SEND != "" && *ADDRESS != "" { + fmt.Printf("Can`t send and receive at the same time. Specify only -s or -a\n") + os.Exit(-1) + } + + // sending or receiving + if *SEND != "" { + // sending + isSending = true + } else if *ADDRESS != "" { + // receiving + isSending = false + } +} + +func main() { + nodeOptions := node.NodeOptions{ + IsSending: isSending, + WorkingPort: *PORT, + ServerSide: &node.ServerSideNodeOptions{ + ServingPath: *SEND, + Recursive: *RECUSRIVE, + }, + ClientSide: &node.ClientSideNodeOptions{ + ConnectionAddr: *ADDRESS, + DownloadsFolderPath: *DOWNLOADS_DIR, + }, + } + + node, err := node.NewNode(&nodeOptions) + if err != nil { + fmt.Printf("Error constructing a new node: %s\n", err) + os.Exit(-1) + } + + node.Start() +} diff --git a/src/node/errors.go b/src/node/errors.go new file mode 100644 index 0000000..a39fc3a --- /dev/null +++ b/src/node/errors.go @@ -0,0 +1,8 @@ +package node + +import "fmt" + +var ( + ErrorNotConnected error = fmt.Errorf("not connected") + ErrorSentAll error = fmt.Errorf("sent the whole file") +) diff --git a/src/node/node.go b/src/node/node.go new file mode 100644 index 0000000..524c8c6 --- /dev/null +++ b/src/node/node.go @@ -0,0 +1,429 @@ +package node + +import ( + "bytes" + "encoding/binary" + "net" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "fmt" + + "github.com/Unbewohnte/ftu/addr" + "github.com/Unbewohnte/ftu/checksum" + "github.com/Unbewohnte/ftu/fsys" + "github.com/Unbewohnte/ftu/protocol" +) + +type NodeInnerStates struct { + Stopped bool + Connected bool + AllowedToTransfer bool +} + +type Net struct { + ConnAddr string + Conn net.Conn + Port uint + EncryptionKey []byte +} + +type TransferInfo struct { + Ready bool // is the other node ready to receive another piece + ServingPath string // path to the thing that will be sent + Recursive bool + AcceptedFiles []*fsys.File + DownloadsPath string +} + +// Sender and receiver in one type ! +type Node struct { + PacketPipe chan *protocol.Packet + Mutex *sync.Mutex + IsSending bool + Net *Net + State *NodeInnerStates + TransferInfo *TransferInfo +} + +// Creates a new node +func NewNode(options *NodeOptions) (*Node, error) { + mutex := new(sync.Mutex) + + node := Node{ + PacketPipe: make(chan *protocol.Packet, 100), + Mutex: mutex, + IsSending: options.IsSending, + Net: &Net{ + Port: options.WorkingPort, + ConnAddr: options.ClientSide.ConnectionAddr, + EncryptionKey: nil, + Conn: nil, + }, + State: &NodeInnerStates{ + AllowedToTransfer: false, + Stopped: false, + Connected: false, + }, + TransferInfo: &TransferInfo{ + ServingPath: options.ServerSide.ServingPath, + Recursive: options.ServerSide.Recursive, + AcceptedFiles: nil, + DownloadsPath: options.ClientSide.DownloadsFolderPath, + }, + } + return &node, nil +} + +func (node *Node) connect(addr string, port uint) error { + if port == 0 { + port = node.Net.Port + } + + fmt.Printf("Connecting to %s:%d...\n", addr, port) + + conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", addr, port), time.Second*5) + if err != nil { + return err + } + + fmt.Printf("Connected\n") + + node.Net.Conn = conn + node.State.Connected = true + + return nil +} + +func (node *Node) disconnect() error { + if node.State.Connected && node.Net.Conn != nil { + // notify the other node and close the connection + err := protocol.SendPacket(node.Net.Conn, protocol.Packet{ + Header: protocol.HeaderDisconnecting, + }) + if err != nil { + return err + } + + err = node.Net.Conn.Close() + if err != nil { + return err + } + + node.State.Stopped = true + node.State.Connected = false + } + + return nil +} + +// Waits for connection on a pre-defined port +func (node *Node) waitForConnection() error { + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", node.Net.Port)) + if err != nil { + return err + } + + // accept only one conneciton + connection, err := listener.Accept() + if err != nil { + return err + } + + fmt.Printf("New connection from %s\n", connection.RemoteAddr().String()) + + node.Net.Conn = connection + node.State.Connected = true + + return nil +} + +// Starts the node in either sending or receiving state and performs the transfer +func (node *Node) Start() { + switch node.IsSending { + case true: + // SENDER + + localIP, err := addr.GetLocalIP() + if err != nil { + panic(err) + } + + file, err := fsys.GetFile(node.TransferInfo.ServingPath) + if err != nil { + panic(err) + } + fmt.Printf("Sending \"%s\" (%.2f MB) locally on %s:%d\n", file.Name, float32(file.Size)/1024/1024, localIP, node.Net.Port) + + // wain for another node to connect + err = node.waitForConnection() + if err != nil { + panic(err) + } + + // listen for incoming packets + go receivePackets(node.Net.Conn, node.PacketPipe) + + // send fileoffer + go sendFilePacket(node.Net.Conn, file) + + // mainloop + for { + node.Mutex.Lock() + stopped := node.State.Stopped + node.Mutex.Unlock() + + if stopped { + node.Mutex.Lock() + node.disconnect() + node.Mutex.Unlock() + break + } + + incomingPacket := <-node.PacketPipe + + switch incomingPacket.Header { + case protocol.HeaderReady: + node.Mutex.Lock() + node.TransferInfo.Ready = true + node.Mutex.Unlock() + + case protocol.HeaderAccept: + node.Mutex.Lock() + node.State.AllowedToTransfer = true + node.Mutex.Unlock() + go fmt.Printf("Transfer allowed. Sending...\n") + + case protocol.HeaderDisconnecting: + node.Mutex.Lock() + node.State.Stopped = true + node.Mutex.Unlock() + go fmt.Printf("%s disconnected\n", node.Net.Conn.RemoteAddr()) + + case protocol.HeaderReject: + node.Mutex.Lock() + node.State.Stopped = true + node.Mutex.Unlock() + go fmt.Printf("Transfer rejected. Disconnecting...") + } + + if node.State.AllowedToTransfer { + err = sendPiece(file, node.Net.Conn) + if err != nil { + if err == ErrorSentAll { + // the file has been sent fully + fileIDBuff := new(bytes.Buffer) + err = binary.Write(fileIDBuff, binary.BigEndian, file.ID) + if err != nil { + node.Mutex.Lock() + node.State.Stopped = true + node.Mutex.Unlock() + } + + protocol.SendPacket(node.Net.Conn, protocol.Packet{ + Header: protocol.HeaderEndfile, + Body: fileIDBuff.Bytes(), + }) + + node.Mutex.Lock() + node.State.Stopped = true + node.Mutex.Unlock() + } else { + node.Mutex.Lock() + node.State.Stopped = true + node.Mutex.Unlock() + + fmt.Printf("An error occured when sending a piece of \"%s\": %s\n", file.Name, err) + panic(err) + } + } + } + } + + case false: + // RECEIVER + + // connect to the sending node + err := node.connect(node.Net.ConnAddr, node.Net.Port) + if err != nil { + panic(err) + } + + // listen for incoming packets + go receivePackets(node.Net.Conn, node.PacketPipe) + + // mainloop + for { + node.Mutex.Lock() + stopped := node.State.Stopped + node.Mutex.Unlock() + + if stopped { + node.Mutex.Lock() + node.disconnect() + node.Mutex.Unlock() + break + } + + incomingPacket, ok := <-node.PacketPipe + if !ok { + break + } + + switch incomingPacket.Header { + case protocol.HeaderFile: + go func() { + file, err := decodeFilePacket(incomingPacket) + if err != nil { + panic(err) + } + + fmt.Printf("| ID: %d\n| Filename: %s\n| Size: %.2f MB\n| Checksum: %s\n", file.ID, file.Name, float32(file.Size)/1024/1024, file.Checksum) + var answer string + fmt.Printf("| Download ? [Y/n]: ") + fmt.Scanln(&answer) + fmt.Printf("\n\n") + + responsePacketFileIDBuffer := new(bytes.Buffer) + binary.Write(responsePacketFileIDBuffer, binary.BigEndian, file.ID) + + if strings.EqualFold(answer, "y") || answer == "" { + // yes + + err = os.MkdirAll(node.TransferInfo.DownloadsPath, os.ModePerm) + if err != nil { + panic(err) + } + + fullFilePath := filepath.Join(node.TransferInfo.DownloadsPath, file.Name) + + // check if the file already exists; if yes - remove it and replace with a new one + _, err := os.Stat(fullFilePath) + if err == nil { + // exists + // remove it + os.Remove(fullFilePath) + } + + file.Path = fullFilePath + file.Open() + + node.Mutex.Lock() + node.TransferInfo.AcceptedFiles = append(node.TransferInfo.AcceptedFiles, file) + node.Mutex.Unlock() + + // notify the node that we`re ready to transportation + err = protocol.SendPacket(node.Net.Conn, protocol.Packet{ + Header: protocol.HeaderReady, + }) + if err != nil { + panic(err) + } + + // send aceptance packet + protocol.SendPacket(node.Net.Conn, protocol.Packet{ + Header: protocol.HeaderAccept, + Body: responsePacketFileIDBuffer.Bytes(), + }) + + } else { + // no + err = protocol.SendPacket(node.Net.Conn, protocol.Packet{ + Header: protocol.HeaderReject, + Body: responsePacketFileIDBuffer.Bytes(), + }) + if err != nil { + panic(err) + } + + node.Mutex.Lock() + node.State.Stopped = true + node.Mutex.Unlock() + } + }() + + case protocol.HeaderFileBytes: + // check if this file has been accepted to receive + fileIDReader := bytes.NewReader(incomingPacket.Body) + var fileID uint64 + err := binary.Read(fileIDReader, binary.BigEndian, &fileID) + if err != nil { + panic(err) + } + + node.Mutex.Lock() + for _, acceptedFile := range node.TransferInfo.AcceptedFiles { + if acceptedFile.ID == fileID { + // accepted + + // append provided bytes to the file + + fileBytes := incomingPacket.Body[8:] + _, err = acceptedFile.Handler.Write(fileBytes) + if err != nil { + panic(err) + } + } + } + node.Mutex.Unlock() + + err = protocol.SendPacket(node.Net.Conn, protocol.Packet{ + Header: protocol.HeaderReady, + }) + if err != nil { + panic(err) + } + + case protocol.HeaderEndfile: + fileIDReader := bytes.NewReader(incomingPacket.Body) + var fileID uint64 + err := binary.Read(fileIDReader, binary.BigEndian, &fileID) + if err != nil { + panic(err) + } + + node.Mutex.Lock() + for index, acceptedFile := range node.TransferInfo.AcceptedFiles { + if acceptedFile.ID == fileID { + // accepted + + // close the handler afterwards + defer acceptedFile.Handler.Close() + + // remove this file from the pool + node.TransferInfo.AcceptedFiles = append(node.TransferInfo.AcceptedFiles[:index], node.TransferInfo.AcceptedFiles[index+1:]...) + + // compare checksums + realChecksum, err := checksum.GetPartialCheckSum(acceptedFile.Handler) + if err != nil { + panic(err) + } + + fmt.Printf("| Checking hashes for file \"%s\"\n", acceptedFile.Name) + if realChecksum != acceptedFile.Checksum { + fmt.Printf("| %s --- %s file is corrupted\n", realChecksum, acceptedFile.Checksum) + break + } else { + fmt.Printf("| %s --- %s\n", realChecksum, acceptedFile.Checksum) + break + } + } + } + + node.State.Stopped = true + node.Mutex.Unlock() + + case protocol.HeaderDisconnecting: + node.Mutex.Lock() + node.State.Stopped = true + node.Mutex.Unlock() + + go fmt.Printf("%s disconnected\n", node.Net.Conn.RemoteAddr()) + } + } + + } +} diff --git a/src/node/node_test.go b/src/node/node_test.go new file mode 100644 index 0000000..c1fd6ac --- /dev/null +++ b/src/node/node_test.go @@ -0,0 +1,51 @@ +package node + +import ( + "fmt" + "os" + "testing" +) + +// Not complete +func Test_Sendfile(t *testing.T) { + rnodeOptions := NodeOptions{ + IsSending: false, + WorkingPort: 8888, + ServerSide: &ServerSideNodeOptions{ + ServingPath: "", + Recursive: false, + }, + ClientSide: &ClientSideNodeOptions{ + ConnectionAddr: "localhost", + DownloadsFolderPath: "../testfiles/testDownload/", + }, + } + receivingNode, err := NewNode(&rnodeOptions) + if err != nil { + fmt.Printf("Error constructing a new node: %s\n", err) + os.Exit(-1) + } + + snodeOptions := NodeOptions{ + IsSending: true, + WorkingPort: 8888, + ServerSide: &ServerSideNodeOptions{ + ServingPath: "../testfiles/testfile.txt", + Recursive: false, + }, + ClientSide: &ClientSideNodeOptions{ + ConnectionAddr: "", + DownloadsFolderPath: "", + }, + } + + sendingNode, err := NewNode(&snodeOptions) + if err != nil { + fmt.Printf("Error constructing a new node: %s\n", err) + os.Exit(-1) + } + + go receivingNode.Start() + + sendingNode.Start() +} diff --git a/src/node/options.go b/src/node/options.go new file mode 100644 index 0000000..0ff1a1b --- /dev/null +++ b/src/node/options.go @@ -0,0 +1,19 @@ +package node + +type ServerSideNodeOptions struct { + ServingPath string + Recursive bool +} + +type ClientSideNodeOptions struct { + ConnectionAddr string + DownloadsFolderPath string +} + +// Options to configure the node +type NodeOptions struct { + IsSending bool + WorkingPort uint + ServerSide *ServerSideNodeOptions + ClientSide *ClientSideNodeOptions +} diff --git a/src/node/packets.go b/src/node/packets.go new file mode 100644 index 0000000..f5094d2 --- /dev/null +++ b/src/node/packets.go @@ -0,0 +1,93 @@ +// node-specific packets and packet handling +package node + +import ( + "bytes" + "encoding/binary" + "net" + + "github.com/Unbewohnte/ftu/fsys" + "github.com/Unbewohnte/ftu/protocol" +) + +// Reads packets from connection in an endless loop, sends them to the channel +func receivePackets(connection net.Conn, packetPipe chan *protocol.Packet) error { + for { + if connection == nil { + return ErrorNotConnected + } + + packetBytes, err := protocol.ReadFromConn(connection) + if err != nil { + close(packetPipe) + return err + } + + incomingPacket, err := protocol.BytesToPacket(packetBytes) + if err != nil { + close(packetPipe) + return err + } + + packetPipe <- incomingPacket + } +} + +// decodes packet with the header FILE into the fsys.File struct +func decodeFilePacket(filePacket *protocol.Packet) (*fsys.File, error) { + // FILE~(idInBinary)(filenameLengthInBinary)(filename)(filesize)(checksumLengthInBinary)checksum + + // retrieve data from packet body + + // id + packetReader := bytes.NewBuffer(filePacket.Body) + + var fileID uint64 + err := binary.Read(packetReader, binary.BigEndian, &fileID) + if err != nil { + panic(err) + } + + // filename + var filenameLength uint64 + err = binary.Read(packetReader, binary.BigEndian, &filenameLength) + if err != nil { + panic(err) + } + + filenameBytes := make([]byte, filenameLength) + _, err = packetReader.Read(filenameBytes) + if err != nil { + panic(err) + } + + filename := string(filenameBytes) + + // filesize + var filesize uint64 + err = binary.Read(packetReader, binary.BigEndian, &filesize) + if err != nil { + panic(err) + } + + // checksum + var checksumLength uint64 + err = binary.Read(packetReader, binary.BigEndian, &checksumLength) + if err != nil { + panic(err) + } + checksumBytes := make([]byte, checksumLength) + _, err = packetReader.Read(checksumBytes) + if err != nil { + panic(err) + } + checksum := string(checksumBytes) + + return &fsys.File{ + ID: fileID, + Name: filename, + Size: filesize, + Checksum: checksum, + Handler: nil, + }, nil +} diff --git a/src/node/transfer.go b/src/node/transfer.go new file mode 100644 index 0000000..4c718c5 --- /dev/null +++ b/src/node/transfer.go @@ -0,0 +1,132 @@ +package node + +import ( + "bytes" + "encoding/binary" + "io" + "net" + "os" + + "github.com/Unbewohnte/ftu/checksum" + "github.com/Unbewohnte/ftu/fsys" + "github.com/Unbewohnte/ftu/protocol" +) + +// sends a notification about the file +func sendFilePacket(connection net.Conn, file *fsys.File) error { + if connection == nil { + return ErrorNotConnected + } + + err := file.Open() + if err != nil { + return err + } + + // FILE~(idInBinary)(filenameLengthInBinary)(filename)(filesize)(checksumLengthInBinary)checksum + + // send file packet with file description + filePacket := protocol.Packet{ + Header: protocol.HeaderFile, + } + fPacketBodyBuff := new(bytes.Buffer) + + // file id + binary.Write(fPacketBodyBuff, binary.BigEndian, &file.ID) + + // filename + filenameLen := uint64(len([]byte(file.Name))) + binary.Write(fPacketBodyBuff, binary.BigEndian, &filenameLen) + fPacketBodyBuff.Write([]byte(file.Name)) + + // size + binary.Write(fPacketBodyBuff, binary.BigEndian, &file.Size) + + // checksum + fileChecksum, err := checksum.GetPartialCheckSum(file.Handler) + if err != nil { + return err + } + + checksumLen := uint64(len([]byte(fileChecksum))) + binary.Write(fPacketBodyBuff, binary.BigEndian, &checksumLen) + fPacketBodyBuff.Write([]byte(fileChecksum)) + + filePacket.Body = fPacketBodyBuff.Bytes() + + err = protocol.SendPacket(connection, filePacket) + if err != nil { + return err + } + + return nil +} + +// sends a notification about the directory +func sendDirectoryPacket(connection net.Conn, dir *fsys.Directory) error { + if connection == nil { + return ErrorNotConnected + } + + return nil +} + +// sends a piece of file to the connection; The next calls will send +// another piece util the file has been fully sent +func sendPiece(file *fsys.File, connection net.Conn) error { + if file.Handler == nil { + fHandler, err := os.Open(file.Path) + if err != nil { + return err + } + + file.Handler = fHandler + } + + if file.SentBytes == 0 { + file.Handler.Seek(0, io.SeekStart) + } + + if file.Size == file.SentBytes { + return ErrorSentAll + } + + fileBytesPacket := protocol.Packet{ + Header: protocol.HeaderFileBytes, + } + + packetBodyBuff := new(bytes.Buffer) + + // write file ID first + err := binary.Write(packetBodyBuff, binary.BigEndian, &file.ID) + if err != nil { + return err + } + + // fill the remaining space of packet with the contents of a file + canSendBytes := uint64(protocol.MAXPACKETSIZE) - fileBytesPacket.Size() - uint64(packetBodyBuff.Len()) + + if (file.Size - file.SentBytes) < canSendBytes { + canSendBytes = (file.Size - file.SentBytes) + } + fileBytes := make([]byte, canSendBytes) + + read, err := file.Handler.ReadAt(fileBytes, int64(file.SentBytes)) + if err != nil { + return err + } + + packetBodyBuff.Write(fileBytes) + + fileBytesPacket.Body = packetBodyBuff.Bytes() + + // send it to the other side + err = protocol.SendPacket(connection, fileBytesPacket) + if err != nil { + return err + } + + file.SentBytes += uint64(read) + + return nil +} diff --git a/protocol/constants.go b/src/protocol/constants.go similarity index 67% rename from protocol/constants.go rename to src/protocol/constants.go index 4492b28..13d70c7 100644 --- a/protocol/constants.go +++ b/src/protocol/constants.go @@ -6,11 +6,6 @@ package protocol // (packets with size bigger than MAXPACKETSIZE are invalid and will not be sent) const MAXPACKETSIZE uint = 131072 // 128 KiB -// PACKETSIZEDELIMETER. -// Character that delimits one and the other sides of the next incoming packet. -// ie: |packet_size_here|packet_here, where "|" is PACKETSIZEDELIMETER -const PACKETSIZEDELIMETER string = "|" - // HEADERDELIMETER. // Character that delimits header of the packet from the body of the packet. // ie: FILEINFO~img.png diff --git a/src/protocol/headers.go b/src/protocol/headers.go new file mode 100644 index 0000000..ae1dcc4 --- /dev/null +++ b/src/protocol/headers.go @@ -0,0 +1,73 @@ +// This file describes various headers of the protocol and how to use them +package protocol + +type Header string + +// Headers + +//// In the following examples "~" is the HEADERDELIMETER + +// ENCRKEY. +// The FIRST header to be sent. Sent immediately after the connection has been established +// by sender. Body contains randomly generated by sender aes encryption key. +// ie: ENCRKEY~SUPER_SECURE_ENCRYPTION_KEY_|||| +const HeaderEncryptionKey Header = "ENCRKEY" + +// REJECT. +// Sent only by receiver if the receiver has decided to not download the contents. +// The body must contain a file ID in binary. +// ie: REJECT~1111011 +const HeaderReject Header = "REJECT" + +// ACCEPT. +// The opposite of the previous REJECT. Sent by receiver when +// he has agreed to download the file|directory. The body must contain +// the ID of a file in binary that is allowed to upload +// ie: ACCEPT~1111011 +const HeaderAccept Header = "ACCEPT" + +// DONE. +// Sent by sender. Warns the receiver that the transfer has been done and +// there is no more information to give. +// ie: DONE~ +// Usually after the packet with this header has been sent, the receiver will send +// another packet back with header BYE!, telling that it`s going to disconnect +const HeaderDone Header = "DONE" + +// READY. +// Sent by receiver when it has read and processed the last +// FILEBYTES packet. The sender is not allowed to "spam" FILEBYTES +// packets without the permission of receiver. +// ie: READY!~ +const HeaderReady Header = "READY" + +// BYE!. +// Packet with this header can be sent both by receiver and sender. +// It`s used when the sender or the receiver are going to disconnect +// and will not be able to communicate. +// (Usually it`s when the error has happened OR after the DONE header +// has been sent by sender, warning receiver that there is no data to send) +// The BODY is better to be empty. +// ie: BYE!~ +const HeaderDisconnecting Header = "BYE!" + +// FILE. +// Sent by sender, indicating that the file is going to be sent. +// The body structure must follow such structure: +// FILE~(idInBinary)(filenameLengthInBinary)(filename)(filesize)(checksumLengthInBinary)checksum +const HeaderFile Header = "FILE" + +// FILEBYTES. +// Sent only by sender. The packet`s body must contain +// a file`s Identifier and a portion of its bytes. +// ie: FILEBYTES~(fileIDinBinary)(File`sBinaryData) +const HeaderFileBytes Header = "FILEBYTES" + +// ENDFILE +// Sent by sender when the file`s contents fully has been sent. +// The body must contain a file ID. +// ie: ENDFILE~(fileIDIinBinary) +const HeaderEndfile Header = "ENDFILE" + +// DIRECTORY. (TODO) +const HeaderDirectory Header = "DIRECTORY" diff --git a/src/protocol/packet.go b/src/protocol/packet.go new file mode 100644 index 0000000..2806808 --- /dev/null +++ b/src/protocol/packet.go @@ -0,0 +1,157 @@ +// This file describes the general packet structure and provides methods to work with them before|after the transportation + +// General packet structure: +// (size of the whole packet in binary)(packet header)(header delimeter (~))(packet contents) + +package protocol + +import ( + "bytes" + "encoding/binary" + "fmt" + "net" + "strings" + + "github.com/Unbewohnte/ftu/encryption" +) + +// Internal representation of packet before|after the transportation +type Packet struct { + Header Header + Body []byte +} + +// Returns a size of the given packet as if it would be sent and presented in bytes. +// ie: FILE~bytes_here +func (packet *Packet) Size() uint64 { + packetBytes := new(bytes.Buffer) + packetBytes.Write([]byte(packet.Header)) + packetBytes.Write([]byte(HEADERDELIMETER)) + packetBytes.Write(packet.Body) + + return uint64(packetBytes.Len()) +} + +var ErrorNotPacketBytes error = fmt.Errorf("not packet bytes") + +// Converts packet bytes into Packet struct +func BytesToPacket(packetbytes []byte) (*Packet, error) { + // check if there`s a header delimiter present + pString := string(packetbytes) + if !strings.Contains(pString, HEADERDELIMETER) { + return nil, ErrorNotPacketBytes + } + + var header Header + var body []byte + + for counter, b := range packetbytes { + if string(b) == HEADERDELIMETER { + header = Header(packetbytes[0:counter]) + body = packetbytes[counter+1:] + break + } + } + + return &Packet{ + Header: header, + Body: body, + }, nil +} + +var ErrorExceededMaxPacketsize error = fmt.Errorf("too big packet") + +// Converts given packet struct into ready-to-transfer bytes, constructed by following the protocol +func (packet *Packet) ToBytes() ([]byte, error) { + packetSize := packet.Size() + + if packetSize > uint64(MAXPACKETSIZE) { + return nil, ErrorExceededMaxPacketsize + } + + // creating a buffer and writing the whole packet into it + packetBuffer := new(bytes.Buffer) + + // packet size bytes + err := binary.Write(packetBuffer, binary.BigEndian, &packetSize) + if err != nil { + return nil, err + } + + // header, delimeter and body ie: FILENAME~file.txt + packetBuffer.Write([]byte(packet.Header)) + packetBuffer.Write([]byte(HEADERDELIMETER)) + packetBuffer.Write(packet.Body) + + return packetBuffer.Bytes(), nil +} + +// Sends given packet to connection, following all the protocol`s rules. +// ALL packets MUST be sent by this method +func SendPacket(connection net.Conn, packet Packet) error { + packetBytes, err := packet.ToBytes() + if err != nil { + return err + } + // fmt.Printf("DEBUG: sending packet %+v\n", packet) + + // write the result (ie: (packetsize)(header)~(bodybytes)) + connection.Write(packetBytes) + + return nil +} + +// Encrypts packet`s BODY with AES encryption +func (packet *Packet) EncryptBody(key []byte) error { + // encrypting packet`s body + encryptedBody, err := encryption.Encrypt(key, packet.Body) + if err != nil { + return err + } + packet.Body = encryptedBody + + return nil +} + +// Reads a packet from given connection, returns its bytes. +// ASSUMING THAT THE PACKETS ARE SENT BY `SendPacket` function !!!! +func ReadFromConn(connection net.Conn) ([]byte, error) { + var packetSize uint64 + err := binary.Read(connection, binary.BigEndian, &packetSize) + if err != nil { + return nil, err + } + + // have a packetsize, now reading the whole packet + packetBuffer := new(bytes.Buffer) + + // splitting a big-sized packet into chunks and constructing it from pieces + left := packetSize + for { + if left == 0 { + break + } + + buff := make([]byte, 8192) + if left < uint64(len(buff)) { + buff = make([]byte, left) + } + + read, _ := connection.Read(buff) + left -= uint64(read) + + packetBuffer.Write(buff[:read]) + } + + // read the rest of the packet + // packet := make([]byte, packetSize) + // read, err := connection.Read(packet) + // if err != nil { + // return nil, err + // } + + // fmt.Printf("DEBUG: read from connection: %s; length: %d\n", packetBuffer.Bytes()[:40], packetBuffer.Len()) + // fmt.Printf("DEBUG: read from connection: %s; length: %d\n", packet, len(packet)) + + return packetBuffer.Bytes(), nil +} diff --git a/protocol/protocol_test.go b/src/protocol/protocol_test.go similarity index 58% rename from protocol/protocol_test.go rename to src/protocol/protocol_test.go index a488b87..f7c0cdf 100644 --- a/protocol/protocol_test.go +++ b/src/protocol/protocol_test.go @@ -9,30 +9,28 @@ import ( // Practically tests the whole protocol func TestTransfer(t *testing.T) { packet := Packet{ - Header: HeaderFilename, + Header: "randomheader", Body: []byte("fIlEnAmE.txt"), } - packetBuffer := new(bytes.Buffer) - packetBuffer.Write([]byte(packet.Header)) - packetBuffer.Write([]byte(HEADERDELIMETER)) - packetBuffer.Write(packet.Body) - // a valid representation of received packet`s bytes - packetBytes := packetBuffer.Bytes() + packetBytes, err := packet.ToBytes() + if err != nil { + t.Fatalf("%s", err) + } // imitating a connection l, err := net.Listen("tcp", ":9999") if err != nil { - t.Errorf("Unexpected error: %s", err) + t.Fatalf("%s", err) } c, err := net.Dial("tcp", "localhost:9999") if err != nil { - t.Errorf("Unexpected error: %s", err) + t.Fatalf("%s", err) } cc, err := l.Accept() if err != nil { - t.Errorf("Unexpected error: %s", err) + t.Fatalf("%s", err) } defer c.Close() defer cc.Close() @@ -40,25 +38,28 @@ func TestTransfer(t *testing.T) { // sending packet err = SendPacket(cc, packet) if err != nil { - t.Errorf("SendPacket failed: %s", err) + t.Fatalf("SendPacket failed: %s", err) } - // + // reading it from c receivedPacket, err := ReadFromConn(c) if err != nil { - t.Errorf("ReadFromConn failed: %s", err) + t.Fatalf("ReadFromConn failed: %s", err) } + // drop packetsize for valid packet bytes because they are also dropped in ReadFromConn + packetBytes = packetBytes[8:] + for index, b := range receivedPacket { if b != packetBytes[index] { - t.Errorf("Failed: wanted: %v, got: %v", packetBytes[index], b) + t.Fatalf("Error: packet bytes do not match: expected %v, got: %v; valid packet: %v; received packet: %v", string(packetBytes[index]), string(b), packetBytes, receivedPacket) } } } func TestBytesToPacket(t *testing.T) { packet := Packet{ - Header: HeaderFilename, + Header: HeaderFileBytes, Body: []byte("fIlEnAmE.txt"), } @@ -70,9 +71,12 @@ func TestBytesToPacket(t *testing.T) { // a valid representation of received packet`s bytes packetBytes := packetBuffer.Bytes() - convertedPacket := BytesToPacket(packetBytes) + convertedPacket, err := BytesToPacket(packetBytes) + if err != nil { + t.Fatalf("BytesToPacket error: %s", err) + } if convertedPacket.Header != packet.Header || string(convertedPacket.Body) != string(packet.Body) { - t.Errorf("BytesToPacket failed") + t.Fatalf("BytesToPacket error: header or body of converted packet does not match with the original") } } diff --git a/testfiles/testfile.txt b/src/testfiles/testDownload/testfile.txt old mode 100644 new mode 100755 similarity index 100% rename from testfiles/testfile.txt rename to src/testfiles/testDownload/testfile.txt diff --git a/testfiles/testdir/testdir2/testfile3.txt b/src/testfiles/testdir/testdir2/testfile3.txt similarity index 100% rename from testfiles/testdir/testdir2/testfile3.txt rename to src/testfiles/testdir/testdir2/testfile3.txt diff --git a/testfiles/testdir/testfile2.txt b/src/testfiles/testdir/testfile2.txt similarity index 100% rename from testfiles/testdir/testfile2.txt rename to src/testfiles/testdir/testfile2.txt diff --git a/testfiles/testdir3/testfile4 b/src/testfiles/testdir3/testfile4 similarity index 100% rename from testfiles/testdir3/testfile4 rename to src/testfiles/testdir3/testfile4 diff --git a/src/testfiles/testfile.txt b/src/testfiles/testfile.txt new file mode 100644 index 0000000..99a7032 --- /dev/null +++ b/src/testfiles/testfile.txt @@ -0,0 +1,11 @@ +727 WYSI Airman Badeu square + + + +doable + + +FCeeeeeeeeeeeeeeeeeeeeeeeee + + +testfile it is \ No newline at end of file