Browse Source

First working version

main
Unbewohnte 3 years ago
commit
ef149fb0b8
  1. BIN
      FTU
  2. 9
      LICENSE
  3. 56
      README.md
  4. 197
      client/client.go
  5. 3
      go.mod
  6. 72
      main.go
  7. 155
      protocol/protocol.go
  8. 37
      server/file.go
  9. 236
      server/server.go

BIN
FTU

Binary file not shown.

9
LICENSE

@ -0,0 +1,9 @@
The MIT License (MIT)
Copyright © 2021 Unbewohne | Nikolay Kasyanov
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

56
README.md

@ -0,0 +1,56 @@
# FTU (FileTransferringUtility)
## Send files through the Net !
---
## What is that ?
This application is like an FTP server, but overcomplicated, probably overengineered monstrosity. (basically, a file server, but P2P and only one file is shared).
---
## Why ?
Learning
---
## How does this work ?
In order to transfer one file on one computer to another - they need to establish a connection.
In order to establish a connection - there needs to be a 1) server (the owner of the file), waiting for connections, and a 2) client, who will try to connect to a server. If the requirements are met - a client will connect to a server and the packet exchange will begin.
The server and the client needs to communicate with packets according to certain rules, given by a [protocol](https://github.com/unbewohnte/FTU/protocol/).
In my implementation there is only one basic packet template with fixed fields. The packets are divided into several groups by its headers, this way my basic packet`s template can be used in many ways, without need of creating a brand-new packet with a different kind of a template.
Thus, with a connection and a way of communication, the server will send a handshake packet to a client that describes a filename and its size. The client will have the choice of accepting or rejecting the packet. If rejected - the connection will be closed and the program will exit. If accepted - the file will be transfered via packets.
---
## Known issues|problems|lack of features|reasons why it`s bad
1. **VERY** slow
2. **VERY** expensive on resources
3. If `MAXFILEDATASIZE` is bigger than appr. 1024 - the packets on the other end will not be unmarshalled due to error ??
4. Lack of proper error-handling
5. Lack of information about the process of transferring (ETA, lost packets, etc.)
6. No way to verify if the transferred file is not corrupted
7. No encryption
## Good points
1. It... works ?
---
## IMPORTANT NOTE
This is NOT intended to be a serious application. I'm learning and this is a product of my curiosity. If you're a beginner too, please don't try to find something useful in my code, I am not an expert.
Also, this utility only works if both the server and the client have a port-forwarding enabled and configured. Fortunatelly, locally it works without any port-forwarding.
---
## Inspired by [croc](https://github.com/schollz/croc)
---
## License
MIT

197
client/client.go

@ -0,0 +1,197 @@
package client
import (
"fmt"
"net"
"os"
"path/filepath"
"strings"
"github.com/Unbewohnte/FTU/protocol"
)
// Representation of a tcp client
type Client struct {
DownloadsFolder string
Connection net.Conn
IncomingPackets chan protocol.Packet
Stopped bool
ReadyToReceive bool
PacketCounter uint64
}
// Creates a new client with default fields
func NewClient(downloadsFolder string) *Client {
os.MkdirAll(downloadsFolder, os.ModePerm)
info, err := os.Stat(downloadsFolder)
if err != nil {
panic(err)
}
if !info.IsDir() {
panic("Downloads folder is not a directory")
}
incomingPacketsChan := make(chan protocol.Packet, 5)
var PacketCounter uint64 = 0
fmt.Println("Created a new client")
return &Client{
DownloadsFolder: downloadsFolder,
Connection: nil,
IncomingPackets: incomingPacketsChan,
Stopped: false,
ReadyToReceive: false,
PacketCounter: PacketCounter,
}
}
// Closes the connection
func (c *Client) Disconnect() {
c.Connection.Close()
}
// Closes the connection, warns the server and exits the mainloop
func (c *Client) Stop() {
disconnectionPacket := protocol.Packet{
Header: protocol.HeaderDisconnecting,
}
protocol.SendPacket(c.Connection, disconnectionPacket)
c.Stopped = true
c.Disconnect()
}
// Connects to a given address over tcp. Sets a connection to a client
func (c *Client) Connect(addr string) error {
fmt.Printf("Trying to connect to %s...\n", addr)
connection, err := net.Dial("tcp", addr)
if err != nil {
return fmt.Errorf("could not connect to %s: %s", addr, err)
}
c.Connection = connection
fmt.Println("Connected to ", c.Connection.RemoteAddr())
return nil
}
// Handles the fileinfo packet. The choice of acceptance is given to the user
func (c *Client) HandleFileOffer(fileinfoPacket protocol.Packet) error {
// inform the user about the file
fmt.Printf("Incoming fileinfo packet:\nFilename: %s\nFilesize: %.3fMB\nAccept ? [Y/N]: ",
fileinfoPacket.Filename, float32(fileinfoPacket.Filesize)/1024/1024)
// get and process the input
var input string
fmt.Scanln(&input)
input = strings.TrimSpace(input)
input = strings.ToLower(input)
// reject the file
if input != "y" {
rejectionPacket := protocol.Packet{
Header: protocol.HeaderReject,
}
err := protocol.SendPacket(c.Connection, rejectionPacket)
if err != nil {
return fmt.Errorf("could not send a rejection packet: %s", err)
}
c.ReadyToReceive = false
return nil
}
// accept the file
acceptancePacket := protocol.Packet{
Header: protocol.HeaderAccept,
}
err := protocol.SendPacket(c.Connection, acceptancePacket)
if err != nil {
return fmt.Errorf("could not send an acceptance packet: %s", err)
}
// can and ready to receive file packets
c.ReadyToReceive = true
return nil
}
// Handles the download by writing incoming bytes into the file
func (c *Client) WritePieceOfFile(filePacket protocol.Packet) error {
c.ReadyToReceive = false
// open|create a file with the same name as the filepacket`s file name
file, err := os.OpenFile(filepath.Join(c.DownloadsFolder, filePacket.Filename), os.O_CREATE|os.O_APPEND|os.O_WRONLY, os.ModePerm)
if err != nil {
return err
}
// just write the filedata
file.Write(filePacket.FileData)
file.Close()
c.PacketCounter++
c.ReadyToReceive = true
return nil
}
// Listens in an endless loop; reads incoming packages and puts them into channel
func (c *Client) ReceivePackets() {
for {
incomingPacket := protocol.ReadFromConn(c.Connection)
isvalid, _ := protocol.IsValidPacket(incomingPacket)
if !isvalid {
continue
}
c.IncomingPackets <- incomingPacket
}
}
// The "head" of the client. Similarly as in server`s logic "glues" everything together.
// Current structure allows the client to receive any type of packet
// in any order and react correspondingly
func (c *Client) MainLoop() {
go c.ReceivePackets()
for {
if c.Stopped {
// exit the mainloop
break
}
// 1) send -> 2) handle received if necessary
// send a packet telling server to send another piece of file
if c.ReadyToReceive {
readyPacket := protocol.Packet{
Header: protocol.HeaderReady,
}
protocol.SendPacket(c.Connection, readyPacket)
c.ReadyToReceive = false
}
// no incoming packets ? Skipping the packet handling part
if len(c.IncomingPackets) == 0 {
continue
}
// take the packet and handle depending on the header
incomingPacket := <-c.IncomingPackets
// handling each packet header differently
switch incomingPacket.Header {
case protocol.HeaderFileInfo:
go c.HandleFileOffer(incomingPacket)
case protocol.HeaderFileData:
go c.WritePieceOfFile(incomingPacket)
case protocol.HeaderDisconnecting:
// the server is ded, no need to stay alive as well
fmt.Println("Done. Got ", c.PacketCounter, " packets in total")
c.Stopped = true
c.Stop()
}
}
}

3
go.mod

@ -0,0 +1,3 @@
module github.com/Unbewohnte/FTU
go 1.16

72
main.go

@ -0,0 +1,72 @@
package main
import (
"flag"
"fmt"
"os"
"strings"
"github.com/Unbewohnte/FTU/client"
"github.com/Unbewohnte/FTU/server"
)
// flags
var PORT *int = flag.Int("port", 8080, "Specifies a port for a server")
var SERVERADDR *string = flag.String("addr", "", "Specifies an IP for connection")
var ISSERVER *bool = flag.Bool("server", false, "Server")
var DOWNLOADSFOLDER *string = flag.String("downloadto", "", "Specifies where the client will store downloaded file")
var SHAREDFILE *string = flag.String("sharefile", "", "Specifies what file server will serve")
// helpMessage
var HELPMSG string = `
"-port", default: 8080, Specifies a port for a server
"-addr", default: "", Specifies an IP for connection
"-server", default: false, Share file or connect and receive one ?
"-downloadto", default: "", Specifies where the client will store downloaded file
"-sharefile", default: "", Specifies what file server will share`
// Input-validation
func checkFlags() {
if *ISSERVER {
if strings.TrimSpace(*SHAREDFILE) == "" {
fmt.Println("No file specified !\n", HELPMSG)
os.Exit(1)
}
if *PORT <= 0 {
fmt.Println("Invalid port !\n", HELPMSG)
os.Exit(1)
}
} else if !*ISSERVER {
if strings.TrimSpace(*SERVERADDR) == "" {
fmt.Println("Invalid IP address !\n", HELPMSG)
os.Exit(1)
}
if strings.TrimSpace(*DOWNLOADSFOLDER) == "" {
*DOWNLOADSFOLDER = "./downloads/"
fmt.Println("Empty downloads folder. Changed to ./downloads/")
}
}
}
// parse flags, validate given values
func init() {
flag.Parse()
checkFlags()
}
func main() {
if *ISSERVER {
// 1) create server -> 2) wait for a client ->|
// 3) send handshake packet -> 4) if accepted - upload file
server := server.NewServer(*PORT, *SHAREDFILE)
server.WaitForConnection()
server.MainLoop()
} else {
// 1) create client -> 2) try to connect to a server -> 3) wait for a handshake ->|
// 4) accept or refuse -> 5) download|don`t_download file
client := client.NewClient(*DOWNLOADSFOLDER)
client.Connect(fmt.Sprintf("%s:%d", *SERVERADDR, *PORT))
client.MainLoop()
}
}

155
protocol/protocol.go

@ -0,0 +1,155 @@
package protocol
import (
"encoding/json"
"fmt"
"net"
"strconv"
"strings"
)
// a package that describes how server and client should communicate
const MAXPACKETSIZE int = 2048 // whole packet
const MAXFILEDATASIZE int = 512 // only `FileData` | MUST be less than `MAXPACKETSIZE`
const PACKETSIZEDELIMETER string = "|"
// Headers
type Header string
const HeaderFileData Header = "FILEDATA"
const HeaderFileInfo Header = "FILEINFO"
const HeaderReject Header = "FILE_REJECT"
const HeaderAccept Header = "FILE_ACCEPT"
const HeaderReady Header = "READY"
const HeaderDisconnecting Header = "BYE!"
// Packet structure.
// A Packet without a header is an invalid packet
type Packet struct {
Header Header `json:"Header"`
Filename string `json:"Filename"`
Filesize uint64 `json:"Filesize"`
FileData []byte `json:"Filedata"`
}
// converts valid packet bytes into `Packet` struct
func ReadPacketBytes(packetBytes []byte) Packet {
// makes sure that the packet is ALWAYS less or equal to the maximum packet size
// this allows not to use any client or server checks
//fmt.Println("READING packet: ", string(packetBytes))
var packet Packet
err := json.Unmarshal(packetBytes, &packet)
if err != nil {
fmt.Printf("Could not unmarshal the packet: %s\n", err)
return Packet{}
}
return packet
}
// Converts `Packet` struct into []byte
func EncodePacket(packet Packet) []byte {
packetBytes, err := json.Marshal(packet)
if err != nil {
return []byte("")
}
return packetBytes
}
// Measures the packet length
func MeasurePacket(packet Packet) uint64 {
packetBytes := EncodePacket(packet)
return uint64(len(packetBytes))
}
// Checks if given packet is valid, returns a boolean and an explanation message
func IsValidPacket(packet Packet) (bool, string) {
if MeasurePacket(packet) > uint64(MAXPACKETSIZE) {
return false, "Exceeded MAXPACKETSIZE"
}
if len(packet.FileData) > MAXFILEDATASIZE {
return false, "Exceeded MAXFILEDATASIZE"
}
if strings.TrimSpace(string(packet.Header)) == "" {
return false, "Blank header"
}
return true, ""
}
// Sends a given packet to connection using a special sending format
// ALL packets MUST be sent by this method
func SendPacket(connection net.Conn, packet Packet) error {
isvalid, msg := IsValidPacket(packet)
if !isvalid {
return fmt.Errorf("this packet is invalid !: %v; The error: %v", packet, msg)
}
packetSize := MeasurePacket(packet)
// write packetsize between delimeters (ie: |727|{"HEADER":"PING"...})
connection.Write([]byte(fmt.Sprintf("%s%d%s", PACKETSIZEDELIMETER, packetSize, PACKETSIZEDELIMETER)))
// write an actual packet
connection.Write(EncodePacket(packet))
//fmt.Println("Sending packet: ", string(EncodePacket(packet)), " Length: ", packetSize)
return nil
}
// Reads a packet from a connection by retrieving the packet length. Only once
// ASSUMING THAT THE PACKETS ARE SENT BY `SendPacket` method !!!!
func ReadFromConn(connection net.Conn) Packet {
var gotPacketSize bool = false
var delimeterCounter int = 0
var packetSizeStr string = ""
var packetSize int = 0
for {
// still need to get a packetsize
if !gotPacketSize {
// reading byte-by-byte
buffer := make([]byte, 1)
connection.Read(buffer)
// found a delimeter
if string(buffer) == PACKETSIZEDELIMETER {
delimeterCounter++
// the first delimeter is found, skipping the rest of the code
if delimeterCounter == 1 {
continue
}
}
// found the first delimeter, skip was performed, now reading an actual packetsize
if delimeterCounter == 1 {
packetSizeStr += string(buffer)
} else if delimeterCounter == 2 {
// found the last delimeter, thus already read the whole packetsize
packetSize, _ = strconv.Atoi(packetSizeStr)
gotPacketSize = true
}
// skipping the rest of the code because we don`t know the packet size yet
continue
}
// have a packetsize, now reading the whole packet
//fmt.Println("Got a packetsize!: ", packetSize)
packetBuffer := make([]byte, packetSize)
connection.Read(packetBuffer)
packet := ReadPacketBytes(packetBuffer)
isvalid, _ := IsValidPacket(packet)
if isvalid {
return packet
}
break
}
return Packet{}
}

37
server/file.go

@ -0,0 +1,37 @@
package server
import (
"fmt"
"os"
)
// Struct that represents the served file. Used internally in the server
type File struct {
path string
Filename string
Filesize uint64
SentBytes uint64
LeftBytes uint64
SentPackets uint64
Handler *os.File
}
// Prepares a file for serving. Used for preparing info before sending a handshake
func getFile(path string) (*File, error) {
info, err := os.Stat(path)
if err != nil {
return nil, fmt.Errorf("could not get a fileinfo: %s", err)
}
handler, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("couldn`t be able to open the file: %s", err)
}
return &File{
path: path,
Filename: info.Name(),
Filesize: uint64(info.Size()),
SentBytes: 0,
LeftBytes: uint64(info.Size()),
Handler: handler,
}, nil
}

236
server/server.go

@ -0,0 +1,236 @@
package server
import (
"fmt"
"io"
"net"
"net/http"
"github.com/Unbewohnte/FTU/protocol"
)
// gets a local ip. Borrowed from StackOverflow, thank you, whoever I brought it from
func GetLocalIP() (string, error) {
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
return "", err
}
defer conn.Close()
localAddr := conn.LocalAddr().(*net.UDPAddr)
return string(localAddr.IP), nil
}
// gets a remote ip. Borrowed from StackOverflow, thank you, whoever I brought it from
func GetRemoteIP() (string, error) {
resp, err := http.Get("https://api.ipify.org?format=text")
if err != nil {
return "", fmt.Errorf("could not make a request to get your remote IP: %s", err)
}
defer resp.Body.Close()
ip, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("could not read a response: %s", err)
}
return string(ip), nil
}
// The main server struct
type Server struct {
Port int
FileToTransfer *File
Listener net.Listener
Connection net.Conn
IncomingPackets chan protocol.Packet
CanTransfer bool
Stopped bool
}
// Creates a new server with default fields
func NewServer(port int, filepath string) *Server {
fileToTransfer, err := getFile(filepath)
if err != nil {
panic(err)
}
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
panic(err)
}
incomingPacketsChan := make(chan protocol.Packet, 5)
remoteIP, err := GetRemoteIP()
if err != nil {
panic(err)
}
localIP, err := GetLocalIP()
if err != nil {
panic(err)
}
fmt.Printf("Created a new server at %s:%d (remote)\n%s:%d (local)\n", remoteIP, port, localIP, port)
return &Server{
Port: port,
FileToTransfer: fileToTransfer,
Listener: listener,
Connection: nil,
IncomingPackets: incomingPacketsChan,
Stopped: false,
}
}
// Closes the connection, warns about it the client and exits the mainloop
func (s *Server) Stop() {
disconnectionPacket := protocol.Packet{
Header: protocol.HeaderDisconnecting,
}
err := protocol.SendPacket(s.Connection, disconnectionPacket)
if err != nil {
panic(fmt.Sprintf("could not send a disconnection packet: %s", err))
}
s.Stopped = true
s.Disconnect()
}
// Closes current connection
func (s *Server) Disconnect() {
s.Connection.Close()
}
// Accepts one connection
func (s *Server) WaitForConnection() error {
connection, err := s.Listener.Accept()
if err != nil {
return fmt.Errorf("could not accept a connection: %s", err)
}
s.Connection = connection
fmt.Println("New connection from ", s.Connection.RemoteAddr())
return nil
}
// Closes the listener. Used only when there is still no connection from `AcceptConnections`
func (s *Server) StopListening() {
s.Listener.Close()
}
// Sends a packet with all information about a file to current connection
func (s *Server) SendOffer() error {
err := protocol.SendPacket(s.Connection, protocol.Packet{
Header: protocol.HeaderFileInfo,
Filename: s.FileToTransfer.Filename,
Filesize: s.FileToTransfer.Filesize,
})
if err != nil {
return fmt.Errorf("could not send an information about the file: %s", err)
}
return nil
}
// Sends one file packet to the client
func (s *Server) SendPiece() error {
// if no data to send - exit
if s.FileToTransfer.LeftBytes == 0 {
fmt.Printf("Done. Sent %d file packets\n", s.FileToTransfer.SentPackets)
s.Stop()
}
fileBytes := make([]byte, protocol.MAXFILEDATASIZE)
// if there is less data to send than the limit - create a buffer of needed size
if s.FileToTransfer.LeftBytes < uint64(protocol.MAXFILEDATASIZE) {
fileBytes = make([]byte, protocol.MAXFILEDATASIZE-(protocol.MAXFILEDATASIZE-int(s.FileToTransfer.LeftBytes)))
}
// reading bytes from the point where we left
read, err := s.FileToTransfer.Handler.ReadAt(fileBytes, int64(s.FileToTransfer.SentBytes))
if err != nil {
return fmt.Errorf("could not read from a file: %s", err)
}
// constructing a file packet and sending it
fileDataPacket := protocol.Packet{
Header: protocol.HeaderFileData,
Filename: s.FileToTransfer.Filename,
Filesize: s.FileToTransfer.Filesize,
FileData: fileBytes,
}
err = protocol.SendPacket(s.Connection, fileDataPacket)
if err != nil {
return fmt.Errorf("could not send a file packet : %s", err)
}
// doing a "logging" for the next time
s.FileToTransfer.LeftBytes -= uint64(read)
s.FileToTransfer.SentBytes += uint64(read)
s.FileToTransfer.SentPackets++
return nil
}
// Listens in an endless loop; reads incoming packages and puts them into channel
func (s *Server) ReceivePackets() {
for {
incomingPacket := protocol.ReadFromConn(s.Connection)
isvalid, _ := protocol.IsValidPacket(incomingPacket)
if !isvalid {
continue
}
s.IncomingPackets <- incomingPacket
}
}
// The "head" of the server. "Glues" all things together.
// Current structure allows the server to receive any type of packet
// in any order and react correspondingly
func (s *Server) MainLoop() {
go s.ReceivePackets()
// send an information about the shared file to the client
s.SendOffer()
for {
if s.Stopped {
// exit the mainloop
break
}
// send, then process received packets
// no incoming packets ? Skipping the packet handling part
if len(s.IncomingPackets) == 0 {
continue
}
incomingPacket := <-s.IncomingPackets
// handling each packet header differently
switch incomingPacket.Header {
case protocol.HeaderAccept:
fmt.Printf("Client has accepted the transfer !\n")
// allowed to send file packets
s.CanTransfer = true
case protocol.HeaderReject:
fmt.Println("Client has rejected the transfer")
s.Stop()
// client is ready to receive the next file packet
case protocol.HeaderReady:
if !s.CanTransfer {
break
}
err := s.SendPiece()
if err != nil {
fmt.Println(err)
}
case protocol.HeaderDisconnecting:
s.Stop()
}
}
}
Loading…
Cancel
Save