Browse Source

Rewrite; Current status: can send one file, not encrypted

main
Unbewohnte 3 years ago
parent
commit
2da9cd5bd0
  1. 2
      LICENSE
  2. 17
      Makefile
  3. 68
      README.md
  4. 106
      checksum/checksum.go
  5. 42
      checksum/checksum_test.go
  6. 12
      install.sh
  7. 80
      main.go
  8. 56
      node/node.go
  9. 11
      node/noder.go
  10. 23
      node/options.go
  11. 83
      protocol/headers.go
  12. 173
      protocol/packet.go
  13. 30
      receiver/file.go
  14. 356
      receiver/receiver.go
  15. 9
      receiver/transferinfo.go
  16. 45
      sender/file.go
  17. 35
      sender/ip.go
  18. 341
      sender/sender.go
  19. 9
      sender/transferinfo.go
  20. 9
      src/LICENSE
  21. 18
      src/addr/outbound.go
  22. 79
      src/checksum/checksum.go
  23. 25
      src/checksum/checksum_test.go
  24. 0
      src/encryption/decrypt.go
  25. 0
      src/encryption/encrypt.go
  26. 0
      src/encryption/encryption_test.go
  27. 0
      src/encryption/key.go
  28. 10
      src/fsys/dir.go
  29. 9
      src/fsys/dir_test.go
  30. 17
      src/fsys/file.go
  31. 2
      src/fsys/file_test.go
  32. 0
      src/go.mod
  33. 106
      src/main.go
  34. 8
      src/node/errors.go
  35. 429
      src/node/node.go
  36. 51
      src/node/node_test.go
  37. 19
      src/node/options.go
  38. 93
      src/node/packets.go
  39. 132
      src/node/transfer.go
  40. 5
      src/protocol/constants.go
  41. 73
      src/protocol/headers.go
  42. 157
      src/protocol/packet.go
  43. 38
      src/protocol/protocol_test.go
  44. 0
      src/testfiles/testDownload/testfile.txt
  45. 0
      src/testfiles/testdir/testdir2/testfile3.txt
  46. 0
      src/testfiles/testdir/testfile2.txt
  47. 0
      src/testfiles/testdir3/testfile4
  48. 11
      src/testfiles/testfile.txt

2
LICENSE

@ -1,6 +1,6 @@
The MIT License (MIT) 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: 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:

17
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)

68
README.md

@ -1,4 +1,4 @@
# FTU (FileTransferringUtility) # ftu (FileTransferringUtility)
## Send files through the Net ! ## 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 ## ● Installation
### ● From release (Pre-compiled) ### ● From release (Pre-compiled)
- Proceed to [releases page](https://github.com/Unbewohnte/ftu/releases) - Proceed to [releases page](https://github.com/Unbewohnte/ftu/releases)
- Choose a version/architecture you have and download an archive - Choose a version/architecture you have and download an archive
- Unpack 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) ### ● 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` - `git clone https://github.com/Unbewohnte/ftu.git`
- `cd` into the folder - `cd` into the folder
- `go build` - to simply compile for your OS/ARCHITECTURE || `CGO_ENABLED=0 go build` - to compile a static executable - `make`
- If on GNU/Linux - run `sudo make install`
### ● Final steps (optional)
- `cd` into folder if you`re not there already
- `chmod +x install.sh` - make installation script executable
- `sudo ./install.sh`
Now you have ftu installed ! Now you have ftu installed !
--- ---
## ● Usage ## ● Usage
`ftu [FLAGS_HERE]` `ftu -h` - to print a usage message
### ● Flags `ftu [FLAGS]`
`ftu --help` - to get all flags` description
- `-port` (int) - specifies a working port (if sending - listens on this port, else - tries to connect to this port); ### ● FLAGs
- `-addr` (string) - specifies an address to connect to; - -p [Uinteger_here] for port
- `-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; - -r [true|false] for recursive sending of a directory
- `-downloadto` (string) - specifies path to a folder where the receiver wants to store downloaded file; - -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 ### ● Examples
- `ftu -sharefile="/home/some_path_here/FILETOSHARE.zip"` - creates a server that will share `FILETOSHARE.zip` on port `8080` `ftu -p 89898 -s /home/user/Downloads/someVideo.mp4`
- `ftu -sharefile="/home/some_path_here/FILETOSHARE.zip" - port=727` - same as before, but on port `727` creates a node on a non-default port 89898 that will send "someVideo.mp4" to the other node that connects to you
- `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 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 ## ● Testing
In 'ftu' directory: In 'src' directory:
- `go test ./...` - to test everything - `go test ./...` - to test everything
- `go test -v ./...` - to test everything, with additional information - `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 MIT
## ● TODO ## ● TODO
- multiple filepaths as args, not as a flag - Send directory
- send all files in a directory - Wire back encryption
- send all files in a directory recursively
- ip address as an arg, not as a flag

106
checksum/checksum.go

@ -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
}

42
checksum/checksum_test.go

@ -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")
}
}
}

12
install.sh

@ -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

80
main.go

@ -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()
}
}

56
node/node.go

@ -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
}

11
node/noder.go

@ -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
}

23
node/options.go

@ -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
}

83
protocol/headers.go

@ -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!"

173
protocol/packet.go

@ -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
}

30
receiver/file.go

@ -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
}

356
receiver/receiver.go

@ -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()
}
}
}

9
receiver/transferinfo.go

@ -1,9 +0,0 @@
package receiver
import "time"
type transferInfo struct {
ReceivedFileBytesPackets uint64
ApproximateNumOfPackets uint64
StartTime time.Time
}

45
sender/file.go

@ -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
}

35
sender/ip.go

@ -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
}

341
sender/sender.go

@ -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()
}
}
}

9
sender/transferinfo.go

@ -1,9 +0,0 @@
package sender
import "time"
type transferInfo struct {
SentFileBytesPackets uint64
ApproximateNumOfPackets uint64
StartTime time.Time
}

9
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.

18
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
}

79
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
}

25
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")
}
}

0
encryption/decrypt.go → src/encryption/decrypt.go

0
encryption/encrypt.go → src/encryption/encrypt.go

0
encryption/encryption_test.go → src/encryption/encryption_test.go

0
encryption/key.go → src/encryption/key.go

10
fs/dir.go → src/fsys/dir.go

@ -1,4 +1,4 @@
package fs package fsys
import ( import (
"fmt" "fmt"
@ -18,15 +18,11 @@ type Directory struct {
var ErrorNotDirectory error = fmt.Errorf("not a directory") var ErrorNotDirectory error = fmt.Errorf("not a directory")
func GetDir(path string, recursive bool) (*Directory, error) { func GetDir(path string, recursive bool) (*Directory, error) {
fmt.Println("Provided path ", path)
absPath, err := filepath.Abs(path) absPath, err := filepath.Abs(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
fmt.Println("absolute path ", absPath)
stats, err := os.Stat(absPath) stats, err := os.Stat(absPath)
if err != nil { if err != nil {
return nil, err return nil, err
@ -56,8 +52,6 @@ func GetDir(path string, recursive bool) (*Directory, error) {
// do the recursive magic // do the recursive magic
innerDirPath := filepath.Join(absPath, entry.Name()) innerDirPath := filepath.Join(absPath, entry.Name())
fmt.Println("inner dir path ", innerDirPath)
innerDir, err := GetDir(innerDirPath, true) innerDir, err := GetDir(innerDirPath, true)
if err != nil { if err != nil {
return nil, err return nil, err
@ -70,8 +64,6 @@ func GetDir(path string, recursive bool) (*Directory, error) {
} else { } else {
innerFilePath := filepath.Join(absPath, entryInfo.Name()) innerFilePath := filepath.Join(absPath, entryInfo.Name())
fmt.Println("inner file path ", innerFilePath)
innerFile, err := GetFile(innerFilePath) innerFile, err := GetFile(innerFilePath)
if err != nil { if err != nil {
return nil, err return nil, err

9
fs/dir_test.go → src/fsys/dir_test.go

@ -1,4 +1,4 @@
package fs package fsys
import "testing" import "testing"
@ -19,13 +19,8 @@ func Test_GetDirRecursive(t *testing.T) {
t.Fatalf("GetDir error: %s", err) t.Fatalf("GetDir error: %s", err)
} }
expectedAmountOfUpperDirectories := 2 expectedAmountOfUpperDirectories := 3
if len(dir.Directories) != expectedAmountOfUpperDirectories { if len(dir.Directories) != expectedAmountOfUpperDirectories {
t.Fatalf("GetDir error: expected to have %d inner directories; got %d", expectedAmountOfUpperDirectories, len(dir.Directories)) 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)
}
} }

17
fs/file.go → src/fsys/file.go

@ -1,4 +1,4 @@
package fs package fsys
import ( import (
"fmt" "fmt"
@ -6,13 +6,18 @@ import (
"path/filepath" "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 { type File struct {
ID uint64
Name string Name string
Path string Path string
ParentPath string ParentPath string
Size uint64 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") var ErrorNotFile error = fmt.Errorf("not a file")
@ -38,6 +43,7 @@ func GetFile(path string) (*File, error) {
} }
file := File{ file := File{
ID: FileIDsCounter,
Name: stats.Name(), Name: stats.Name(),
Path: absPath, Path: absPath,
ParentPath: filepath.Dir(absPath), ParentPath: filepath.Dir(absPath),
@ -45,12 +51,15 @@ func GetFile(path string) (*File, error) {
Handler: nil, Handler: nil,
} }
// increment ids counter so the next file will have a different ID
FileIDsCounter++
return &file, nil return &file, nil
} }
// Opens file for read/write operations // Opens file for read/write operations
func (file *File) Open() error { 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 { if err != nil {
return err return err
} }

2
fs/file_test.go → src/fsys/file_test.go

@ -1,4 +1,4 @@
package fs package fsys
import ( import (
"io" "io"

0
go.mod → src/go.mod

106
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()
}

8
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")
)

429
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())
}
}
}
}

51
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()
}

19
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
}

93
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
}

132
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
}

5
protocol/constants.go → src/protocol/constants.go

@ -6,11 +6,6 @@ package protocol
// (packets with size bigger than MAXPACKETSIZE are invalid and will not be sent) // (packets with size bigger than MAXPACKETSIZE are invalid and will not be sent)
const MAXPACKETSIZE uint = 131072 // 128 KiB 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. // HEADERDELIMETER.
// Character that delimits header of the packet from the body of the packet. // Character that delimits header of the packet from the body of the packet.
// ie: FILEINFO~img.png // ie: FILEINFO~img.png

73
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"

157
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
}

38
protocol/protocol_test.go → src/protocol/protocol_test.go

@ -9,30 +9,28 @@ import (
// Practically tests the whole protocol // Practically tests the whole protocol
func TestTransfer(t *testing.T) { func TestTransfer(t *testing.T) {
packet := Packet{ packet := Packet{
Header: HeaderFilename, Header: "randomheader",
Body: []byte("fIlEnAmE.txt"), 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 // 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 // imitating a connection
l, err := net.Listen("tcp", ":9999") l, err := net.Listen("tcp", ":9999")
if err != nil { if err != nil {
t.Errorf("Unexpected error: %s", err) t.Fatalf("%s", err)
} }
c, err := net.Dial("tcp", "localhost:9999") c, err := net.Dial("tcp", "localhost:9999")
if err != nil { if err != nil {
t.Errorf("Unexpected error: %s", err) t.Fatalf("%s", err)
} }
cc, err := l.Accept() cc, err := l.Accept()
if err != nil { if err != nil {
t.Errorf("Unexpected error: %s", err) t.Fatalf("%s", err)
} }
defer c.Close() defer c.Close()
defer cc.Close() defer cc.Close()
@ -40,25 +38,28 @@ func TestTransfer(t *testing.T) {
// sending packet // sending packet
err = SendPacket(cc, packet) err = SendPacket(cc, packet)
if err != nil { if err != nil {
t.Errorf("SendPacket failed: %s", err) t.Fatalf("SendPacket failed: %s", err)
} }
// // reading it from c
receivedPacket, err := ReadFromConn(c) receivedPacket, err := ReadFromConn(c)
if err != nil { 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 { for index, b := range receivedPacket {
if b != packetBytes[index] { 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) { func TestBytesToPacket(t *testing.T) {
packet := Packet{ packet := Packet{
Header: HeaderFilename, Header: HeaderFileBytes,
Body: []byte("fIlEnAmE.txt"), Body: []byte("fIlEnAmE.txt"),
} }
@ -70,9 +71,12 @@ func TestBytesToPacket(t *testing.T) {
// a valid representation of received packet`s bytes // a valid representation of received packet`s bytes
packetBytes := packetBuffer.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) { 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")
} }
} }

0
testfiles/testfile.txt → src/testfiles/testDownload/testfile.txt

0
testfiles/testdir/testdir2/testfile3.txt → src/testfiles/testdir/testdir2/testfile3.txt

0
testfiles/testdir/testfile2.txt → src/testfiles/testdir/testfile2.txt

0
testfiles/testdir3/testfile4 → src/testfiles/testdir3/testfile4

11
src/testfiles/testfile.txt

@ -0,0 +1,11 @@
727 WYSI Airman Badeu square
doable
FCeeeeeeeeeeeeeeeeeeeeeeeee
testfile it is
Loading…
Cancel
Save