From 4c10b2af00635d9009d2020773e491da210c1091 Mon Sep 17 00:00:00 2001
From: Aleksandr Kovalko
Date: Sat, 2 May 2026 02:58:45 +0200
Subject: [PATCH] Prepare v1.0.0: fix build, harden inputs, add e2e + release
workflow
---
.github/workflows/release.yml | 84 ++++++++++++
.github/workflows/tests.yml | 18 ++-
.gitignore | 30 ++++-
Makefile | 41 ++++--
README.md | 171 ++++++++++++++++++------
lib/cxxopts | 2 +-
src/Config.cpp | 6 +-
src/Config.hpp | 2 +
src/Main.cpp | 243 ++++++++++++++++++++++------------
src/Receiver.cpp | 126 ++++++++++++++----
src/Sender.cpp | 174 +++++++++++++++---------
src/Utils.hpp | 60 +++------
tests/e2e.sh | 100 ++++++++++++++
13 files changed, 782 insertions(+), 275 deletions(-)
create mode 100644 .github/workflows/release.yml
create mode 100755 tests/e2e.sh
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..b4040cc
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,84 @@
+name: Release
+
+on:
+ push:
+ tags: ['v*']
+
+jobs:
+ build:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - os: ubuntu-latest
+ artifact_name: FileBroadcaster
+ asset_name: FileBroadcaster-linux-x86_64
+ - os: macos-latest
+ artifact_name: FileBroadcaster
+ asset_name: FileBroadcaster-macos-arm64
+ - os: windows-latest
+ artifact_name: FileBroadcaster.exe
+ asset_name: FileBroadcaster-windows-x86_64.exe
+
+ defaults:
+ run:
+ shell: ${{ matrix.os == 'windows-latest' && 'msys2 {0}' || 'bash' }}
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ submodules: recursive
+
+ - name: Set up MSYS2 (Windows)
+ if: runner.os == 'Windows'
+ uses: msys2/setup-msys2@v2
+ with:
+ msystem: MINGW64
+ install: mingw-w64-x86_64-gcc make
+
+ - name: Build (release)
+ env:
+ VERSION: ${{ github.ref_name }}
+ run: |
+ if [ "$RUNNER_OS" == "Windows" ]; then
+ make program \
+ CXXFLAGS="-std=c++17 -O2 -Wall -Wextra -pthread -DFILEBROADCASTER_VERSION=\\\"${VERSION}\\\"" \
+ LDLIBS_WIN="-lws2_32 -static -static-libgcc -static-libstdc++"
+ mv FileBroadcaster FileBroadcaster.exe
+ else
+ make program \
+ CXXFLAGS="-std=c++17 -O2 -Wall -Wextra -pthread -DFILEBROADCASTER_VERSION=\"\\\"${VERSION}\\\"\""
+ fi
+
+ - name: Rename artifact
+ run: mv ${{ matrix.artifact_name }} ${{ matrix.asset_name }}
+
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ matrix.asset_name }}
+ path: ${{ matrix.asset_name }}
+ if-no-files-found: error
+
+ release:
+ needs: build
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Download all artifacts
+ uses: actions/download-artifact@v4
+ with:
+ path: artifacts
+ merge-multiple: true
+
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v2
+ with:
+ files: artifacts/*
+ generate_release_notes: true
+ draft: false
+ prerelease: ${{ contains(github.ref_name, '-') }}
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 04d3f6c..6e4a32d 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -7,9 +7,10 @@ on:
branches: [master, develop, tests]
jobs:
- gtest:
+ build:
runs-on: ${{ matrix.os }}
strategy:
+ fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
@@ -39,6 +40,14 @@ jobs:
if: runner.os == 'macOS'
run: brew install googletest
+ - name: Build program
+ run: |
+ if [ "$RUNNER_OS" == "Windows" ]; then
+ make program LDLIBS_WIN="-lws2_32"
+ else
+ make program
+ fi
+
- name: Build tests
run: |
if [ "$RUNNER_OS" == "macOS" ]; then
@@ -50,3 +59,10 @@ jobs:
- name: Run tests
run: ./GTests --gtest_filter=*
+
+ - name: Run E2E loopback tests
+ # Skip on Windows: SO_REUSEADDR semantics on Winsock cause one socket
+ # to silently capture all loopback traffic, which breaks two-process
+ # localhost tests. The protocol works fine across separate hosts.
+ if: runner.os != 'Windows'
+ run: make e2e
diff --git a/.gitignore b/.gitignore
index 91ed369..612d39d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,10 +31,36 @@
*.out
*.app
-#Visual studio dirs
+# Project binaries
+/FileBroadcaster
+/GTests
+
+# Debug symbols
+*.dSYM/
+*.pdb
+
+# Build directories
+build/
+out/
+
+# Visual Studio
.vs/
x64/
x86/
Debug/
Release/
-packages/
\ No newline at end of file
+packages/
+
+# IDE / editor metadata
+.idea/
+.vscode/
+*.swp
+*.swo
+
+# Tooling
+compile_commands.json
+.cache/
+
+# OS noise
+.DS_Store
+Thumbs.db
diff --git a/Makefile b/Makefile
index ba1e835..efb20dd 100644
--- a/Makefile
+++ b/Makefile
@@ -1,17 +1,34 @@
-program:
- g++ src/Main.cpp src/Receiver.cpp src/Sender.cpp src/Config.cpp \
- -std=c++14 -pthread \
- -Ilib/cxxopts/include \
- -o FileBroadcaster
+CXX ?= g++
+CXXFLAGS ?= -std=c++17 -O2 -Wall -Wextra -pthread
+INCLUDES = -Ilib/cxxopts/include
+LDLIBS_WIN ?=
+
+SRCS = src/Main.cpp src/Receiver.cpp src/Sender.cpp src/Config.cpp
+TARGET = FileBroadcaster
GTEST_CFLAGS ?=
GTEST_LDFLAGS ?=
+.PHONY: all program gtests e2e clean
+
+all: program
+
+program: $(TARGET)
+
+$(TARGET): $(SRCS)
+ $(CXX) $(SRCS) $(CXXFLAGS) $(INCLUDES) -o $(TARGET) $(LDLIBS_WIN)
+
gtests:
- g++ tests/Tests.cpp \
- -std=c++17 -pthread \
- -Ilib/cxxopts/include \
- $(GTEST_CFLAGS) \
- -lgtest \
- $(GTEST_LDFLAGS) \
- -o GTests
+ $(CXX) tests/Tests.cpp \
+ $(CXXFLAGS) \
+ $(INCLUDES) \
+ $(GTEST_CFLAGS) \
+ -lgtest \
+ $(GTEST_LDFLAGS) \
+ -o GTests
+
+e2e: program
+ BINARY=./$(TARGET) bash tests/e2e.sh
+
+clean:
+ rm -f $(TARGET) $(TARGET).exe GTests GTests.exe
diff --git a/README.md b/README.md
index 314f99d..6b1bc73 100644
--- a/README.md
+++ b/README.md
@@ -13,86 +13,175 @@
-UDP broadcast file transfer — sends a file to all computers in the same LAN simultaneously, with automatic retransmission of lost packets.
+UDP broadcast file transfer — sends a single file to every host on the same
+LAN at once, with automatic retransmission of dropped packets.
## Table of Contents
- [Features](#features)
- [Quick Start](#quick-start)
-- [Requirements](#requirements)
- [Installation](#installation)
- [Parameters](#parameters)
+- [Examples](#examples)
- [How It Works](#how-it-works)
- [Packet Structure](#packet-structure)
+- [Limitations](#limitations)
+- [Building from Source](#building-from-source)
+- [License](#license)
## Features
-- Broadcast to all computers in LAN with a single send
+- Broadcast to every host on a LAN with a single transmission
- Unicast mode for point-to-point transfer
- Automatic retransmission of lost packets
-- Configurable MTU and TTL
-- Windows, Linux, and macOS support
+- Configurable MTU and timeout
+- Windows, Linux, and macOS binaries built on every release
## Quick Start
-**Sender** (machine that has the file):
-```
+Download the binary for your platform from the
+[releases page](https://github.com/gistrec/File-Broadcaster/releases) and run
+it directly. No installation required.
+
+**Sender** (host that has the file):
+
+```sh
./FileBroadcaster --type sender --file photo.jpg
```
-**Receiver** (one or more machines in the same LAN):
-```
+**Receiver** (one or more hosts on the same LAN):
+
+```sh
./FileBroadcaster --type receiver --file photo.jpg
```
-To send to a specific IP instead of broadcasting to the whole LAN:
-```
+Send to a specific host instead of broadcasting to the whole LAN:
+
+```sh
./FileBroadcaster --type sender --file photo.jpg --broadcast 192.168.1.50
```
-## Requirements
+## Installation
-**Windows:**
-- Visual Studio 2019 or later
+Pre-built binaries for Linux x86_64, macOS arm64, and Windows x86_64 are
+attached to every [GitHub Release](https://github.com/gistrec/File-Broadcaster/releases).
-**Linux / macOS:**
-- g++ or clang++ with C++14 support
-- pthreads
+If your platform isn't covered, see [Building from Source](#building-from-source).
-## Installation
+## Parameters
+
+| Parameter | Default | Range | Description |
+| --------- | ------- | ----- | ----------- |
+| `-f, --file` | `file.out` | — | File to send or save |
+| `-t, --type` | `sender` | `sender` / `receiver` | Run mode |
+| `--broadcast` | `yes` | `yes` or IPv4 | `yes` for LAN broadcast, or a specific IP for unicast |
+| `-p, --port` | `33333` | 1..65535 | Destination port for outgoing packets |
+| `--bind-port` | `33333` | 1..65535 | Local port to bind on |
+| `--mtu` | `1500` | 64..65507 | Max packet size in bytes |
+| `--ttl` | `15` | > 0 | Seconds of silence before giving up |
+| `-h, --help` | — | — | Print help |
+| `--version` | — | — | Print version |
-**Linux / macOS:**
+## Examples
+
+**LAN broadcast** (one sender, many receivers):
+
+```sh
+# On the sender host
+./FileBroadcaster --type sender --file album.zip
+
+# On every receiver host
+./FileBroadcaster --type receiver --file album.zip
```
-git clone https://github.com/gistrec/File-Broadcaster.git
-git submodule update --init --recursive
-make program
+
+**Targeted unicast** (when broadcast is blocked or you only have one receiver):
+
+```sh
+# On the sender host (sends data to 10.0.0.42)
+./FileBroadcaster --type sender --file album.zip --broadcast 10.0.0.42
+
+# On 10.0.0.42 (receiver default --broadcast=yes broadcasts RESENDs)
+./FileBroadcaster --type receiver --file album.zip
```
-**Windows:**
-1. Clone the repository and run `git submodule update --init --recursive`
-2. Open `FileBroadcaster.sln` in Visual Studio
-3. Build the project
+**Loopback test** (sender and receiver on the same host — useful for
+development):
-## Parameters
+```sh
+# Receiver listens on 33401, sends RESEND back to the sender's bind port (33402)
+./FileBroadcaster --type receiver --file out.bin \
+ --broadcast 127.0.0.1 --port 33402 --bind-port 33401 &
-| Parameter | Default | Description |
-| --------- | ------- | ----------- |
-| `-p, --port` | `33333` | Port for sender and receiver |
-| `-f, --file` | `file.out` | File to send or save |
-| `-t, --type` | `sender` | `sender` or `receiver` |
-| `--ttl` | `15` | Seconds to wait before timing out |
-| `--mtu` | `1500` | Max packet size in bytes |
-| `--broadcast` | `yes` | `yes` for LAN broadcast, or a specific IP for unicast |
+# Sender listens on 33402, sends data to the receiver's bind port (33401)
+./FileBroadcaster --type sender --file in.bin \
+ --broadcast 127.0.0.1 --port 33401 --bind-port 33402
+```
## How It Works
-1. Sender broadcasts a `NEW_PACKET` with the total file size
-2. Sender splits the file into chunks and broadcasts each one as a `TRANSFER` packet
-3. Sender broadcasts a `FINISH` packet when all chunks are sent
-4. Receiver checks for missing chunks and requests them with `RESEND` packets
-5. Sender retransmits each requested chunk
-6. Steps 4–5 repeat until all chunks are received or TTL expires
+1. Sender broadcasts a `NEW_PACKET` packet with the total file size.
+2. Each receiver allocates a buffer of that size and clears its part registry.
+3. Sender splits the file into MTU-sized chunks and broadcasts each one as a
+ `TRANSFER` packet.
+4. Sender broadcasts a `FINISH` packet when all chunks have been sent.
+5. Each receiver scans for missing chunks and requests them with `RESEND`
+ packets.
+6. Sender retransmits each requested chunk.
+7. Steps 5–6 repeat until every chunk is received or the TTL expires.
## Packet Structure

+
+## Limitations
+
+- The whole file is held in RAM on both sides. The receiver enforces a 4 GiB
+ cap on the announced file size; the sender is bounded only by available
+ memory.
+- No data integrity check beyond UDP's optional 16-bit checksum. If the
+ payload is corrupted in a way the checksum doesn't catch, the receiver will
+ silently produce a corrupted file.
+- No authentication. Any host on the same LAN can send a `NEW_PACKET` and any
+ receiver bound to the chosen port will accept it.
+- No encryption. The payload travels as plaintext UDP.
+
+## Building from Source
+
+### Requirements
+
+- **Linux / macOS:** GCC 7+ or Clang 5+ with C++17 support, GNU Make, pthreads.
+- **Windows (MinGW64):** [MSYS2](https://www.msys2.org/) with
+ `mingw-w64-x86_64-gcc` and `make`.
+- **Windows (Visual Studio):** Visual Studio 2017 or later with the v141
+ platform toolset.
+
+### Linux / macOS / MSYS2
+
+```sh
+git clone https://github.com/gistrec/File-Broadcaster.git
+cd File-Broadcaster
+git submodule update --init --recursive
+make program
+```
+
+The binary is written to `./FileBroadcaster`.
+
+### Windows (Visual Studio)
+
+1. Clone the repository.
+2. Run `git submodule update --init --recursive`.
+3. Open `FileBroadcaster.sln` in Visual Studio.
+4. Build the solution.
+
+### Tests
+
+```sh
+make gtests # unit tests (requires Google Test)
+./GTests
+
+make e2e # loopback end-to-end test (small + large file)
+```
+
+## License
+
+[MIT](LICENSE).
diff --git a/lib/cxxopts b/lib/cxxopts
index 3d405ef..44380e5 160000
--- a/lib/cxxopts
+++ b/lib/cxxopts
@@ -1 +1 @@
-Subproject commit 3d405ef1639a918ea8798666e2b02eb9cef889c0
+Subproject commit 44380e5a44706ab7347f400698c703eb2a196202
diff --git a/src/Config.cpp b/src/Config.cpp
index cb3e973..b8f47a1 100644
--- a/src/Config.cpp
+++ b/src/Config.cpp
@@ -8,9 +8,9 @@ int ttl_max = 0;
SOCKET _socket;
-SOCKADDR_IN server_address = { 0 };
-SOCKADDR_IN client_address = { 0 };
-SOCKADDR_IN broadcast_address = { 0 };
+SOCKADDR_IN server_address = {};
+SOCKADDR_IN client_address = {};
+SOCKADDR_IN broadcast_address = {};
addr_len server_address_length = sizeof(server_address);
addr_len client_address_length = sizeof(client_address);
diff --git a/src/Config.hpp b/src/Config.hpp
index 047f449..06273ed 100644
--- a/src/Config.hpp
+++ b/src/Config.hpp
@@ -3,6 +3,8 @@
#include "Utils.hpp"
+#include
+
extern std::string fileName; // File Name to transfer or receive
diff --git a/src/Main.cpp b/src/Main.cpp
index 5ddb563..dd60f69 100644
--- a/src/Main.cpp
+++ b/src/Main.cpp
@@ -1,10 +1,27 @@
#include "cxxopts.hpp"
#include "Config.hpp"
+#include
+#include
+#include
+
+#ifndef FILEBROADCASTER_VERSION
+#define FILEBROADCASTER_VERSION "1.0.0"
+#endif
+
namespace Receiver { void run(); }
namespace Sender { void run(); }
+static void cleanupAndExit(int code) {
+ if (_socket != INVALID_SOCKET) {
+ CLOSE_SOCKET(_socket);
+ }
+ CLEANUP_NETWORK();
+ std::exit(code);
+}
+
+
int main(int argc, char* argv[]) {
// Parsing input parameters from the CLI
cxxopts::Options options("File-Broadcaster", "UDP Broadcast file transfer");
@@ -14,96 +31,152 @@ int main(int argc, char* argv[]) {
.show_positional_help();
options.add_options()
- ("f,file", "File name", cxxopts::value()->default_value("file.out"))
- ("t,type", "Receiver or sender", cxxopts::value()->default_value("sender"))
- ("broadcast", "Broadcast address", cxxopts::value()->default_value("yes"))
- ("p,port", "Port", cxxopts::value()->default_value("33333"))
- ("mtu", "MTU packet", cxxopts::value()->default_value("1500"))
- ("ttl", "Time to live", cxxopts::value()->default_value("15"));
-
- auto result = options.parse(argc, argv);
-
- #if defined(_WIN32) || defined(_WIN64) //
- WORD socketVer; // Initializing the use
- WSADATA wsaData; // of the Winsock DLL
- socketVer = MAKEWORD(2, 2); // by this process.
- if (WSAStartup(socketVer, &wsaData) != 0) { //
- std::cerr << "Error: WSAStartup failed" << std::endl; //
- exit(1); //
- } //
- #endif //
-
- mtu = result["mtu"].as(); //
- ttl = result["ttl"].as(); // Initializing some variable
- ttl_max = result["ttl"].as(); //
- fileName = result["file"].as(); //
-
-
- _socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); //
- if (_socket == INVALID_SOCKET) { // Create socket with
- std::cout << "Error: Can't create socket" << std::endl; // datagram-based protocol
- exit(1); //
- } else { //
- std::cout << "Ok: Socket created" << std::endl; //
- } //
-
- client_address.sin_family = AF_INET; //
- client_address.sin_port = htons(result["port"].as()); // Creating local address
- client_address.sin_addr.s_addr = INADDR_ANY; //
-
- memcpy(&server_address, &client_address, sizeof(server_address)); // Server address == Client address
-
- broadcast_address.sin_family = AF_INET; // Creating broadcast
- broadcast_address.sin_port = htons(result["port"].as()); // address
-
- if (result["broadcast"].as() == "yes") { //
- #if defined(_WIN32) || defined(_WIN64) // Getting access to
- char broadcastEnable = 1; // the broadcast address
- #else //
- int broadcastEnable = 1; //
- #endif //
- //
- if (setsockopt(_socket, SOL_SOCKET, SO_BROADCAST, //
- &broadcastEnable, sizeof(broadcastEnable)) == 0) { //
- std::cout << "Ok: Got access to broadcast" << std::endl; //
- } else { //
- std::cerr << "Error: Can't get access to broadcast" << std::endl; //
- CLOSE_SOCKET(_socket); //
- exit(1); //
- } // If parameter "broadcast" is "yes", then
- broadcast_address.sin_addr.s_addr = INADDR_BROADCAST; // change server address
- } else { // to broadcast
- broadcast_address.sin_addr.s_addr = // Else change server address
- inet_addr(result["broadcast"].as().c_str()); // to address in parameter
- } //
-
- if (bind(_socket, reinterpret_cast(&client_address), sizeof(client_address)) == 0) {//
- std::cout << "Ok: Socket binded" << std::endl; //
- } else { // Bind socket to
- std::cerr << "Error: Can't bind socket" << std::endl; // client address
- CLOSE_SOCKET(_socket); //
- exit(1); //
- } //
-
- #if defined(_WIN32) || defined(_WIN64) //
- int tv = 1000; // user timeout in milliseconds [ms] //
- setsockopt(_socket, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv)); // Set socket
- #else // receive timeout
- struct timeval tv; // to 1 sec
- tv.tv_sec = 1; //
- tv.tv_usec = 0; //
- setsockopt(_socket, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv)); //
+ ("f,file", "File name", cxxopts::value()->default_value("file.out"))
+ ("t,type", "Receiver or sender", cxxopts::value()->default_value("sender"))
+ ("broadcast", "Broadcast address", cxxopts::value()->default_value("yes"))
+ ("p,port", "Destination port for outgoing packets", cxxopts::value()->default_value("33333"))
+ ("bind-port", "Local port to bind on", cxxopts::value()->default_value("33333"))
+ ("mtu", "MTU packet", cxxopts::value()->default_value("1500"))
+ ("ttl", "Time to live", cxxopts::value()->default_value("15"))
+ ("h,help", "Print help")
+ ("version", "Print version");
+
+ auto result = [&]() -> cxxopts::ParseResult {
+ try {
+ return options.parse(argc, argv);
+ } catch (const cxxopts::exceptions::exception& e) {
+ std::cerr << "Error: " << e.what() << std::endl;
+ std::exit(1);
+ }
+ }();
+
+ if (result.count("help")) {
+ std::cout << options.help() << std::endl;
+ return 0;
+ }
+ if (result.count("version")) {
+ std::cout << "File-Broadcaster " << FILEBROADCASTER_VERSION << std::endl;
+ return 0;
+ }
+
+ // Validate CLI parameters before touching sockets so we can fail fast
+ int parsed_mtu = result["mtu"].as();
+ int parsed_ttl = result["ttl"].as();
+ int parsed_port = result["port"].as();
+ int parsed_bind_port = result["bind-port"].as();
+
+ if (parsed_mtu < 64 || parsed_mtu > 65507) {
+ std::cerr << "Error: --mtu must be between 64 and 65507" << std::endl;
+ return 1;
+ }
+ if (parsed_ttl <= 0) {
+ std::cerr << "Error: --ttl must be greater than 0" << std::endl;
+ return 1;
+ }
+ if (parsed_port <= 0 || parsed_port > 65535) {
+ std::cerr << "Error: --port must be between 1 and 65535" << std::endl;
+ return 1;
+ }
+ if (parsed_bind_port <= 0 || parsed_bind_port > 65535) {
+ std::cerr << "Error: --bind-port must be between 1 and 65535" << std::endl;
+ return 1;
+ }
+
+ const std::string type = result["type"].as();
+ if (type != "sender" && type != "receiver") {
+ std::cerr << "Error: --type must be 'sender' or 'receiver'" << std::endl;
+ return 1;
+ }
+
+ const std::string broadcast_arg = result["broadcast"].as();
+
+ #if defined(_WIN32) || defined(_WIN64)
+ WORD socketVer = MAKEWORD(2, 2);
+ WSADATA wsaData;
+ if (WSAStartup(socketVer, &wsaData) != 0) {
+ std::cerr << "Error: WSAStartup failed" << std::endl;
+ return 1;
+ }
+ #endif
+
+ mtu = parsed_mtu;
+ ttl = parsed_ttl;
+ ttl_max = parsed_ttl;
+ fileName = result["file"].as();
+
+ _socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
+ if (_socket == INVALID_SOCKET) {
+ std::cerr << "Error: Can't create socket" << std::endl;
+ CLEANUP_NETWORK();
+ return 1;
+ }
+ std::cout << "Ok: Socket created" << std::endl;
+
+ int reuseAddr = 1;
+ if (setsockopt(_socket, SOL_SOCKET, SO_REUSEADDR,
+ reinterpret_cast(&reuseAddr), sizeof(reuseAddr)) != 0) {
+ std::cerr << "Warning: Failed to set SO_REUSEADDR" << std::endl;
+ }
+ #ifdef SO_REUSEPORT
+ int reusePort = 1;
+ if (setsockopt(_socket, SOL_SOCKET, SO_REUSEPORT,
+ reinterpret_cast(&reusePort), sizeof(reusePort)) != 0) {
+ std::cerr << "Warning: Failed to set SO_REUSEPORT" << std::endl;
+ }
+ #endif
+
+ client_address.sin_family = AF_INET;
+ client_address.sin_port = htons(static_cast(parsed_bind_port));
+ client_address.sin_addr.s_addr = INADDR_ANY;
+
+ memcpy(&server_address, &client_address, sizeof(server_address));
+
+ broadcast_address.sin_family = AF_INET;
+ broadcast_address.sin_port = htons(static_cast(parsed_port));
+
+ if (broadcast_arg == "yes") {
+ int broadcastEnable = 1;
+ if (setsockopt(_socket, SOL_SOCKET, SO_BROADCAST,
+ reinterpret_cast(&broadcastEnable),
+ sizeof(broadcastEnable)) != 0) {
+ std::cerr << "Error: Can't get access to broadcast" << std::endl;
+ cleanupAndExit(1);
+ }
+ std::cout << "Ok: Got access to broadcast" << std::endl;
+ broadcast_address.sin_addr.s_addr = INADDR_BROADCAST;
+ } else {
+ if (inet_pton(AF_INET, broadcast_arg.c_str(), &broadcast_address.sin_addr) != 1) {
+ std::cerr << "Error: --broadcast must be 'yes' or a valid IPv4 address" << std::endl;
+ cleanupAndExit(1);
+ }
+ }
+
+ if (bind(_socket, reinterpret_cast(&client_address), sizeof(client_address)) != 0) {
+ std::cerr << "Error: Can't bind socket" << std::endl;
+ cleanupAndExit(1);
+ }
+ std::cout << "Ok: Socket bound" << std::endl;
+
+ #if defined(_WIN32) || defined(_WIN64)
+ DWORD tv = 1000; // user timeout in milliseconds [ms]
+ setsockopt(_socket, SOL_SOCKET, SO_RCVTIMEO,
+ reinterpret_cast(&tv), sizeof(tv));
+ #else
+ struct timeval tv;
+ tv.tv_sec = 1;
+ tv.tv_usec = 0;
+ setsockopt(_socket, SOL_SOCKET, SO_RCVTIMEO,
+ reinterpret_cast(&tv), sizeof(tv));
#endif
// Run receiver or sender
- if (result["type"].as() == "receiver") { //
- Receiver::run(); //
- } else if (result["type"].as() == "sender") { // Run receiver or sender
- Sender::run(); // application
- } else { //
- std::cerr << "Error: Type not found" << std::endl; //
+ if (type == "receiver") {
+ Receiver::run();
+ } else {
+ Sender::run();
}
CLOSE_SOCKET(_socket);
+ CLEANUP_NETWORK();
return 0;
}
diff --git a/src/Receiver.cpp b/src/Receiver.cpp
index 2b85a73..1034bf8 100644
--- a/src/Receiver.cpp
+++ b/src/Receiver.cpp
@@ -1,21 +1,37 @@
#include "Utils.hpp"
#include "Config.hpp"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+using namespace std::chrono_literals;
+
namespace Receiver {
+// Hard upper bound on the file size announced by a sender. We allocate the file
+// in RAM, so anything larger than this would either fail or cause OOM. Adjust
+// only if you know the receiver has enough memory.
+constexpr size_t MAX_FILE_LENGTH = 4ULL * 1024 * 1024 * 1024; // 4 GiB
+
/**
* List of received parts
*/
-std::set parts;
+std::set parts;
/**
* Get empty parts
*/
-std::vector getEmptyParts() {
- std::vector result;
- // For each parts
- for (int i = 0; i < (int)((file_length + mtu - 1) / mtu); i++) {
+std::vector getEmptyParts() {
+ std::vector result;
+ size_t total_parts = (file_length + mtu - 1) / static_cast(mtu);
+ for (size_t i = 0; i < total_parts; i++) {
if (parts.find(i) == parts.end()) result.push_back(i);
}
return result;
@@ -26,11 +42,17 @@ std::vector getEmptyParts() {
* Gets empty parts and requests them from the server
*/
void checkParts() {
- char* buffer = new char[2 * mtu];
+ char* buffer = new (std::nothrow) char[2 * mtu];
+ if (!buffer) {
+ std::cerr << "Error: Can't allocate receive buffer" << std::endl;
+ delete[] file;
+ file = nullptr;
+ return;
+ }
- std::vector emptyParts = getEmptyParts();
+ std::vector emptyParts = getEmptyParts();
- while (ttl && emptyParts.size() > 0) {
+ while (ttl && !emptyParts.empty()) {
for (auto index : emptyParts) {
snprintf(buffer, 7, "RESEND");
Utils::writeBytesFromNumber(buffer + 6, index, 4);
@@ -40,7 +62,8 @@ void checkParts() {
std::this_thread::sleep_for(20ms);
}
- SOCKADDR_IN sender_address = { 0 };
+ SOCKADDR_IN sender_address;
+ memset(&sender_address, 0, sizeof(sender_address));
addr_len sender_address_length = sizeof(sender_address);
auto length = recvfrom(_socket, buffer, 2 * mtu, 0,
reinterpret_cast(&sender_address), &sender_address_length);
@@ -53,14 +76,15 @@ void checkParts() {
ttl = ttl_max;
if (strncmp(buffer, "TRANSFER", 8) == 0) {
- int part = Utils::getNumberFromBytes(buffer + 8, 4);
- int size = Utils::getNumberFromBytes(buffer + 12, 4);
- int total_parts = (file_length + mtu - 1) / mtu;
+ size_t part = Utils::getNumberFromBytes(buffer + 8, 4);
+ size_t size = Utils::getNumberFromBytes(buffer + 12, 4);
+ size_t total_parts = (file_length + mtu - 1) / static_cast(mtu);
- if (part < 0 || part >= total_parts || size <= 0 || size > mtu) continue;
+ if (part >= total_parts || size == 0 || size > static_cast(mtu)) continue;
+ if (static_cast(length) < size + 16) continue;
parts.insert(part);
- memcpy(file + part * mtu, buffer + 16, size);
+ memcpy(file + part * static_cast(mtu), buffer + 16, size);
std::cout << "Receive " << part << " part with size " << size << std::endl;
}
@@ -77,24 +101,51 @@ void checkParts() {
}
std::ofstream output(fileName, std::ofstream::binary);
- output.write(file, file_length);
+ if (!output.is_open()) {
+ std::cerr << "Error: Can't open output file " << fileName << std::endl;
+ delete[] file;
+ file = nullptr;
+ return;
+ }
+ output.write(file, static_cast(file_length));
+ if (!output) {
+ std::cerr << "Error: Failed to write output file " << fileName << std::endl;
+ delete[] file;
+ file = nullptr;
+ return;
+ }
+ output.close();
std::cout << "File successfully received" << std::endl;
delete[] file;
file = nullptr;
- CLOSE_SOCKET(_socket);
- exit(0);
}
void run() {
bool finish = false; // Sender finished transferring
- char* buffer = new char[2 * mtu];
+ char* buffer = new (std::nothrow) char[2 * mtu];
+ if (!buffer) {
+ std::cerr << "Error: Can't allocate receive buffer" << std::endl;
+ return;
+ }
- while (auto length = recvfrom(_socket, buffer, 2 * mtu, 0, reinterpret_cast(&server_address), &server_address_length)) {
+ while (auto length = recvfrom(_socket, buffer, 2 * mtu, 0,
+ reinterpret_cast(&server_address),
+ &server_address_length)) {
// Sender is no longer available
if (ttl <= 0) {
+ delete[] buffer;
+ delete[] file;
+ file = nullptr;
+ return;
+ }
+
+ // Got FINISH but never received NEW_PACKET — joined too late or sender
+ // misbehaving. Bail with an error rather than waiting for ttl to drain.
+ if (finish && file == nullptr) {
+ std::cerr << "Error: Received FINISH without NEW_PACKET — joined too late" << std::endl;
delete[] buffer;
return;
}
@@ -113,27 +164,46 @@ void run() {
ttl = ttl_max; // Update ttl
- if (strncmp(buffer, "NEW_PACKET", 10) == 0) {
- file_length = Utils::getNumberFromBytes(buffer + 10, 4); // Read section "file length"
+ if (strncmp(buffer, "NEW_PACKET", 10) == 0 && static_cast(length) >= 14) {
+ size_t announced = Utils::getNumberFromBytes(buffer + 10, 4);
+ if (announced == 0) {
+ std::cerr << "Error: Sender announced empty file" << std::endl;
+ delete[] buffer;
+ return;
+ }
+ if (announced > MAX_FILE_LENGTH) {
+ std::cerr << "Error: Sender announced file size " << announced
+ << " bytes, exceeds limit of " << MAX_FILE_LENGTH << std::endl;
+ delete[] buffer;
+ return;
+ }
+
+ file_length = announced;
delete[] file;
parts.clear();
- file = new char[file_length];
+ file = new (std::nothrow) char[file_length];
+ if (!file) {
+ std::cerr << "Error: Can't allocate " << file_length << " bytes" << std::endl;
+ delete[] buffer;
+ return;
+ }
memset(file, 0, file_length);
std::cout << "Receive information about new file size: " << file_length << std::endl;
std::cout << "Number of parts: " << (file_length + mtu - 1) / mtu << std::endl;
} else if (strncmp(buffer, "TRANSFER", 8) == 0 && file != nullptr) {
- int part = Utils::getNumberFromBytes(buffer + 8, 4); // Read section "index"
- int size = Utils::getNumberFromBytes(buffer + 12, 4); // Read section "size"
- int total_parts = (file_length + mtu - 1) / mtu;
+ size_t part = Utils::getNumberFromBytes(buffer + 8, 4); // Read section "index"
+ size_t size = Utils::getNumberFromBytes(buffer + 12, 4); // Read section "size"
+ size_t total_parts = (file_length + mtu - 1) / static_cast(mtu);
- if (part < 0 || part >= total_parts || size <= 0 || size > mtu) continue;
+ if (part >= total_parts || size == 0 || size > static_cast(mtu)) continue;
+ if (static_cast(length) < size + 16) continue;
parts.insert(part);
std::cout << "Receive " << part << " part with size " << size << std::endl;
- memcpy(file + part * mtu, buffer + 16, size);
+ memcpy(file + part * static_cast(mtu), buffer + 16, size);
} else if (strncmp(buffer, "FINISH", 6) == 0) {
// If receiver didn't receive a finish message
if (!finish) {
@@ -143,6 +213,8 @@ void run() {
}
}
delete[] buffer;
+ delete[] file;
+ file = nullptr;
}
} //namespace Receiver
diff --git a/src/Sender.cpp b/src/Sender.cpp
index 888dec3..f405921 100644
--- a/src/Sender.cpp
+++ b/src/Sender.cpp
@@ -1,118 +1,159 @@
#include "Utils.hpp"
#include "Config.hpp"
+#include
+#include
+#include
+#include
+#include
+#include
+#include