├── .gitattributes ├── res ├── icons │ ├── app.icns │ └── app.ico ├── app.manifest ├── Info.plist.in └── app.rc.in ├── docs ├── img │ ├── banner.png │ ├── screenshot.png │ └── scalability.png ├── dependencies.md ├── building.md ├── testing.md └── CONTRIBUTING.md ├── paper ├── architecture.png ├── paper.bib └── paper.md ├── src ├── module.modulemap ├── gui │ ├── newserver.hpp │ ├── about.hpp │ ├── newconnip.hpp │ ├── newconnbt.hpp │ ├── menu.state.hpp │ ├── newconn.hpp │ ├── menu.hpp │ ├── notifications.hpp │ ├── newserver.cpp │ ├── about.cpp │ ├── imguiext.cpp │ ├── newconnip.cpp │ ├── newconn.cpp │ ├── menu.cpp │ └── newconnbt.cpp ├── utils │ ├── overload.hpp │ ├── handleptr.hpp │ ├── uuids.cpp │ ├── uuids.hpp │ ├── strings.hpp │ ├── strings.cpp │ └── settingsparser.cpp ├── app │ ├── fs.hpp │ ├── config.hpp.in │ ├── appcore.hpp │ ├── settings.hpp │ └── fs.cpp ├── sockets │ ├── clientsockettls.hpp │ ├── delegates │ │ ├── client.hpp │ │ ├── bidirectional.hpp │ │ ├── windows │ │ │ ├── sockethandle.cpp │ │ │ ├── bidirectional.cpp │ │ │ └── client.cpp │ │ ├── linux │ │ │ ├── sockethandle.cpp │ │ │ ├── bidirectional.cpp │ │ │ └── client.cpp │ │ ├── macos │ │ │ ├── sockethandle.cpp │ │ │ ├── bidirectional.cpp │ │ │ ├── client.cpp │ │ │ └── server.cpp │ │ ├── noops.hpp │ │ ├── server.hpp │ │ ├── traits.hpp │ │ ├── sockethandle.hpp │ │ ├── secure │ │ │ ├── clienttls.hpp │ │ │ └── clienttls.cpp │ │ └── delegates.hpp │ ├── serversocket.hpp │ ├── incomingsocket.hpp │ ├── clientsocket.hpp │ └── socket.hpp ├── net │ ├── device.hpp │ ├── btutils.internal.hpp │ ├── enums.hpp │ ├── btutils.hpp │ ├── btutils.macos.cpp │ ├── netutils.hpp │ └── netutils.cpp ├── components │ ├── connwindow.hpp │ ├── ioconsole.hpp │ ├── window.hpp │ ├── windowlist.hpp │ ├── serverwindow.hpp │ ├── sdpwindow.hpp │ ├── connwindow.cpp │ ├── ioconsole.cpp │ └── console.hpp ├── os │ ├── errcheck.hpp │ ├── bluetooth.hpp │ ├── error.hpp │ ├── bluetooth.cpp │ ├── error.cpp │ └── async.linux.cpp └── main.cpp ├── .swiftformat ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature-request.yml │ └── bug-report.yml ├── pull_request_template.md └── workflows │ ├── build-debug.yml │ ├── build-all-os.yml │ ├── clang-format-check.yml │ ├── publish-release.yml │ ├── build-windows.yml │ ├── update-deps-list.yml │ ├── build-linux.yml │ └── build-macos.yml ├── .gitignore ├── .editorconfig ├── tests ├── src │ ├── helpers │ │ ├── testio.hpp │ │ ├── init.cpp │ │ ├── testio.cpp │ │ └── helpers.hpp │ ├── bt.cpp │ ├── cancel.cpp │ ├── ip.cpp │ └── https.cpp ├── settings │ └── readme.md ├── benchmarks │ └── server.cpp └── scripts │ └── server.py ├── xmake ├── swift_settings.lua ├── packages │ ├── r │ │ └── remix-icon │ │ │ └── xmake.lua │ └── n │ │ └── noto-sans-mono │ │ └── xmake.lua ├── scripts │ ├── get_latest_changes.lua │ ├── generate_deps_list.lua │ └── parse_lock_file.lua └── download.lua ├── CITATION.cff ├── swift └── Package.swift └── .clang-format /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | docs/img/** binary 3 | res/icons/** binary 4 | -------------------------------------------------------------------------------- /res/icons/app.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhaleConnect/whaleconnect/HEAD/res/icons/app.icns -------------------------------------------------------------------------------- /res/icons/app.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhaleConnect/whaleconnect/HEAD/res/icons/app.ico -------------------------------------------------------------------------------- /docs/img/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhaleConnect/whaleconnect/HEAD/docs/img/banner.png -------------------------------------------------------------------------------- /docs/img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhaleConnect/whaleconnect/HEAD/docs/img/screenshot.png -------------------------------------------------------------------------------- /paper/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhaleConnect/whaleconnect/HEAD/paper/architecture.png -------------------------------------------------------------------------------- /docs/img/scalability.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhaleConnect/whaleconnect/HEAD/docs/img/scalability.png -------------------------------------------------------------------------------- /src/module.modulemap: -------------------------------------------------------------------------------- 1 | module BluetoothCpp { 2 | header "os/bluetooth.hpp" 3 | export * 4 | } 5 | 6 | module Menu { 7 | header "gui/menu.state.hpp" 8 | export * 9 | } 10 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | # This project uses SwiftFormat for formatting Swift: 2 | # https://github.com/nicklockwood/SwiftFormat 3 | 4 | --maxwidth 120 5 | --indent 4 6 | --indentcase true 7 | --wrapparameters after-first 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Questions 4 | url: https://github.com/WhaleConnect/whaleconnect/discussions 5 | about: Submit questions and open-ended discussions 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Desktop configuration files 2 | desktop.ini 3 | .DS_Store 4 | 5 | # Build and cache 6 | .build/ 7 | .cache/ 8 | .xmake/ 9 | build/ 10 | 11 | # IDE directories 12 | .vscode/ 13 | 14 | # Test settings 15 | tests/settings/settings.ini 16 | 17 | # clangd configuration 18 | .clangd 19 | -------------------------------------------------------------------------------- /src/gui/newserver.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include "components/windowlist.hpp" 7 | 8 | // Renders the "New Server" window. 9 | void drawNewServerWindow(WindowList& servers, bool& open); 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Top-most EditorConfig file 2 | root = true 3 | 4 | # All files 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | indent_size = 4 11 | 12 | # Config, manifest, and documentation files 13 | [*.{manifest,plist,clang-format,ini,yml,md}] 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /src/gui/about.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | // Draws a window containing version/build information. 7 | void drawAboutWindow(bool& open); 8 | 9 | // Draws a window containing useful links. 10 | void drawLinksWindow(bool& open); 11 | -------------------------------------------------------------------------------- /src/gui/newconnip.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include "components/windowlist.hpp" 7 | 8 | // Renders the tab in the "New Connection" window for Internet-based connections. 9 | void drawIPConnectionTab(WindowList& connections); 10 | -------------------------------------------------------------------------------- /src/gui/newconnbt.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include "components/windowlist.hpp" 7 | 8 | // Renders the tab in the "New Connection" window for Bluetooth-based connections. 9 | void drawBTConnectionTab(WindowList& connections, WindowList& sdpWindows); 10 | -------------------------------------------------------------------------------- /src/utils/overload.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | // Applies the overload pattern to std::visit. 7 | template 8 | struct Overload : Ts... { 9 | using Ts::operator()...; 10 | }; 11 | 12 | template 13 | Overload(Ts...) -> Overload; 14 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Before submitting your Pull Request, please complete the following: 2 | 3 | - Read the [contributing guidelines](https://github.com/WhaleConnect/whaleconnect/blob/main/docs/CONTRIBUTING.md). 4 | - Delete this template prior to your submission. 5 | 6 | --- 7 | 8 | Helpful information to include: 9 | 10 | - A brief summary of the changes 11 | - Has this PR been discussed in a separate issue? (Reference the issue number with a # sign to create cross-links) 12 | -------------------------------------------------------------------------------- /tests/src/helpers/testio.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include "net/device.hpp" 7 | #include "sockets/socket.hpp" 8 | 9 | // Performs basic I/O checks on a socket. 10 | void testIO(const Socket& socket, bool useRunLoop = false); 11 | 12 | // Connects a socket, then performs I/O checks. 13 | void testIOClient(const Socket& socket, const Device& device, bool useRunLoop = false); 14 | -------------------------------------------------------------------------------- /src/app/fs.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | namespace fs = std::filesystem; 9 | 10 | namespace AppFS { 11 | // Gets the directory of the executable. In a macOS app bundle, returns the path to the Contents directory. 12 | fs::path getBasePath(); 13 | 14 | // Gets the path to the settings directory. 15 | fs::path getSettingsPath(); 16 | } 17 | -------------------------------------------------------------------------------- /src/gui/menu.state.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | namespace Menu { 7 | inline bool settingsOpen = false; 8 | inline bool newConnectionOpen = true; 9 | inline bool newServerOpen = false; 10 | inline bool notificationsOpen = false; 11 | inline bool aboutOpen = false; 12 | inline bool linksOpen = false; 13 | 14 | void setWindowFocus(const char* title); 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/build-debug.yml: -------------------------------------------------------------------------------- 1 | name: Debug build 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'docs/**' 7 | - '.github/ISSUE_TEMPLATE/**' 8 | - '.github/pull_request_template.md' 9 | - '.github/workflows/clang-format-check.yml' 10 | - '.github/workflows/publish-release.yml' 11 | - '.github/workflows/update-deps-list.yml' 12 | branches: 13 | - '**' 14 | jobs: 15 | build: 16 | uses: ./.github/workflows/build-all-os.yml 17 | with: 18 | mode: debug 19 | -------------------------------------------------------------------------------- /res/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | PerMonitorV2 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/sockets/clientsockettls.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include "socket.hpp" 7 | #include "delegates/noops.hpp" 8 | #include "delegates/secure/clienttls.hpp" 9 | 10 | // An outgoing connection secured by TLS. 11 | class ClientSocketTLS : public Socket { 12 | Delegates::ClientTLS client; 13 | Delegates::NoopServer server; 14 | 15 | public: 16 | ClientSocketTLS() : Socket(client, client, client, server) {} 17 | }; 18 | -------------------------------------------------------------------------------- /src/app/config.hpp.in: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | // This file defines build-time configuration through xmake. 5 | 6 | #pragma once 7 | 8 | namespace Config { 9 | constexpr auto version = "${VERSION}"; 10 | constexpr auto versionBuild = "${VERSION_BUILD}"; 11 | constexpr auto gitCommitLong = "${GIT_COMMIT_LONG}"; 12 | constexpr auto plat = "${plat}"; 13 | constexpr auto arch = "${arch}"; 14 | constexpr auto debug = ${DEBUG}; 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/build-all-os.yml: -------------------------------------------------------------------------------- 1 | name: All OS build 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | mode: 7 | type: string 8 | required: true 9 | 10 | jobs: 11 | build-linux: 12 | uses: ./.github/workflows/build-linux.yml 13 | with: 14 | mode: ${{ inputs.mode }} 15 | 16 | build-macos: 17 | uses: ./.github/workflows/build-macos.yml 18 | with: 19 | mode: ${{ inputs.mode }} 20 | 21 | build-windows: 22 | uses: ./.github/workflows/build-windows.yml 23 | with: 24 | mode: ${{ inputs.mode }} 25 | -------------------------------------------------------------------------------- /tests/settings/readme.md: -------------------------------------------------------------------------------- 1 | # Test Settings Directory 2 | 3 | This directory should contain the settings file for WhaleConnect's tests, which is excluded from version control. 4 | 5 | ## File Format 6 | 7 | Create a file called `settings.ini` with the following structure: 8 | 9 | ```ini 10 | [ip] 11 | v4 = [IPv4 address of your server] 12 | v6 = [IPv6 address of your server] 13 | 14 | ; Ports to use for TCP and UDP testing 15 | tcpPort = 3000 16 | udpPort = 3001 17 | 18 | [bluetooth] 19 | mac = [MAC address of your server] 20 | 21 | rfcommPort = 1 22 | l2capPSM = 12345 23 | ``` 24 | -------------------------------------------------------------------------------- /src/gui/newconn.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | #include "components/windowlist.hpp" 9 | #include "net/device.hpp" 10 | 11 | // Adds a ConnWindow to a window list and handles errors during socket creation. 12 | void addConnWindow(WindowList& list, bool useTLS, const Device& device, std::string_view extraInfo); 13 | 14 | // Draws the new connection window. 15 | void drawNewConnectionWindow(bool& open, WindowList& connections, WindowList& sdpWindows); 16 | -------------------------------------------------------------------------------- /src/net/device.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | 9 | #include "enums.hpp" 10 | 11 | // Remote device metadata. 12 | struct Device { 13 | ConnectionType type = ConnectionType::None; // Connection protocol 14 | std::string name; // Device name for display 15 | std::string address; // Address (IP address for TCP / UDP, MAC address for Bluetooth) 16 | std::uint16_t port = 0; // Port (or PSM for L2CAP, channel for RFCOMM) 17 | }; 18 | -------------------------------------------------------------------------------- /xmake/swift_settings.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | -- SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | import("core.project.config") 5 | 6 | function getSwiftBuildMode() 7 | return config.mode() 8 | end 9 | 10 | function getSwiftSettings() 11 | local buildDir = format("$(buildir)/swift/%s", getSwiftBuildMode()) 12 | local libDir = path.join(config.get("xcode"), "Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift") 13 | local linkDir = path.join(libDir, "macosx") 14 | 15 | return buildDir, libDir, linkDir 16 | end 17 | -------------------------------------------------------------------------------- /src/sockets/delegates/client.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include "delegates.hpp" 7 | #include "sockethandle.hpp" 8 | #include "net/device.hpp" 9 | #include "utils/task.hpp" 10 | 11 | namespace Delegates { 12 | // Manages operations on client sockets. 13 | template 14 | class Client : public ClientDelegate { 15 | SocketHandle& handle; 16 | 17 | public: 18 | explicit Client(SocketHandle& handle) : handle(handle) {} 19 | 20 | Task<> connect(Device device) override; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/gui/menu.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | #include "components/windowlist.hpp" 9 | 10 | namespace Menu { 11 | // Draws the main menu bar. 12 | void drawMenuBar(bool& quit, WindowList& connections, WindowList& servers); 13 | 14 | void setupMenuBar(); 15 | 16 | void addWindowMenuItem(std::string_view name); 17 | 18 | void removeWindowMenuItem(std::string_view name); 19 | 20 | void addServerMenuItem(std::string_view name); 21 | 22 | void removeServerMenuItem(std::string_view name); 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/handleptr.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | // Provides operator() for function pointers. 9 | template 10 | struct DeleterAdapter { 11 | void operator()(T* p) const { 12 | Fn(p); 13 | } 14 | }; 15 | 16 | // Type alias to manage system handles with RAII. 17 | // T: the type of the handle to manage (pointer removed) 18 | // Fn: the address of the function to free the handle, taking a parameter of T* 19 | template 20 | using HandlePtr = std::unique_ptr>; 21 | -------------------------------------------------------------------------------- /src/sockets/serversocket.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include "socket.hpp" 7 | #include "delegates/noops.hpp" 8 | #include "delegates/server.hpp" 9 | #include "delegates/sockethandle.hpp" 10 | 11 | // A server that accepts incoming connections. 12 | template 13 | class ServerSocket : public Socket { 14 | Delegates::SocketHandle handle; 15 | Delegates::NoopIO io; 16 | Delegates::NoopClient client; 17 | Delegates::Server server{ handle }; 18 | 19 | public: 20 | ServerSocket() : Socket(handle, io, client, server) {} 21 | }; 22 | -------------------------------------------------------------------------------- /tests/src/helpers/init.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include 5 | #include 6 | 7 | #include "os/async.hpp" 8 | 9 | // Listener to initialize OS APIs when tests are run. 10 | struct InitListener : Catch::EventListenerBase { 11 | using Catch::EventListenerBase::EventListenerBase; 12 | 13 | void testRunStarting(const Catch::TestRunInfo&) override { 14 | Async::init(1, 128); 15 | } 16 | 17 | void testRunEnded(const Catch::TestRunStats&) override { 18 | Async::cleanup(); 19 | } 20 | }; 21 | 22 | CATCH_REGISTER_LISTENER(InitListener) 23 | -------------------------------------------------------------------------------- /src/sockets/delegates/bidirectional.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | 9 | #include "delegates.hpp" 10 | #include "sockethandle.hpp" 11 | #include "utils/task.hpp" 12 | 13 | namespace Delegates { 14 | // Manages bidirectional communication on a socket. 15 | template 16 | class Bidirectional : public IODelegate { 17 | SocketHandle& handle; 18 | 19 | public: 20 | explicit Bidirectional(SocketHandle& handle) : handle(handle) {} 21 | 22 | Task<> send(std::string data) override; 23 | 24 | Task recv(std::size_t size) override; 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /xmake/packages/r/remix-icon/xmake.lua: -------------------------------------------------------------------------------- 1 | package("remix-icon") 2 | set_kind("library", { headeronly = true }) 3 | set_homepage("https://github.com/Remix-Design/RemixIcon") 4 | set_description("Remix Icon font") 5 | set_license("Apache-2.0") 6 | 7 | add_urls("https://github.com/Remix-Design/RemixIcon/archive/refs/tags/v$(version).tar.gz") 8 | add_versions("4.3.0", "5bdfaf4863ca75fca22cb728a13741116ce7d2fc0b5b9a6d7261eb2453bc137d") 9 | 10 | on_install(function (package) 11 | os.cp("fonts/remixicon.ttf", package:installdir()) 12 | package:addenv("REMIX_ICON_PATH", path.join(package:installdir(), "remixicon.ttf")) 13 | end) 14 | 15 | on_test(function (package) 16 | assert(os.isfile(package:getenv("REMIX_ICON_PATH")[1])) 17 | end) 18 | -------------------------------------------------------------------------------- /src/app/appcore.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | namespace AppCore { 7 | // Re-applies Dear ImGui configuration before the next frame according to app settings. 8 | void configOnNextFrame(); 9 | 10 | // Sets up backends/context, configures Dear ImGui, and creates a main application window. 11 | bool init(); 12 | 13 | // Checks if the main window should be closed and creates a new frame at the start of every loop iteration. 14 | bool newFrame(); 15 | 16 | // Handles the rendering of the window at the end of every loop iteration. 17 | void render(); 18 | 19 | // Cleans up all backends and destroys the main window. 20 | void cleanup(); 21 | } 22 | -------------------------------------------------------------------------------- /src/net/btutils.internal.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | #include "btutils.hpp" 9 | 10 | namespace BTUtils::Internal { 11 | // Gets the major and minor version numbers from a profile descriptor. 12 | inline void extractVersionNums(std::uint16_t version, BTUtils::ProfileDesc& desc) { 13 | // Bit operations to extract the two octets 14 | // The major and minor version numbers are stored in the high-order 8 bits and low-order 8 bits of the 16-bit 15 | // version number, respectively. This is the same for both Windows and Linux. 16 | desc.versionMajor = version >> 8; 17 | desc.versionMinor = version & 0xFF; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/sockets/incomingsocket.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | #include "socket.hpp" 9 | #include "delegates/bidirectional.hpp" 10 | #include "delegates/noops.hpp" 11 | #include "delegates/sockethandle.hpp" 12 | 13 | // An incoming connection (one accepted from a server). 14 | template 15 | class IncomingSocket : public Socket { 16 | Delegates::SocketHandle handle; 17 | Delegates::Bidirectional io{ handle }; 18 | Delegates::NoopClient client; 19 | Delegates::NoopServer server; 20 | 21 | public: 22 | explicit IncomingSocket(Delegates::SocketHandle&& handle) : 23 | Socket(this->handle, io, client, server), handle(std::move(handle)) {} 24 | }; 25 | -------------------------------------------------------------------------------- /src/gui/notifications.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | // Icons to display in notifications. 9 | enum class NotificationType { Error, Warning, Info, Success }; 10 | 11 | namespace ImGuiExt { 12 | // Adds a notification with text, icon, and an optional automatic close timeout. 13 | void addNotification(std::string_view s, NotificationType type, float timeout = 10); 14 | 15 | // Draws the notifications in the bottom-right corner of the window. 16 | void drawNotifications(); 17 | 18 | // Draws a window containing the notifications. 19 | void drawNotificationsWindow(bool& open); 20 | 21 | // Draws a menu containing the notifications. 22 | void drawNotificationsMenu(bool& notificationsOpen); 23 | } 24 | -------------------------------------------------------------------------------- /src/sockets/clientsocket.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include "socket.hpp" 7 | #include "delegates/bidirectional.hpp" 8 | #include "delegates/client.hpp" 9 | #include "delegates/noops.hpp" 10 | #include "delegates/sockethandle.hpp" 11 | #include "net/enums.hpp" 12 | 13 | // An outgoing connection. 14 | template 15 | class ClientSocket : public Socket { 16 | Delegates::SocketHandle handle; 17 | Delegates::Bidirectional io{ handle }; 18 | Delegates::Client client{ handle }; 19 | Delegates::NoopServer server; 20 | 21 | public: 22 | ClientSocket() : Socket(handle, io, client, server) {} 23 | }; 24 | 25 | using ClientSocketIP = ClientSocket; 26 | using ClientSocketBT = ClientSocket; 27 | -------------------------------------------------------------------------------- /xmake/scripts/get_latest_changes.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | -- SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | -- This script is used in the CI to publish releases 5 | 6 | local baseText = [[These notes are also available on the [changelog](https://github.com/WhaleConnect/whaleconnect/blob/main/docs/changelog.md). 7 | 8 | All executables are unsigned, so you may see warnings from your operating system if you run them. 9 | 10 | ]] 11 | 12 | function main(...) 13 | local changelogPath = path.join(os.projectdir(), "docs", "changelog.md") 14 | local changelog = io.readfile(changelogPath) 15 | 16 | local latestChanges = changelog:match("\n## .-\n\n(.-)\n\n## ") 17 | if latestChanges then 18 | io.writefile("changelog-latest.txt", baseText .. latestChanges) 19 | else 20 | os.exit(1) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /xmake/packages/n/noto-sans-mono/xmake.lua: -------------------------------------------------------------------------------- 1 | package("noto-sans-mono") 2 | set_kind("library", { headeronly = true }) 3 | set_homepage("https://github.com/notofonts/notofonts.github.io") 4 | set_description("Noto Sans Mono font") 5 | set_license("OFL-1.1") 6 | 7 | add_urls("https://github.com/notofonts/latin-greek-cyrillic/releases/download/NotoSansMono-v$(version)/NotoSansMono-v$(version).zip") 8 | add_versions("2.014", "090cf6c5e03f337a755630ca888b1fef463e64ae7b33ee134e9309c05f978732") 9 | 10 | on_install(function (package) 11 | os.cp("NotoSansMono/unhinted/ttf/NotoSansMono-Regular.ttf", package:installdir()) 12 | package:addenv("NOTO_SANS_MONO_PATH", path.join(package:installdir(), "NotoSansMono-Regular.ttf")) 13 | end) 14 | 15 | on_test(function (package) 16 | assert(os.isfile(package:getenv("NOTO_SANS_MONO_PATH")[1])) 17 | end) 18 | -------------------------------------------------------------------------------- /src/utils/uuids.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "uuids.hpp" 5 | 6 | #include 7 | 8 | UUIDs::UUID128 UUIDs::fromSegments(std::uint32_t d1, std::uint16_t d2, std::uint16_t d3, std::uint64_t d4) { 9 | UUIDs::UUID128 ret; 10 | 11 | // Input fields have a system-dependent endianness, while bytes in a UUID128 are ordered based on network byte 12 | // ordering 13 | d1 = byteSwap(d1); 14 | d2 = byteSwap(d2); 15 | d3 = byteSwap(d3); 16 | d4 = byteSwap(d4); 17 | 18 | // Copy data into return object, the destination pointer is incremented by the sum of the previous sizes 19 | std::memcpy(ret.data(), &d1, 4); 20 | std::memcpy(ret.data() + 4, &d2, 2); 21 | std::memcpy(ret.data() + 6, &d3, 2); 22 | std::memcpy(ret.data() + 8, &d4, 8); 23 | return ret; 24 | } 25 | -------------------------------------------------------------------------------- /src/sockets/delegates/windows/sockethandle.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include "sockets/delegates/sockethandle.hpp" 7 | 8 | #include "os/async.hpp" 9 | 10 | template 11 | void Delegates::SocketHandle::closeImpl() { 12 | Async::submit(Async::Shutdown{ { **this, nullptr } }); 13 | Async::submit(Async::Close{ { **this, nullptr } }); 14 | } 15 | 16 | template 17 | void Delegates::SocketHandle::cancelIO() { 18 | Async::submit(Async::Cancel{ { **this, nullptr } }); 19 | } 20 | 21 | template void Delegates::SocketHandle::closeImpl(); 22 | template void Delegates::SocketHandle::cancelIO(); 23 | 24 | template void Delegates::SocketHandle::closeImpl(); 25 | template void Delegates::SocketHandle::cancelIO(); 26 | -------------------------------------------------------------------------------- /src/sockets/delegates/linux/sockethandle.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "sockets/delegates/sockethandle.hpp" 5 | 6 | #include "net/enums.hpp" 7 | #include "os/async.hpp" 8 | 9 | template 10 | void Delegates::SocketHandle::closeImpl() { 11 | Async::submit(Async::Shutdown{ { **this, nullptr } }); 12 | Async::submit(Async::Close{ { **this, nullptr } }); 13 | } 14 | 15 | template 16 | void Delegates::SocketHandle::cancelIO() { 17 | Async::submit(Async::Cancel{ { **this, nullptr } }); 18 | } 19 | 20 | template void Delegates::SocketHandle::closeImpl(); 21 | template void Delegates::SocketHandle::cancelIO(); 22 | 23 | template void Delegates::SocketHandle::closeImpl(); 24 | template void Delegates::SocketHandle::cancelIO(); 25 | -------------------------------------------------------------------------------- /src/sockets/delegates/macos/sockethandle.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "sockets/delegates/sockethandle.hpp" 5 | 6 | #include "net/enums.hpp" 7 | #include "os/async.hpp" 8 | #include "os/bluetooth.hpp" 9 | 10 | template <> 11 | void Delegates::SocketHandle::closeImpl() { 12 | Async::submit(Async::Shutdown{ { **this, nullptr } }); 13 | Async::submit(Async::Close{ { **this, nullptr } }); 14 | } 15 | 16 | template <> 17 | void Delegates::SocketHandle::cancelIO() { 18 | Async::submit(Async::Cancel{ { **this, nullptr } }); 19 | } 20 | 21 | template <> 22 | void Delegates::SocketHandle::closeImpl() { 23 | handle->close(); 24 | } 25 | 26 | template <> 27 | void Delegates::SocketHandle::cancelIO() { 28 | AsyncBT::cancel(handle->getHash()); 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/clang-format-check.yml: -------------------------------------------------------------------------------- 1 | name: clang-format Check 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'docs/**' 7 | - '.github/ISSUE_TEMPLATE/**' 8 | - '.github/pull_request_template.md' 9 | - '.github/workflows/build-debug.yml' 10 | - '.github/workflows/publish-release.yml' 11 | - '.github/workflows/update-deps-list.yml' 12 | pull_request: 13 | paths-ignore: 14 | - 'docs/**' 15 | 16 | jobs: 17 | clang-format-check: 18 | name: clang-format Check 19 | runs-on: ubuntu-latest 20 | strategy: 21 | matrix: 22 | path: 23 | - './src' 24 | - './tests' 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v4 29 | 30 | - name: clang-format Check 31 | uses: DoozyX/clang-format-lint-action@v0.17 32 | with: 33 | source: ${{ matrix.path }} 34 | clangFormatVersion: 16 35 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: "1.2.0" 2 | authors: 3 | - family-names: Sun 4 | given-names: Aidan 5 | orcid: "https://orcid.org/0009-0000-6282-2497" 6 | doi: 10.5281/zenodo.13328366 7 | message: If you use this software, please cite our article in the 8 | Journal of Open Source Software. 9 | preferred-citation: 10 | authors: 11 | - family-names: Sun 12 | given-names: Aidan 13 | orcid: "https://orcid.org/0009-0000-6282-2497" 14 | date-published: 2024-08-27 15 | doi: 10.21105/joss.06964 16 | issn: 2475-9066 17 | issue: 100 18 | journal: Journal of Open Source Software 19 | publisher: 20 | name: Open Journals 21 | start: 6964 22 | title: "WhaleConnect: A General-Purpose, Cross-Platform Network 23 | Communication Application" 24 | type: article 25 | url: "https://joss.theoj.org/papers/10.21105/joss.06964" 26 | volume: 9 27 | title: "WhaleConnect: A General-Purpose, Cross-Platform Network 28 | Communication Application" 29 | -------------------------------------------------------------------------------- /src/app/settings.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | #include "utils/uuids.hpp" 12 | 13 | namespace Settings { 14 | // Variables that can be configured through settings 15 | 16 | namespace Font { 17 | inline std::string file; 18 | inline std::vector ranges; 19 | inline std::uint8_t size; 20 | } 21 | 22 | namespace GUI { 23 | inline bool roundedCorners; 24 | inline bool windowTransparency; 25 | inline bool systemMenu; 26 | } 27 | 28 | namespace OS { 29 | inline std::uint8_t numThreads; 30 | inline std::uint8_t queueEntries; 31 | inline std::vector> bluetoothUUIDs; 32 | } 33 | 34 | // Loads the application settings. 35 | void load(); 36 | 37 | // Saves the application settings. 38 | void save(); 39 | 40 | // Renders the settings UI. 41 | void drawSettingsWindow(bool& open); 42 | } 43 | -------------------------------------------------------------------------------- /tests/src/bt.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include 5 | 6 | #include 7 | 8 | #include "helpers/testio.hpp" 9 | #include "net/enums.hpp" 10 | #include "sockets/clientsocket.hpp" 11 | #include "utils/settingsparser.hpp" 12 | 13 | TEST_CASE("I/O (Bluetooth)") { 14 | SettingsParser parser; 15 | parser.load(SETTINGS_FILE); 16 | 17 | const auto mac = parser.get("bluetooth", "mac"); 18 | const auto rfcommPort = parser.get("bluetooth", "rfcommPort"); 19 | 20 | // L2CAP sockets are not supported on Windows 21 | #if !OS_WINDOWS 22 | const auto l2capPSM = parser.get("bluetooth", "l2capPSM"); 23 | #endif 24 | 25 | using enum ConnectionType; 26 | 27 | SECTION("RFCOMM") { 28 | ClientSocketBT s; 29 | testIOClient(s, { RFCOMM, "", mac, rfcommPort }, true); 30 | } 31 | 32 | #if !OS_WINDOWS 33 | SECTION("L2CAP") { 34 | ClientSocketBT s; 35 | testIOClient(s, { L2CAP, "", mac, l2capPSM }, true); 36 | } 37 | #endif 38 | } 39 | -------------------------------------------------------------------------------- /swift/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "WhaleConnect", 6 | platforms: [ 7 | .macOS(.v14), 8 | ], 9 | products: [ 10 | .library(name: "BluetoothMacOS", type: .static, targets: ["BluetoothMacOS"]), 11 | .library(name: "GUIMacOS", type: .static, targets: ["GUIMacOS"]), 12 | ], 13 | targets: [ 14 | .target( 15 | name: "BluetoothMacOS", 16 | dependencies: [], 17 | path: "./Sources/bluetooth", 18 | cxxSettings: [ 19 | .define("NO_EXPOSE_INTERNAL"), 20 | ], 21 | swiftSettings: [ 22 | .interoperabilityMode(.Cxx), 23 | .unsafeFlags(["-I", "../src"]), 24 | ] 25 | ), 26 | .target( 27 | name: "GUIMacOS", 28 | dependencies: [], 29 | path: "./Sources/gui", 30 | swiftSettings: [ 31 | .interoperabilityMode(.Cxx), 32 | .unsafeFlags(["-I", "../src"]), 33 | ] 34 | ), 35 | ], 36 | cxxLanguageStandard: .cxx2b 37 | ) 38 | -------------------------------------------------------------------------------- /src/components/connwindow.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | 9 | #include "ioconsole.hpp" 10 | #include "window.hpp" 11 | #include "net/device.hpp" 12 | #include "sockets/delegates/delegates.hpp" 13 | #include "utils/task.hpp" 14 | 15 | // Handles a socket connection in a GUI window. 16 | class ConnWindow : public Window { 17 | SocketPtr socket; // Internal socket 18 | IOConsole console; 19 | bool connected = false; 20 | bool pendingRecv = false; 21 | 22 | // Connects to the server. 23 | Task<> connect(Device device); 24 | 25 | // Sends a string through the socket. 26 | Task<> sendHandler(std::string s); 27 | 28 | // Receives a string from the socket and displays it in the console output. 29 | Task<> readHandler(); 30 | 31 | // Handles incoming I/O. 32 | void onBeforeUpdate() override; 33 | 34 | void onUpdate() override; 35 | 36 | public: 37 | ConnWindow(std::string_view title, bool useTLS, const Device& device, std::string_view); 38 | 39 | ~ConnWindow() override; 40 | }; 41 | -------------------------------------------------------------------------------- /tests/src/helpers/testio.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include 5 | 6 | #include "helpers.hpp" 7 | #include "net/device.hpp" 8 | #include "sockets/socket.hpp" 9 | #include "utils/task.hpp" 10 | 11 | void testIO(const Socket& socket, bool useRunLoop) { 12 | // Check the socket is valid 13 | REQUIRE(socket.isValid()); 14 | 15 | // Send/receive 16 | runSync( 17 | [&socket]() -> Task<> { 18 | constexpr const char* echoString = "echo test"; 19 | 20 | co_await socket.send(echoString); 21 | 22 | // Receive data and check if the string matches 23 | // The co_await is outside the CHECK() macro to prevent it from being expanded and evaluated multiple times. 24 | auto recvResult = co_await socket.recv(1024); 25 | CHECK(recvResult.data == echoString); 26 | }, 27 | useRunLoop); 28 | } 29 | 30 | void testIOClient(const Socket& socket, const Device& device, bool useRunLoop) { 31 | runSync([&socket, &device]() -> Task<> { co_await socket.connect(device); }, useRunLoop); 32 | 33 | testIO(socket, useRunLoop); 34 | } 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest a new feature. 3 | labels: ['enhancement'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | We encourage using GitHub as a centralized place for community engagement. Please complete the following before submitting your issue: 9 | 10 | - Read the [contributing guidelines](https://github.com/WhaleConnect/whaleconnect/blob/main/docs/CONTRIBUTING.md). 11 | - Search for [past issues](https://github.com/WhaleConnect/whaleconnect/issues?q=) to check for similar discussions. 12 | 13 | Thank you for taking the time to complete these steps and fill out this issue form. 14 | 15 | - type: textarea 16 | attributes: 17 | label: Motivation 18 | description: A description of how this feature would be useful and what problems it solves. 19 | validations: 20 | required: true 21 | 22 | - type: textarea 23 | attributes: 24 | label: Feature Proposal 25 | description: A description of the feature and how it works. Include designs or expected behavior if necessary. 26 | validations: 27 | required: true 28 | 29 | - type: textarea 30 | attributes: 31 | label: Possible Alternatives 32 | description: Anything you have already tried and how current solutions may not be viable. 33 | -------------------------------------------------------------------------------- /res/Info.plist.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSHighResolutionCapable 6 | 7 | CFBundleIconFile 8 | app 9 | CFBundleIdentifier 10 | com.aidansun.whaleconnect 11 | CFBundleName 12 | WhaleConnect 13 | CFBundleDisplayName 14 | WhaleConnect 15 | CFBundleVersion 16 | ${VERSION} 17 | CFBundleShortVersionString 18 | ${VERSION} 19 | CFBundleExecutable 20 | WhaleConnect 21 | CFBundlePackageType 22 | APPL 23 | LSMinimumSystemVersion 24 | 14.0.0 25 | NSBluetoothAlwaysUsageDescription 26 | This app contains functions that require access to Bluetooth. 27 | NSHumanReadableCopyright 28 | Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 29 | CFBundleSupportedPlatforms 30 | 31 | MacOSX 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/net/enums.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | // Enumeration to determine socket types at compile time. 7 | enum class SocketTag { IP, BT }; 8 | 9 | // All possible connection types. 10 | // L2CAP connections are not supported on Windows because of limitations with the Microsoft Bluetooth stack. 11 | enum class ConnectionType { None, TCP, UDP, L2CAP, RFCOMM }; 12 | 13 | // IP versions. 14 | enum class IPType { None, IPv4, IPv6 }; 15 | 16 | inline const char* getConnectionTypeName(ConnectionType type) { 17 | using enum ConnectionType; 18 | switch (type) { 19 | case TCP: 20 | return "TCP"; 21 | case UDP: 22 | return "UDP"; 23 | case L2CAP: 24 | return "L2CAP"; 25 | case RFCOMM: 26 | return "RFCOMM"; 27 | case None: 28 | return "None"; 29 | default: 30 | return "Unknown connection type"; 31 | } 32 | } 33 | 34 | inline const char* getIPTypeName(IPType type) { 35 | using enum IPType; 36 | switch (type) { 37 | case None: 38 | return "None"; 39 | case IPv4: 40 | return "IPv4"; 41 | case IPv6: 42 | return "IPv6"; 43 | default: 44 | return "Unknown IP type"; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /res/app.rc.in: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "winuser.h" 5 | 6 | 1 RT_MANIFEST "${RESDIR}/app.manifest" 7 | 8 | IDI_APPICON ICON "${RESDIR}/icons/app.ico" 9 | GLFW_ICON ICON DISCARDABLE "${RESDIR}/icons/app.ico" 10 | 11 | 1 VERSIONINFO 12 | FILEVERSION ${VERSION_MAJOR},${VERSION_MINOR},${VERSION_ALTER},0 13 | PRODUCTVERSION ${VERSION_MAJOR},${VERSION_MINOR},${VERSION_ALTER},0 14 | FILEFLAGSMASK 0x3fL 15 | #if ${DEBUG} 16 | FILEFLAGS 0x1L 17 | #else 18 | FILEFLAGS 0x0L 19 | #endif 20 | FILEOS 0x4L 21 | FILETYPE 0x1L 22 | FILESUBTYPE 0x0L 23 | BEGIN 24 | BLOCK "StringFileInfo" 25 | BEGIN 26 | BLOCK "040904b0" 27 | BEGIN 28 | VALUE "CompanyName", "WhaleConnect" 29 | VALUE "FileDescription", "Cross-platform network communication software" 30 | VALUE "FileVersion", "${VERSION}" 31 | VALUE "InternalName", "WhaleConnect.exe" 32 | VALUE "LegalCopyright", "Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors" 33 | VALUE "OriginalFilename", "WhaleConnect.exe" 34 | VALUE "ProductName", "WhaleConnect" 35 | VALUE "ProductVersion", "${VERSION}" 36 | END 37 | END 38 | BLOCK "VarFileInfo" 39 | BEGIN 40 | VALUE "Translation", 0x0409, 0x04B0 41 | END 42 | END 43 | -------------------------------------------------------------------------------- /src/sockets/delegates/noops.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | #include "delegates.hpp" 9 | #include "net/device.hpp" 10 | #include "sockets/socket.hpp" // IWYU pragma: keep 11 | #include "utils/task.hpp" 12 | 13 | namespace Delegates { 14 | // Provides no-ops for I/O operations. 15 | struct NoopIO : IODelegate { 16 | Task<> send(std::string) override { 17 | co_return; 18 | } 19 | 20 | Task recv(std::size_t) override { 21 | co_return {}; 22 | } 23 | }; 24 | 25 | // Provides no-ops for client operations. 26 | struct NoopClient : ClientDelegate { 27 | Task<> connect(Device) override { 28 | co_return; 29 | } 30 | }; 31 | 32 | // Provides no-ops for server operations. 33 | struct NoopServer : ServerDelegate { 34 | ServerAddress startServer(const Device&) override { 35 | return {}; 36 | } 37 | 38 | Task accept() override { 39 | co_return {}; 40 | } 41 | 42 | Task recvFrom(std::size_t) override { 43 | co_return {}; 44 | } 45 | 46 | Task<> sendTo(Device, std::string) override { 47 | co_return; 48 | } 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /tests/src/cancel.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include 5 | 6 | #include 7 | 8 | #include "helpers/helpers.hpp" 9 | #include "net/enums.hpp" 10 | #include "os/async.hpp" 11 | #include "os/error.hpp" 12 | #include "sockets/clientsocket.hpp" 13 | #include "utils/settingsparser.hpp" 14 | #include "utils/task.hpp" 15 | 16 | TEST_CASE("Cancellation") { 17 | SettingsParser parser; 18 | parser.load(SETTINGS_FILE); 19 | 20 | const auto v4Addr = parser.get("ip", "v4"); 21 | const auto tcpPort = parser.get("ip", "tcpPort"); 22 | 23 | // Create IPv4 TCP socket 24 | ClientSocketIP sock; 25 | 26 | // Connect 27 | runSync([&]() -> Task<> { co_await sock.connect({ ConnectionType::TCP, "", v4Addr, tcpPort }); }); 28 | 29 | bool running = true; 30 | [&]() -> Task<> { 31 | try { 32 | co_await sock.recv(4); 33 | } catch (const System::SystemError& e) { 34 | CHECK(e.isCanceled()); 35 | running = false; 36 | } 37 | }(); 38 | 39 | int iterations = 0; 40 | while (running) { 41 | using namespace std::literals; 42 | 43 | Async::handleEvents(false); 44 | if (iterations == 5) sock.cancelIO(); 45 | 46 | iterations++; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/utils/uuids.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | namespace UUIDs { 11 | // 128-bit UUID represented in a platform-independent way. 12 | using UUID128 = std::array; 13 | 14 | // Swaps endianness to/from UUID byte order (big endian). 15 | template 16 | constexpr T byteSwap(T from) { 17 | if constexpr (std::endian::native == std::endian::big) return from; 18 | 19 | static_assert(std::endian::native == std::endian::little, "Unsupported/unknown endianness"); 20 | return std::byteswap(from); 21 | } 22 | 23 | UUID128 fromSegments(std::uint32_t d1, std::uint16_t d2, std::uint16_t d3, std::uint64_t d4); 24 | 25 | // Constructs a 128-bit Bluetooth UUID given the short (16- or 32-bit) UUID. 26 | inline UUIDs::UUID128 createFromBase(std::uint32_t uuidShort) { 27 | // To turn a 16-bit UUID into a 128-bit UUID: 28 | // | The 16-bit Attribute UUID replaces the x's in the following: 29 | // | 0000xxxx - 0000 - 1000 - 8000 - 00805F9B34FB 30 | // https://stackoverflow.com/a/36212021 31 | // (The same applies with a 32-bit UUID) 32 | return UUIDs::fromSegments(uuidShort, 0x0000, 0x1000, 0x800000805F9B34FB); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/sockets/delegates/linux/bidirectional.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "sockets/delegates/bidirectional.hpp" 5 | 6 | #include 7 | 8 | #include "net/enums.hpp" 9 | #include "os/async.hpp" 10 | #include "utils/task.hpp" 11 | 12 | template 13 | Task<> Delegates::Bidirectional::send(std::string data) { 14 | co_await Async::run([this, &data](Async::CompletionResult& result) { 15 | Async::submit(Async::Send{ { *handle, &result }, data }); 16 | }); 17 | } 18 | 19 | template 20 | Task Delegates::Bidirectional::recv(std::size_t size) { 21 | std::string data(size, 0); 22 | 23 | auto recvResult = co_await Async::run([this, &data](Async::CompletionResult& result) { 24 | Async::submit(Async::Receive{ { *handle, &result }, data }); 25 | }); 26 | 27 | if (recvResult.res == 0) co_return { true, true, "", std::nullopt }; 28 | 29 | data.resize(recvResult.res); 30 | co_return { true, false, data, std::nullopt }; 31 | } 32 | 33 | template Task<> Delegates::Bidirectional::send(std::string); 34 | template Task Delegates::Bidirectional::recv(std::size_t); 35 | 36 | template Task<> Delegates::Bidirectional::send(std::string); 37 | template Task Delegates::Bidirectional::recv(std::size_t); 38 | -------------------------------------------------------------------------------- /src/components/ioconsole.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | #include "console.hpp" 12 | #include "os/error.hpp" 13 | 14 | // Manages a textbox and console with config options. 15 | class IOConsole : public Console { 16 | // State 17 | bool focusOnTextbox = false; // If keyboard focus is applied to the textbox 18 | std::string textBuf; // Send textbox buffer 19 | 20 | // Options 21 | int currentLE = 0; // Index of the line ending selected 22 | bool sendEchoing = true; // If sent strings are displayed in the output 23 | bool clearTextboxOnSubmit = true; // If the textbox is cleared when the submit callback is called 24 | bool addFinalLineEnding = false; // If a final line ending is added to the callback input string 25 | unsigned int recvSize = 1024; // Unsigned int to work with ImGuiDataType 26 | unsigned int recvSizeTmp = 1024; // Temporary buffer to hold input 27 | 28 | void drawControls(); 29 | 30 | public: 31 | // Draws the window contents and returns text entered into the textbox when Enter is pressed. 32 | std::optional updateWithTextbox(); 33 | 34 | unsigned int getRecvSize() { 35 | return recvSize; 36 | } 37 | 38 | // Prints the details of a thrown exception. 39 | void errorHandler(System::SystemError error); 40 | }; 41 | -------------------------------------------------------------------------------- /tests/src/ip.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include 5 | 6 | #include 7 | 8 | #include "helpers/testio.hpp" 9 | #include "net/enums.hpp" 10 | #include "sockets/clientsocket.hpp" 11 | #include "utils/settingsparser.hpp" 12 | 13 | TEST_CASE("I/O (Internet Protocol)") { 14 | SettingsParser parser; 15 | parser.load(SETTINGS_FILE); 16 | 17 | const auto v4Addr = parser.get("ip", "v4"); 18 | const auto v6Addr = parser.get("ip", "v6"); 19 | const auto tcpPort = parser.get("ip", "tcpPort"); 20 | const auto udpPort = parser.get("ip", "udpPort"); 21 | 22 | using enum ConnectionType; 23 | 24 | SECTION("TCP") { 25 | SECTION("IPv4 TCP sockets") { 26 | ClientSocketIP s; 27 | testIOClient(s, { TCP, "", v4Addr, tcpPort }); 28 | } 29 | 30 | SECTION("IPv6 TCP sockets") { 31 | ClientSocketIP s; 32 | testIOClient(s, { TCP, "", v6Addr, tcpPort }); 33 | } 34 | } 35 | 36 | SECTION("UDP") { 37 | SECTION("IPv4 UDP sockets") { 38 | ClientSocketIP s; 39 | testIOClient(s, { UDP, "", v4Addr, udpPort }); 40 | } 41 | 42 | SECTION("IPv6 UDP sockets") { 43 | ClientSocketIP s; 44 | testIOClient(s, { UDP, "", v6Addr, udpPort }); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/sockets/delegates/windows/bidirectional.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "sockets/delegates/bidirectional.hpp" 5 | 6 | #include 7 | 8 | #include "net/enums.hpp" 9 | #include "os/async.hpp" 10 | #include "utils/task.hpp" 11 | 12 | template 13 | Task<> Delegates::Bidirectional::send(std::string data) { 14 | co_await Async::run([this, &data](Async::CompletionResult& result) { 15 | Async::submit(Async::Send{ { *handle, &result }, data }); 16 | }); 17 | } 18 | 19 | template 20 | Task Delegates::Bidirectional::recv(std::size_t size) { 21 | std::string data(size, 0); 22 | 23 | auto recvResult = co_await Async::run([this, &data](Async::CompletionResult& result) { 24 | Async::submit(Async::Receive{ { *handle, &result }, data }); 25 | }); 26 | 27 | // Check for disconnects 28 | if (recvResult.res == 0) co_return { true, true, "", std::nullopt }; 29 | 30 | // Resize string to received size 31 | data.resize(recvResult.res); 32 | co_return { true, false, data, std::nullopt }; 33 | } 34 | 35 | template Task<> Delegates::Bidirectional::send(std::string); 36 | template Task Delegates::Bidirectional::recv(std::size_t); 37 | 38 | template Task<> Delegates::Bidirectional::send(std::string); 39 | template Task Delegates::Bidirectional::recv(std::size_t); 40 | -------------------------------------------------------------------------------- /src/sockets/delegates/server.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include "delegates.hpp" 11 | #include "sockethandle.hpp" 12 | #include "traits.hpp" 13 | #include "net/device.hpp" 14 | #include "net/enums.hpp" 15 | #include "utils/task.hpp" 16 | 17 | namespace Delegates { 18 | // Manages operations on server sockets. 19 | template 20 | class Server : public ServerDelegate { 21 | SocketHandle& handle; 22 | NO_UNIQUE_ADDRESS Traits::Server traits; 23 | 24 | public: 25 | explicit Server(SocketHandle& handle) : handle(handle) {} 26 | 27 | ServerAddress startServer(const Device& serverInfo) override; 28 | 29 | Task accept() override; 30 | 31 | Task recvFrom(std::size_t size) override; 32 | 33 | Task<> sendTo(Device device, std::string data) override; 34 | }; 35 | } 36 | 37 | template <> 38 | ServerAddress Delegates::Server::startServer(const Device& serverInfo); 39 | 40 | template <> 41 | Task Delegates::Server::accept(); 42 | 43 | // There are no connectionless operations on Bluetooth sockets 44 | 45 | template <> 46 | inline Task Delegates::Server::recvFrom(std::size_t) { 47 | std::unreachable(); 48 | } 49 | 50 | template <> 51 | inline Task<> Delegates::Server::sendTo(Device, std::string) { 52 | std::unreachable(); 53 | } 54 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build: 10 | uses: ./.github/workflows/build-all-os.yml 11 | with: 12 | mode: release 13 | 14 | publish-release: 15 | name: Publish release 16 | runs-on: ubuntu-latest 17 | needs: [build] 18 | permissions: 19 | contents: write 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v4 24 | 25 | - name: Download Linux release 26 | uses: actions/download-artifact@v4 27 | with: 28 | name: WhaleConnect-linux 29 | path: WhaleConnect-linux 30 | 31 | - name: Download macOS release 32 | uses: actions/download-artifact@v4 33 | with: 34 | name: WhaleConnect-macos 35 | path: WhaleConnect-macos 36 | 37 | - name: Download Windows release 38 | uses: actions/download-artifact@v4 39 | with: 40 | name: WhaleConnect-windows 41 | path: WhaleConnect-windows 42 | 43 | - name: Compress releases 44 | run: | 45 | zip WhaleConnect-linux.zip WhaleConnect-linux/* -r -qq 46 | zip WhaleConnect-macos.zip WhaleConnect-macos/* -r -qq 47 | zip WhaleConnect-windows.zip WhaleConnect-windows/* -r -qq 48 | 49 | - name: Get latest changes 50 | run: xmake l xmake/scripts/get_latest_changes.lua 51 | 52 | - name: Release 53 | uses: softprops/action-gh-release@v2 54 | with: 55 | body_path: changelog-latest.txt 56 | files: | 57 | WhaleConnect-linux.zip 58 | WhaleConnect-macos.zip 59 | WhaleConnect-windows.zip 60 | -------------------------------------------------------------------------------- /src/os/errcheck.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | 9 | #include "os/error.hpp" 10 | 11 | // clang-format off 12 | // Predicate functors to check success based on return code 13 | inline struct CheckTrue { bool operator()(auto rc) { return static_cast(rc); } } checkTrue; 14 | inline struct CheckZero { bool operator()(auto rc) { return std::cmp_equal(rc, 0); } } checkZero; 15 | inline struct CheckNonError { bool operator()(auto rc) { return std::cmp_not_equal(rc, -1); } } checkNonError; 16 | 17 | // Projection functors to turn return codes into numeric error codes 18 | inline struct UseLastError { System::ErrorCode operator()(auto) { return System::getLastError(); } } useLastError; 19 | inline struct UseReturnCode { System::ErrorCode operator()(auto rc) { return rc; } } useReturnCode; 20 | inline struct UseReturnCodeNeg { System::ErrorCode operator()(auto rc) { return -rc; } } useReturnCodeNeg; 21 | 22 | // clang-format on 23 | 24 | // Calls a system function, and throws an exception if its return code does not match a success value. 25 | // The success condition and thrown error code can be changed with predicate and projection functions. 26 | template 27 | inline T check(const T& rc, Pred checkFn = {}, Proj transformFn = {}, 28 | System::ErrorType type = System::ErrorType::System, 29 | const std::source_location& location = std::source_location::current()) { 30 | System::ErrorCode code = transformFn(rc); 31 | return checkFn(rc) || !System::isFatal(code) ? rc : throw System::SystemError{ code, type, location }; 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/strings.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | namespace Strings { 11 | // Generalized string type for system functions. 12 | // 13 | // On Windows, for a program to be Unicode-aware, it needs to use the Windows API functions ending in "W", 14 | // indicating the function takes strings of wchar_t which are UTF-16 encoded. 15 | // Other platforms can use strings of char which are UTF-8 encoded and can handle Unicode. 16 | using SysStr = std::conditional_t; 17 | 18 | // Generalized string view type for system functions. 19 | using SysStrView = std::basic_string_view; 20 | 21 | // Converts a UTF-8 string into a UTF-16 string on Windows. 22 | SysStr toSys(std::string_view from); 23 | 24 | // Converts a UTF-16 string into a UTF-8 string on Windows. 25 | std::string fromSys(SysStrView from); 26 | 27 | // Converts an integer or decimal value to a system string. 28 | template 29 | requires std::integral || std::floating_point 30 | SysStr toSys(T from) { 31 | if constexpr (OS_WINDOWS) return std::to_wstring(from); 32 | else return std::to_string(from); 33 | } 34 | 35 | // Replaces all occurrences of a substring within a given base string. 36 | std::string replaceAll(std::string str, std::string_view from, std::string_view to); 37 | 38 | // Removes null characters from the end of a string. 39 | template 40 | void stripNull(std::basic_string& s) { 41 | s.erase(s.find(T{})); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/build-windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows build 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | mode: 7 | type: string 8 | required: true 9 | 10 | jobs: 11 | build-windows: 12 | runs-on: windows-latest 13 | env: 14 | XMAKE_GLOBALDIR: ${{ github.workspace }}/xmake-global 15 | 16 | steps: 17 | - name: Get current date as package key 18 | id: cache_key 19 | run: echo "key=$(date +'%W')" >> $GITHUB_OUTPUT 20 | shell: bash 21 | 22 | - name: Checkout repository 23 | uses: actions/checkout@v4 24 | 25 | - name: Setup xmake 26 | uses: xmake-io/github-action-setup-xmake@v1 27 | with: 28 | xmake-version: '2.9.5' 29 | actions-cache-folder: .xmake-cache-W${{ steps.cache_key.outputs.key }} 30 | 31 | - name: Update xmake repository 32 | run: xmake repo --update 33 | 34 | - name: Retrieve dependencies hash 35 | id: dep_hash 36 | run: echo "hash=$(xmake l utils.ci.packageskey)" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append 37 | 38 | - name: Retrieve cached xmake dependencies 39 | uses: actions/cache@v4 40 | with: 41 | path: ${{ env.XMAKE_GLOBALDIR }}/.xmake/packages 42 | key: Windows-${{ steps.dep_hash.outputs.hash }}-W${{ steps.cache_key.outputs.key }} 43 | 44 | - name: Configure xmake 45 | run: xmake f -m ${{ inputs.mode }} --ccache=n -y 46 | 47 | - name: Build 48 | run: xmake 49 | 50 | - name: Install 51 | if: inputs.mode == 'release' 52 | run: xmake i -o dist WhaleConnect 53 | 54 | - name: Upload 55 | if: inputs.mode == 'release' 56 | uses: actions/upload-artifact@v4 57 | with: 58 | name: WhaleConnect-windows 59 | path: dist 60 | -------------------------------------------------------------------------------- /src/components/window.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | // Represents a Dear ImGui window. 12 | class Window { 13 | std::string title; // Window title 14 | bool open = true; // If this window is open 15 | bool* openPtr = &open; // Pointer passed to ImGui::Begin 16 | 17 | // Always runs on every frame, before onUpdate is called. 18 | virtual void onBeforeUpdate() { 19 | // May be overridden optionally 20 | } 21 | 22 | // Redraws the contents of the window. Must be overridden in derived classes. 23 | virtual void onUpdate() = 0; 24 | 25 | protected: 26 | // Enables or disables the window's close button. 27 | void setClosable(bool closable) { 28 | openPtr = closable ? &open : nullptr; 29 | } 30 | 31 | void setTitle(std::string_view newTitle) { 32 | title = newTitle; 33 | } 34 | 35 | public: 36 | // Sets the window title. 37 | explicit Window(std::string_view title) : title(title) {} 38 | 39 | // Virtual destructor provided for derived classes. 40 | virtual ~Window() = default; 41 | 42 | // Gets the window title. 43 | [[nodiscard]] std::string_view getTitle() const { 44 | return title; 45 | } 46 | 47 | // Gets the window's open/closed state. 48 | [[nodiscard]] bool isOpen() const { 49 | return open; 50 | } 51 | 52 | // Updates the window and its contents. 53 | void update() { 54 | onBeforeUpdate(); 55 | 56 | // Render window 57 | if (ImGui::Begin(title.c_str(), openPtr)) onUpdate(); 58 | ImGui::End(); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /src/gui/newserver.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "newserver.hpp" 5 | 6 | #include 7 | 8 | #include "imguiext.hpp" 9 | #include "components/serverwindow.hpp" 10 | #include "net/device.hpp" 11 | #include "net/enums.hpp" 12 | 13 | void drawNewServerWindow(WindowList& servers, bool& open) { 14 | if (!open) return; 15 | 16 | using namespace ImGuiExt::Literals; 17 | 18 | ImGui::SetNextWindowSize(35_fh * 13_fh, ImGuiCond_Appearing); 19 | if (!ImGui::Begin("New Server", &open)) { 20 | ImGui::End(); 21 | return; 22 | } 23 | 24 | using enum ConnectionType; 25 | static Device serverInfo{ TCP, "", "", 0 }; 26 | 27 | if (serverInfo.type == TCP || serverInfo.type == UDP) { 28 | ImGui::SetNextItemWidth(15_fh); 29 | ImGuiExt::inputText("Address", serverInfo.address); 30 | 31 | ImGui::SameLine(); 32 | if (ImGui::Button("IPv4")) serverInfo.address = "0.0.0.0"; 33 | 34 | ImGui::SameLine(); 35 | if (ImGui::Button("IPv6")) serverInfo.address = "::"; 36 | 37 | ImGui::SameLine(); 38 | } 39 | 40 | ImGui::SetNextItemWidth(7_fh); 41 | ImGuiExt::inputScalar("Port", serverInfo.port, 1, 10); 42 | 43 | ImGuiExt::radioButton("TCP", serverInfo.type, TCP); 44 | ImGuiExt::radioButton("UDP", serverInfo.type, UDP); 45 | ImGuiExt::radioButton("RFCOMM", serverInfo.type, RFCOMM); 46 | if constexpr (!OS_WINDOWS) ImGuiExt::radioButton("L2CAP", serverInfo.type, L2CAP); 47 | 48 | // Cannot check the result of add since server titles are generated dynamically. 49 | if (ImGui::Button("Create Server")) servers.add("", serverInfo); 50 | 51 | ImGui::End(); 52 | } 53 | -------------------------------------------------------------------------------- /src/net/btutils.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "device.hpp" 12 | #include "utils/uuids.hpp" 13 | 14 | namespace BTUtils { 15 | struct Instance { 16 | // Initializes the OS APIs to use Bluetooth. 17 | Instance(); 18 | 19 | // Cleans up the OS APIs. 20 | ~Instance(); 21 | }; 22 | 23 | // Bluetooth profile descriptor. 24 | struct ProfileDesc { 25 | std::uint16_t uuid = 0; // 16-bit UUID 26 | std::uint8_t versionMajor = 0; // Major version number 27 | std::uint8_t versionMinor = 0; // Minor version number 28 | }; 29 | 30 | // Service result returned from an SDP inquiry. 31 | struct SDPResult { 32 | std::vector protoUUIDs; // 16-bit protocol UUIDs 33 | std::vector serviceUUIDs; // 128-bit service class UUIDs 34 | std::vector profileDescs; // Profile descriptors 35 | std::uint16_t port = 0; // Port advertised (PSM for L2CAP, channel for RFCOMM) 36 | std::string name; // Service name 37 | std::string desc; // Service description (if any) 38 | }; 39 | 40 | // Gets the Bluetooth devices that are paired to this computer. 41 | // The Device instances returned have no type set because the communication protocol to use with them is 42 | // indeterminate (the function doesn't know if L2CAP or RFCOMM should be used with each). 43 | std::vector getPaired(); 44 | 45 | // Runs a Service Discovery Protocol (SDP) inquiry on a remote device. 46 | std::vector sdpLookup(std::string_view addr, UUIDs::UUID128 uuid, bool flushCache); 47 | } 48 | -------------------------------------------------------------------------------- /src/sockets/delegates/traits.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #if OS_WINDOWS 7 | #include 8 | #elif OS_MACOS 9 | #include 10 | 11 | #include 12 | #endif 13 | 14 | #include "net/enums.hpp" 15 | 16 | namespace Traits { 17 | // Platform-specific traits for socket handles. 18 | #if OS_WINDOWS 19 | template 20 | struct SocketHandle { 21 | using HandleType = SOCKET; 22 | static constexpr auto invalidHandle = INVALID_SOCKET; 23 | }; 24 | #elif OS_MACOS 25 | template 26 | struct SocketHandle {}; 27 | 28 | template <> 29 | struct SocketHandle { 30 | using HandleType = int; 31 | static constexpr auto invalidHandle = -1; 32 | }; 33 | 34 | template <> 35 | struct SocketHandle { 36 | using HandleType = std::optional; 37 | static constexpr auto invalidHandle = std::nullopt; 38 | }; 39 | #elif OS_LINUX 40 | template 41 | struct SocketHandle { 42 | using HandleType = int; 43 | static constexpr auto invalidHandle = -1; 44 | }; 45 | #endif 46 | 47 | // Convenience type alias for socket handle types. 48 | template 49 | using SocketHandleType = SocketHandle::HandleType; 50 | 51 | // Convenience function for invalid socket handle values. 52 | template 53 | constexpr auto invalidSocketHandle() { 54 | return SocketHandle::invalidHandle; 55 | } 56 | 57 | // Traits for server sockets. 58 | template 59 | struct Server {}; 60 | 61 | template <> 62 | struct Server { 63 | IPType ip; 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /.github/workflows/update-deps-list.yml: -------------------------------------------------------------------------------- 1 | name: Update dependencies list 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'docs/**' 7 | - '.github/ISSUE_TEMPLATE/**' 8 | - '.github/pull_request_template.md' 9 | - '.github/workflows/build-debug.yml' 10 | - '.github/workflows/clang-format-check.yml' 11 | - '.github/workflows/publish-release.yml' 12 | branches: 13 | - main 14 | 15 | jobs: 16 | update-deps-list: 17 | runs-on: ubuntu-latest 18 | permissions: 19 | contents: write 20 | 21 | steps: 22 | - name: Get current date as package key 23 | id: cache_key 24 | run: echo "key=$(date +'%W')" >> $GITHUB_OUTPUT 25 | shell: bash 26 | 27 | - name: Checkout repository 28 | uses: actions/checkout@v4 29 | with: 30 | ref: ${{ github.head_ref }} 31 | 32 | - name: Setup xmake 33 | uses: xmake-io/github-action-setup-xmake@v1 34 | with: 35 | xmake-version: '2.9.5' 36 | actions-cache-folder: .xmake-cache-W${{ steps.cache_key.outputs.key }} 37 | 38 | - name: Update dependencies list 39 | run: | 40 | xmake repo --update 41 | xmake l xmake/scripts/parse_lock_file.lua 42 | xmake l xmake/scripts/generate_deps_list.lua 43 | 44 | - name: Commit changes 45 | uses: stefanzweifel/git-auto-commit-action@v5 46 | id: commit_action 47 | with: 48 | commit_message: Update dependencies list 49 | file_pattern: docs/dependencies.md 50 | 51 | - name: Run if changes have been detected 52 | if: steps.commit_action.outputs.changes_detected == 'true' 53 | run: echo "Dependencies list has been updated." 54 | 55 | - name: Run if no changes have been detected 56 | if: steps.commit_action.outputs.changes_detected == 'false' 57 | run: echo "No changes detected in dependencies list." 58 | -------------------------------------------------------------------------------- /src/sockets/socket.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | #include "delegates/delegates.hpp" 9 | #include "net/device.hpp" 10 | #include "utils/task.hpp" 11 | 12 | // Socket of any type. 13 | class Socket { 14 | Delegates::HandleDelegate* handle; 15 | Delegates::IODelegate* io; 16 | Delegates::ClientDelegate* client; 17 | Delegates::ServerDelegate* server; 18 | 19 | public: 20 | Socket(Delegates::HandleDelegate& handle, Delegates::IODelegate& io, Delegates::ClientDelegate& client, 21 | Delegates::ServerDelegate& server) : 22 | handle(&handle), 23 | io(&io), client(&client), server(&server) {} 24 | 25 | virtual ~Socket() = default; 26 | 27 | void close() const { 28 | handle->close(); 29 | } 30 | 31 | bool isValid() const { 32 | return handle->isValid(); 33 | } 34 | 35 | void cancelIO() const { 36 | handle->cancelIO(); 37 | } 38 | 39 | Task<> send(std::string_view data) const { 40 | return io->send(std::string{ data }); 41 | } 42 | 43 | Task recv(std::size_t size) const { 44 | return io->recv(size); 45 | } 46 | 47 | Task<> connect(const Device& device) const { 48 | return client->connect(device); 49 | } 50 | 51 | ServerAddress startServer(const Device& serverInfo) const { 52 | return server->startServer(serverInfo); 53 | } 54 | 55 | Task accept() const { 56 | return server->accept(); 57 | } 58 | 59 | Task recvFrom(std::size_t size) const { 60 | return server->recvFrom(size); 61 | } 62 | 63 | Task<> sendTo(const Device& device, std::string_view data) const { 64 | return server->sendTo(device, std::string{ data }); 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /src/components/windowlist.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "window.hpp" 12 | 13 | // Manages and updates Window objects. 14 | class WindowList { 15 | std::vector> windows; // List of window pointers 16 | 17 | // Checks if the list contains a window with the specified title. 18 | bool validateDuplicate(std::string_view title) { 19 | auto windowIter 20 | = std::ranges::find_if(windows, [title](const auto& current) { return current->getTitle() == title; }); 21 | 22 | return windowIter == windows.end(); 23 | } 24 | 25 | public: 26 | // Adds a new window to the list. 27 | template 28 | requires std::derived_from && std::constructible_from 29 | bool add(std::string_view title, Args&&... args) { 30 | if (!validateDuplicate(title)) return false; 31 | 32 | // Create the pointer and add it to the list 33 | windows.push_back(std::make_unique(title, std::forward(args)...)); 34 | return true; 35 | } 36 | 37 | // Redraws all contained windows and deletes any that have been closed. 38 | void update() { 39 | // Remove all closed windows 40 | std::erase_if(windows, [](const auto& window) { return !window->isOpen(); }); 41 | 42 | // Update all open windows 43 | for (const auto& i : windows) i->update(); 44 | } 45 | 46 | // Expose functions from internal vector 47 | 48 | auto begin() { 49 | return windows.begin(); 50 | } 51 | 52 | auto end() { 53 | return windows.end(); 54 | } 55 | 56 | bool empty() const { 57 | return windows.empty(); 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /src/gui/about.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "about.hpp" 5 | 6 | #include 7 | #include 8 | 9 | #include "gui/imguiext.hpp" 10 | 11 | void drawAboutWindow(bool& open) { 12 | if (!open) return; 13 | 14 | using namespace ImGuiExt::Literals; 15 | ImGui::SetNextWindowSize(25_fh * 20_fh, ImGuiCond_FirstUseEver); 16 | if (!ImGui::Begin("About", &open)) { 17 | ImGui::End(); 18 | return; 19 | } 20 | 21 | static bool copy = false; 22 | if (copy) ImGui::LogToClipboard(); 23 | 24 | ImGui::Text("WhaleConnect"); 25 | ImGui::Text("Cross-platform network communication software"); 26 | 27 | ImGui::SeparatorText("Version/Build"); 28 | ImGui::Text("Version: %s", Config::version); 29 | ImGui::Text("Build: %s", Config::versionBuild); 30 | ImGui::Text("Git commit: %s", Config::gitCommitLong); 31 | 32 | ImGui::SeparatorText("System"); 33 | ImGui::Text("Built for: %s, %s", Config::plat, Config::arch); 34 | 35 | if (copy) { 36 | ImGui::LogFinish(); 37 | copy = false; 38 | } 39 | 40 | ImGui::Spacing(); 41 | if (ImGui::Button("Copy")) copy = true; 42 | ImGui::End(); 43 | } 44 | 45 | void drawLinksWindow(bool& open) { 46 | if (!open) return; 47 | 48 | using namespace ImGuiExt::Literals; 49 | ImGui::SetNextWindowSize(20_fh * 10_fh, ImGuiCond_FirstUseEver); 50 | if (!ImGui::Begin("Links", &open)) { 51 | ImGui::End(); 52 | return; 53 | } 54 | 55 | ImGui::TextWrapped("These are helpful links to get information and support."); 56 | ImGui::TextLinkOpenURL("Repository", "https://github.com/WhaleConnect/whaleconnect"); 57 | ImGui::SameLine(); 58 | ImGui::TextLinkOpenURL("Changelog", "https://github.com/WhaleConnect/whaleconnect/blob/main/docs/changelog.md"); 59 | 60 | ImGui::End(); 61 | } 62 | -------------------------------------------------------------------------------- /src/os/bluetooth.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | // This header is only used on macOS. 5 | 6 | #pragma once 7 | 8 | #include 9 | 10 | #include 11 | 12 | #include "net/device.hpp" 13 | 14 | #ifndef NO_EXPOSE_INTERNAL 15 | #include 16 | 17 | #include "async.hpp" 18 | #endif 19 | 20 | // The type of a Bluetooth I/O operation. 21 | enum class IOType { Send, Receive }; 22 | 23 | // Removes results from previous receive/accept operations on a Bluetooth channel. 24 | void clearBluetoothDataQueue(unsigned long id); 25 | 26 | // Signals completion of a Bluetooth read operation. 27 | void bluetoothReadComplete(unsigned long id, const char* data, std::size_t dataLen); 28 | 29 | // Signals completion of a Bluetooth operation. 30 | bool bluetoothComplete(unsigned long id, IOType ioType, IOReturn status); 31 | 32 | // Signals completion of a Bluetooth accept operation. 33 | void bluetoothAcceptComplete(unsigned long id, const void* handle, const Device& device); 34 | 35 | // Signals closure of a Bluetooth channel. 36 | void bluetoothClosed(unsigned long id); 37 | 38 | // Definitions not exposed to Swift. 39 | #ifndef NO_EXPOSE_INTERNAL 40 | namespace AsyncBT { 41 | struct BTAccept { 42 | Device from; 43 | BluetoothMacOS::BTHandle handle; 44 | }; 45 | 46 | // Creates a pending operation for a Bluetooth channel. 47 | void submit(swift::UInt id, IOType ioType, Async::CompletionResult& result); 48 | 49 | // Gets the first queued result of a Bluetooth read operation. 50 | std::optional getReadResult(swift::UInt id); 51 | 52 | // Gets the first queued result of a Bluetooth accept operation. 53 | BTAccept getAcceptResult(swift::UInt id); 54 | 55 | // Cancels all pending operations on a Bluetooth channel. 56 | void cancel(swift::UInt id); 57 | } 58 | #endif 59 | -------------------------------------------------------------------------------- /src/os/error.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #if OS_WINDOWS 11 | #include 12 | #endif 13 | 14 | namespace System { 15 | #if OS_WINDOWS 16 | using ErrorCode = DWORD; 17 | #else 18 | using ErrorCode = int; 19 | #endif 20 | 21 | // Where an error came from. 22 | enum class ErrorType { 23 | System, // From socket functions or other OS APIs 24 | AddrInfo, // From a call to getaddrinfo 25 | IOReturn // From a call to a macOS kernel function 26 | }; 27 | 28 | // Gets the last error code. This is platform-specific. 29 | ErrorCode getLastError(); 30 | 31 | // Checks if an error code should be handled as a fatal error. 32 | bool isFatal(ErrorCode code); 33 | 34 | // Formats a system error into a readable string. 35 | std::string formatSystemError(ErrorCode code, ErrorType type, const std::source_location& location); 36 | 37 | // Exception structure containing details of an error. 38 | struct SystemError : std::runtime_error { 39 | ErrorCode code = 0; // The platform-specific error code 40 | ErrorType type = ErrorType::System; // The type of the error 41 | 42 | // Constructs an object representing a specific error. 43 | SystemError(ErrorCode code, ErrorType type, 44 | const std::source_location& location = std::source_location::current()) : 45 | std::runtime_error(formatSystemError(code, type, location)), 46 | code(code), type(type) {} 47 | 48 | // Checks if this object represents a fatal error. 49 | explicit operator bool() const { 50 | return isFatal(code); 51 | } 52 | 53 | // Checks if this exception represents a canceled operation. 54 | bool isCanceled() const; 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/build-linux.yml: -------------------------------------------------------------------------------- 1 | name: Linux build 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | mode: 7 | type: string 8 | required: true 9 | 10 | jobs: 11 | build-linux: 12 | runs-on: ubuntu-latest 13 | env: 14 | CC: /usr/bin/gcc-14 15 | CXX: /usr/bin/g++-14 16 | XMAKE_GLOBALDIR: ${{ github.workspace }}/xmake-global 17 | 18 | steps: 19 | - name: Install dependencies 20 | run: | 21 | sudo apt update 22 | sudo apt install libx11-dev libxrandr-dev libxrender-dev libxinerama-dev libxfixes-dev libxcursor-dev libxi-dev libxext-dev libgl-dev -y 23 | 24 | - name: Get current date as package key 25 | id: cache_key 26 | run: echo "key=$(date +'%W')" >> $GITHUB_OUTPUT 27 | 28 | - name: Checkout repository 29 | uses: actions/checkout@v4 30 | 31 | - name: Setup xmake 32 | uses: xmake-io/github-action-setup-xmake@v1 33 | with: 34 | xmake-version: '2.9.5' 35 | actions-cache-folder: .xmake-cache-W${{ steps.cache_key.outputs.key }} 36 | 37 | - name: Update xmake repository 38 | run: xmake repo --update 39 | 40 | - name: Retrieve dependencies hash 41 | id: dep_hash 42 | run: echo "hash=$(xmake l utils.ci.packageskey)" >> $GITHUB_OUTPUT 43 | 44 | - name: Retrieve cached xmake dependencies 45 | uses: actions/cache@v4 46 | with: 47 | path: ${{ env.XMAKE_GLOBALDIR }}/.xmake/packages 48 | key: Linux-${{ steps.dep_hash.outputs.hash }}-W${{ steps.cache_key.outputs.key }} 49 | 50 | - name: Configure xmake 51 | run: xmake f -m ${{ inputs.mode }} --ccache=n -y 52 | 53 | - name: Build 54 | run: xmake 55 | 56 | - name: Install 57 | if: inputs.mode == 'release' 58 | run: xmake i -o dist WhaleConnect 59 | 60 | - name: Upload 61 | if: inputs.mode == 'release' 62 | uses: actions/upload-artifact@v4 63 | with: 64 | name: WhaleConnect-linux 65 | path: dist 66 | -------------------------------------------------------------------------------- /xmake/scripts/generate_deps_list.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | -- SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | -- This script generates the list of dependencies (docs/dependencies.md). 5 | -- The package data file must be generated before running this script: 6 | -- xmake l parse_lock_file.lua 7 | 8 | local header = [[# WhaleConnect Dependencies 9 | 10 | This information was generated with data from xmake-repo. 11 | 12 | OS keys: L = Linux, M = macOS, W = Windows 13 | 14 | | Name | Version | OS | License | Description | 15 | | --- | --- | --- | --- | --- | 16 | ]] 17 | 18 | function main(...) 19 | local packages = io.load(path.join(os.projectdir(), "build", "referenced.txt")) 20 | local lockInfo = io.load(path.join(os.projectdir(), "build", "packages.txt")) 21 | 22 | local outLines = {} 23 | for _, package in ipairs(packages) do 24 | local outLine = {} 25 | 26 | local packageInfo = lockInfo[package] 27 | if packageInfo == nil then 28 | print("Could not find information in lock file for:", package) 29 | else 30 | local packageFile = io.readfile(packageInfo["filepath"]) 31 | 32 | local homepageStr = packageFile:match("set_homepage%(\"([^\n]+)\"%)") 33 | outLine[1] = homepageStr and ("[%s](%s)"):format(package, homepageStr) or package 34 | 35 | outLine[2] = packageInfo["version"] 36 | outLine[3] = table.concat(packageInfo["os"], " ") 37 | 38 | local licenseStr = packageFile:match("set_license%(\"([^\n]+)\"%)") 39 | outLine[4] = licenseStr or "Unknown" 40 | 41 | local descriptionStr = packageFile:match("set_description%(\"([^\n]+)\"%)") 42 | outLine[5] = descriptionStr or "None" 43 | end 44 | 45 | outLines[#outLines + 1] = ("| %s |"):format(table.concat(outLine, " | ")) 46 | end 47 | 48 | local outText = header .. table.concat(outLines, "\n") .. "\n" 49 | io.writefile(path.join(os.projectdir(), "docs", "dependencies.md"), outText) 50 | end 51 | -------------------------------------------------------------------------------- /docs/dependencies.md: -------------------------------------------------------------------------------- 1 | # WhaleConnect Dependencies 2 | 3 | This information was generated with data from xmake-repo. 4 | 5 | OS keys: L = Linux, M = macOS, W = Windows 6 | 7 | | Name | Version | OS | License | Description | 8 | | --- | --- | --- | --- | --- | 9 | | [bluez](http://www.bluez.org) | 5.70.0 | L | GPL-2.0-or-later | Library for the Bluetooth protocol stack for Linux | 10 | | [botan](https://botan.randombit.net) | 3.5.0 | L M W | BSD-2-Clause | Cryptography Toolkit | 11 | | [catch2](https://github.com/catchorg/Catch2) | 3.7.1 | L M W | BSL-1.0 | Catch2 is a multi-paradigm test framework for C++. which also supports Objective-C (and maybe C). | 12 | | [dbus](https://www.freedesktop.org/wiki/Software/dbus/) | 1.14.8 | L | GPL-2.0-or-later | D-Bus is a message bus system, a simple way for applications to talk to one another. | 13 | | [glfw](https://www.glfw.org/) | 3.4.0 | L M W | zlib | GLFW is an Open Source, multi-platform library for OpenGL, OpenGL ES and Vulkan application development. | 14 | | [imgui](https://github.com/ocornut/imgui) | 1.91.6 | L M W | MIT | Bloat-free Immediate Mode Graphical User interface for C++ with minimal dependencies | 15 | | [imguitextselect](https://github.com/AidanSun05/ImGuiTextSelect) | 1.1.3 | L M W | MIT | Text selection implementation for Dear ImGui | 16 | | [liburing](https://github.com/axboe/liburing) | 2.8.0 | L | MIT | liburing provides helpers to setup and teardown io_uring instances | 17 | | [noto-sans-mono](https://github.com/notofonts/notofonts.github.io) | 2.14.0 | L M W | OFL-1.1 | Noto Sans Mono font | 18 | | [opengl](https://opengl.org/) | latest | L M W | Unknown | OpenGL - The Industry Standard for High Performance Graphics | 19 | | [out_ptr](https://github.com/soasis/out_ptr) | 2022.10.7 | L M W | Apache-2.0 | Repository for a C++11 implementation of std::out_ptr (p1132), as a standalone library! | 20 | | [remix-icon](https://github.com/Remix-Design/RemixIcon) | 4.3.0 | L M W | Apache-2.0 | Remix Icon font | 21 | | [utfcpp](https://github.com/nemtrif/utfcpp) | 4.0.6 | L M W | BSL-1.0 | UTF8-CPP: UTF-8 with C++ in a Portable Way | 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report. 3 | labels: ['bug'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | We encourage using GitHub as a centralized place for community engagement. Please complete the following before submitting your issue: 9 | 10 | - Read the [contributing guidelines](https://github.com/WhaleConnect/whaleconnect/blob/main/docs/CONTRIBUTING.md). 11 | - Search for [past issues](https://github.com/WhaleConnect/whaleconnect/issues?q=) to check for similar discussions. 12 | 13 | Thank you for taking the time to complete these steps and fill out this issue form. 14 | 15 | - type: textarea 16 | attributes: 17 | label: WhaleConnect Build Info 18 | description: Go to "Help > About" in WhaleConnect, click Copy, and paste it here. 19 | validations: 20 | required: true 21 | 22 | - type: input 23 | attributes: 24 | label: Operating System 25 | description: What OS are you using? Include (if applicable) - Windows OS version and edition (Settings > System > About), macOS version (Apple menu > About this Mac), Linux distro and kernel version (lsb_release, uname -a). 26 | validations: 27 | required: true 28 | 29 | - type: input 30 | attributes: 31 | label: CPU Architecture 32 | description: What is your CPU architecture? WhaleConnect supports specific OS and architecture combinations, listed in the readme. If you have a custom build for an unsupported combination, indicate that as well - though we may not be able to fully provide support in these cases. 33 | validations: 34 | required: true 35 | 36 | - type: textarea 37 | attributes: 38 | label: Bug Description 39 | description: A concise and detailed description of the current behavior and what you expected. Include screenshots/screen recordings to aid in your description. 40 | validations: 41 | required: true 42 | 43 | - type: textarea 44 | attributes: 45 | label: Environment/Settings Configuration 46 | description: Any setup that you believe could influence this behavior. 47 | -------------------------------------------------------------------------------- /docs/building.md: -------------------------------------------------------------------------------- 1 | # Building WhaleConnect 2 | 3 | To build WhaleConnect from source code, you will need [xmake](https://xmake.io) and an up-to-date compiler (see below). 4 | 5 | All commands are run in the repository root. 6 | 7 | ## Tools 8 | 9 | This project uses C++23 features and Swift-to-C++ interop on macOS. Recommended tools and versions are listed below for each operating system: 10 | 11 | - **Windows:** MSVC 19.39 (Visual Studio 2022 17.9) 12 | - **macOS:** Apple Clang 15, Swift 5.9 (Xcode 15.3) 13 | - **Linux:** GCC 14 14 | 15 | All code is standards compliant. However, because the project uses recent C++ features, other compilers, and versions older than those listed above, may not be supported. 16 | 17 | ## Configuring the Build 18 | 19 | The arguments below are passed to `xmake f`/`xmake config` to configure the build process. Configuration should be done before compiling. 20 | 21 | > [!NOTE] 22 | > You should set all of your desired options in one config command. An invocation overwrites previously set configuration options. 23 | 24 | When configuring for the first time, xmake will ask you to install libraries needed by WhaleConnect. Enter `y` to continue and install them. 25 | 26 | If you are using the xmake VSCode extension, these arguments can be saved in the `xmake.additionalConfigArguments` setting. 27 | 28 | ### Build Mode 29 | 30 | Use one of the following to set the build mode: 31 | 32 | ```text 33 | -m debug # For debug builds (default) 34 | -m release # For release builds 35 | ``` 36 | 37 | ## Compiling With xmake 38 | 39 | ```shell 40 | xmake build swift # Build Swift code (macOS only) 41 | xmake # Build project 42 | xmake run # Run app 43 | ``` 44 | 45 | ## Packaging 46 | 47 | Creating a distributable package for WhaleConnect is done with `xmake i`/`xmake install`: 48 | 49 | ```shell 50 | xmake i -o /path/to/package WhaleConnect 51 | ``` 52 | 53 | This command must be run after a successful compilation. xmake will copy files such as the built executable and all required shared libraries and download third-party licenses into the specified output directory. 54 | -------------------------------------------------------------------------------- /src/sockets/delegates/linux/client.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "sockets/delegates/client.hpp" 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include "net/device.hpp" 13 | #include "net/enums.hpp" 14 | #include "net/netutils.hpp" 15 | #include "os/async.hpp" 16 | #include "os/errcheck.hpp" 17 | 18 | void startConnect(int s, sockaddr* addr, socklen_t len, Async::CompletionResult& result) { 19 | Async::submit(Async::Connect{ { s, &result }, addr, len }); 20 | } 21 | 22 | template <> 23 | Task<> Delegates::Client::connect(Device device) { 24 | auto addr = NetUtils::resolveAddr(device); 25 | 26 | co_await NetUtils::loopWithAddr(addr.get(), [this](const AddrInfoType* result) -> Task<> { 27 | handle.reset(check(socket(result->ai_family, result->ai_socktype, result->ai_protocol))); 28 | co_await Async::run(std::bind_front(startConnect, *handle, result->ai_addr, result->ai_addrlen)); 29 | }); 30 | } 31 | 32 | template <> 33 | Task<> Delegates::Client::connect(Device device) { 34 | // Address of the device to connect to 35 | bdaddr_t bdaddr; 36 | str2ba(device.address.c_str(), &bdaddr); 37 | 38 | // Set the appropriate sockaddr struct based on the protocol 39 | if (device.type == ConnectionType::RFCOMM) { 40 | sockaddr_rc addr{ AF_BLUETOOTH, bdaddr, static_cast(device.port) }; 41 | handle.reset(check(socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM))); 42 | 43 | co_await Async::run(std::bind_front(startConnect, *handle, reinterpret_cast(&addr), sizeof(addr))); 44 | } else { 45 | sockaddr_l2 addr{ AF_BLUETOOTH, htobs(device.port), bdaddr, 0, 0 }; 46 | handle.reset(check(socket(AF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP))); 47 | 48 | co_await Async::run(std::bind_front(startConnect, *handle, reinterpret_cast(&addr), sizeof(addr))); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/src/helpers/helpers.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #if OS_MACOS 12 | #include 13 | #endif 14 | 15 | #include "os/async.hpp" 16 | #include "utils/task.hpp" 17 | 18 | // Concept to ensure a function type is an awaitable coroutine. 19 | template 20 | concept Awaitable = requires (T) { typename std::coroutine_traits>::promise_type; }; 21 | 22 | // Runs a coroutine synchronously. 23 | // Bluetooth functions on macOS require a run loop for events. 24 | void runSync(const Awaitable auto& fn, bool useRunLoop = false) { 25 | bool hasRunLoop = OS_MACOS && useRunLoop; 26 | 27 | // Completion tracking 28 | std::atomic_bool completed = false; 29 | 30 | // Keep track of exceptions thrown in the coroutine 31 | // Exceptions won't propagate out of the coroutine so they must be rethrown manually for the tests to catch them. 32 | std::exception_ptr ptr; 33 | 34 | // Run an outer coroutine, but don't await it 35 | [&]() -> Task<> { 36 | try { 37 | // Await the given coroutine 38 | co_await fn(); 39 | } catch (const std::exception&) { 40 | // Store thrown exceptions 41 | ptr = std::current_exception(); 42 | } 43 | 44 | // Either the coroutine finished, or it threw an exception 45 | if (hasRunLoop) { 46 | #if OS_MACOS 47 | CFRunLoopStop(CFRunLoopGetCurrent()); 48 | #endif 49 | } else { 50 | completed = true; 51 | } 52 | }(); 53 | 54 | // Wait for the completion condition 55 | if (hasRunLoop) { 56 | #if OS_MACOS 57 | CFRunLoopRun(); 58 | #endif 59 | } else { 60 | // clang-format off 61 | while (!completed) Async::handleEvents(); 62 | // clang-format on 63 | } 64 | 65 | // Rethrow any exceptions 66 | if (ptr) std::rethrow_exception(ptr); 67 | } 68 | -------------------------------------------------------------------------------- /src/net/btutils.macos.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "btutils.hpp" 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | #include "btutils.internal.hpp" 13 | #include "device.hpp" 14 | #include "enums.hpp" 15 | #include "os/errcheck.hpp" 16 | #include "os/error.hpp" 17 | 18 | BTUtils::Instance::Instance() = default; 19 | 20 | BTUtils::Instance::~Instance() = default; 21 | 22 | std::vector BTUtils::getPaired() { 23 | auto list = BluetoothMacOS::getPairedDevices(); 24 | std::vector ret; 25 | 26 | for (const auto& i : list) ret.emplace_back(ConnectionType::None, i.getName(), i.getAddress(), 0); 27 | return ret; 28 | } 29 | 30 | std::vector BTUtils::sdpLookup(std::string_view addr, UUIDs::UUID128 uuid, bool flushCache) { 31 | auto list = check( 32 | BluetoothMacOS::sdpLookup(addr.data(), uuid.data(), flushCache), 33 | [](const BluetoothMacOS::LookupResult& result) { return result.getResult() == kIOReturnSuccess; }, 34 | [](const BluetoothMacOS::LookupResult& result) { return result.getResult(); }, System::ErrorType::IOReturn) 35 | .getList(); 36 | 37 | std::vector ret; 38 | 39 | for (const auto& i : list) { 40 | SDPResult result{ .port = i.getPort(), .name = i.getName(), .desc = i.getDesc() }; 41 | 42 | for (const auto& proto : i.getProtoUUIDs()) result.protoUUIDs.push_back(proto); 43 | 44 | for (const auto& service : i.getServiceUUIDs()) { 45 | UUIDs::UUID128 uuid; 46 | std::memcpy(uuid.data(), service, 16); 47 | result.serviceUUIDs.push_back(uuid); 48 | } 49 | 50 | for (const auto& profile : i.getProfileDescs()) { 51 | ProfileDesc desc; 52 | desc.uuid = profile.getUuid(); 53 | Internal::extractVersionNums(profile.getVersion(), desc); 54 | result.profileDescs.push_back(desc); 55 | } 56 | 57 | ret.push_back(result); 58 | } 59 | 60 | return ret; 61 | } 62 | -------------------------------------------------------------------------------- /src/sockets/delegates/macos/bidirectional.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "sockets/delegates/bidirectional.hpp" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | #include "net/enums.hpp" 14 | #include "os/async.hpp" 15 | #include "os/bluetooth.hpp" 16 | #include "os/errcheck.hpp" 17 | #include "os/error.hpp" 18 | #include "utils/task.hpp" 19 | 20 | template <> 21 | Task<> Delegates::Bidirectional::send(std::string data) { 22 | co_await Async::run([this](Async::CompletionResult& result) { 23 | Async::submit(Async::Send{ { *handle, &result } }); 24 | }); 25 | 26 | check(::send(*handle, data.data(), data.size(), 0)); 27 | } 28 | 29 | template <> 30 | Task Delegates::Bidirectional::recv(std::size_t size) { 31 | co_await Async::run([this](Async::CompletionResult& result) { 32 | Async::submit(Async::Receive{ { *handle, &result } }); 33 | }); 34 | 35 | std::string data(size, 0); 36 | auto recvLen = check(::recv(*handle, data.data(), data.size(), 0)); 37 | 38 | if (recvLen == 0) co_return { true, true, "", std::nullopt }; 39 | 40 | data.resize(recvLen); 41 | co_return { true, false, data, std::nullopt }; 42 | } 43 | 44 | template <> 45 | Task<> Delegates::Bidirectional::send(std::string data) { 46 | check((*handle)->write(data), checkZero, useReturnCode, System::ErrorType::IOReturn); 47 | co_await Async::run(std::bind_front(AsyncBT::submit, (*handle)->getHash(), IOType::Send), 48 | System::ErrorType::IOReturn); 49 | } 50 | 51 | template <> 52 | Task Delegates::Bidirectional::recv(std::size_t) { 53 | co_await Async::run(std::bind_front(AsyncBT::submit, (*handle)->getHash(), IOType::Receive), 54 | System::ErrorType::IOReturn); 55 | 56 | auto readResult = AsyncBT::getReadResult((*handle)->getHash()); 57 | co_return readResult ? RecvResult{ true, false, *readResult, std::nullopt } 58 | : RecvResult{ true, true, "", std::nullopt }; 59 | } 60 | -------------------------------------------------------------------------------- /src/sockets/delegates/macos/client.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "sockets/delegates/client.hpp" 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | #include "net/device.hpp" 13 | #include "net/enums.hpp" 14 | #include "net/netutils.hpp" 15 | #include "os/async.hpp" 16 | #include "os/bluetooth.hpp" 17 | #include "os/errcheck.hpp" 18 | #include "os/error.hpp" 19 | 20 | template <> 21 | Task<> Delegates::Client::connect(Device device) { 22 | auto addr = NetUtils::resolveAddr(device); 23 | 24 | co_await NetUtils::loopWithAddr(addr.get(), [this](const AddrInfoType* result) -> Task<> { 25 | handle.reset(check(::socket(result->ai_family, result->ai_socktype, result->ai_protocol))); 26 | 27 | Async::prepSocket(*handle); 28 | 29 | // Start connect 30 | check(::connect(*handle, result->ai_addr, result->ai_addrlen)); 31 | co_await Async::run([this](Async::CompletionResult& result) { 32 | Async::submit(Async::Connect{ { *handle, &result } }); 33 | }); 34 | }); 35 | } 36 | 37 | template <> 38 | Task<> Delegates::Client::connect(Device device) { 39 | bool isL2CAP; 40 | 41 | using enum ConnectionType; 42 | switch (device.type) { 43 | case L2CAP: 44 | isL2CAP = true; 45 | break; 46 | case RFCOMM: 47 | isL2CAP = false; 48 | break; 49 | default: 50 | std::unreachable(); 51 | } 52 | 53 | // Init handle 54 | auto newHandle = check( 55 | BluetoothMacOS::makeBTHandle(device.address, device.port, isL2CAP), 56 | [](const BluetoothMacOS::BTHandleResult& result) { return result.getResult() == kIOReturnSuccess; }, 57 | [](const BluetoothMacOS::BTHandleResult& result) { return result.getResult(); }, System::ErrorType::IOReturn) 58 | .getHandle()[0]; 59 | 60 | handle.reset(newHandle); 61 | co_await Async::run(std::bind_front(AsyncBT::submit, (*handle)->getHash(), IOType::Send), 62 | System::ErrorType::IOReturn); 63 | } 64 | -------------------------------------------------------------------------------- /paper/paper.bib: -------------------------------------------------------------------------------- 1 | @article{Elgazzar_Khalil_Alghamdi_Badr_Abdelkader_Elewah_Buyya_2022, 2 | title = {Revisiting the internet of things: New trends, opportunities and grand challenges}, 3 | url = {https://www.frontiersin.org/journals/the-internet-of-things/articles/10.3389/friot.2022.1073780/full}, 4 | journal = {Frontiers in the Internet of Things}, 5 | publisher = {Frontiers}, 6 | author = {Elgazzar, Khalid et al.}, 7 | year = {2022}, 8 | month = nov, 9 | doi = {10.3389/friot.2022.1073780} 10 | } 11 | 12 | @article{Kumar_Tiwari_Zymbler_2019, 13 | title = {Internet of things is a revolutionary approach for future technology enhancement: A Review}, 14 | url = {https://journalofbigdata.springeropen.com/articles/10.1186/s40537-019-0268-2}, 15 | journal = {Journal of Big Data}, 16 | publisher = {Springer International Publishing}, 17 | author = {Kumar, Sachin and Tiwari, Prayag and Zymbler, Mikhail}, 18 | year = {2019}, 19 | month = dec, 20 | doi = {10.1186/s40537-019-0268-2} 21 | } 22 | 23 | @article{IO_Completion_Ports, 24 | title = {{I/O} Completion Ports}, 25 | url = {https://learn.microsoft.com/en-us/windows/win32/fileio/i-o-completion-ports}, 26 | publisher = {Microsoft Learn}, 27 | author = {Ashcraft, Alvin et al.}, 28 | year = {2022}, 29 | month = aug 30 | } 31 | 32 | @article{kqueue, 33 | title = {{Mac OS X} Manual Page For kqueue(2)}, 34 | url = {https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/kqueue.2.html}, 35 | publisher = {Apple Developer}, 36 | author = {Lemon, Jonathan}, 37 | year = {2000}, 38 | month = apr 39 | } 40 | 41 | @article{IOBluetooth, 42 | title = {IOBluetooth}, 43 | url = {https://developer.apple.com/documentation/iobluetooth}, 44 | publisher = {Apple Developer}, 45 | author = {{Apple Inc.}} 46 | } 47 | 48 | @software{Axboe_liburing_library_for, 49 | author = {Axboe, Jens}, 50 | title = {{liburing}}, 51 | url = {https://github.com/axboe/liburing} 52 | } 53 | 54 | @article{Coroutines_Cpp20, 55 | title = {Coroutines ({C}++20)}, 56 | url = {https://en.cppreference.com/w/cpp/language/coroutines}, 57 | publisher = {cppreference}, 58 | author = {{cppreference Contributors}}, 59 | year = {2024}, 60 | month = jun 61 | } 62 | -------------------------------------------------------------------------------- /src/components/serverwindow.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | 9 | #include "console.hpp" 10 | #include "ioconsole.hpp" 11 | #include "window.hpp" 12 | #include "net/device.hpp" 13 | #include "sockets/delegates/delegates.hpp" 14 | #include "sockets/socket.hpp" 15 | #include "utils/task.hpp" 16 | 17 | // Handles a server socket in a GUI window. 18 | class ServerWindow : public Window { 19 | // Connection-oriented client. 20 | struct Client { 21 | SocketPtr socket; 22 | Console console; 23 | int colorIndex; 24 | bool selected = true; 25 | bool opened = false; 26 | bool remove = false; 27 | bool pendingRecv = false; 28 | bool connected = true; 29 | 30 | Client(SocketPtr&& socket, int colorIndex) : socket(std::move(socket)), colorIndex(colorIndex) {} 31 | 32 | ~Client() { 33 | if (socket) socket->cancelIO(); 34 | } 35 | 36 | Task<> recv(IOConsole& serverConsole, const Device& device, unsigned int size); 37 | }; 38 | 39 | // Device comparator functor for std::map. Using a struct to avoid -Wsubobject-linkage on GCC. 40 | struct CompDevices { 41 | bool operator()(const Device& d1, const Device& d2) const { 42 | return d1.address < d2.address || d1.port < d2.port; 43 | } 44 | }; 45 | 46 | SocketPtr socket; 47 | std::map clients; 48 | bool isDgram; 49 | 50 | bool pendingIO = false; 51 | int colorIndex = 0; 52 | 53 | IOConsole console; 54 | std::string clientsWindowTitle; 55 | 56 | void startServer(const Device& serverInfo); 57 | 58 | // Accepts connection-oriented clients. 59 | Task<> accept(); 60 | 61 | // Receives from datagram-oriented clients. 62 | Task<> recvDgram(); 63 | 64 | // Selects the next color to display clients in. 65 | void nextColor(); 66 | 67 | // Draws the window containing the list of clients. 68 | void drawClientsWindow(); 69 | 70 | void onBeforeUpdate() override; 71 | 72 | void onUpdate() override; 73 | 74 | public: 75 | explicit ServerWindow(std::string_view title, const Device& serverInfo); 76 | 77 | ~ServerWindow() override; 78 | }; 79 | -------------------------------------------------------------------------------- /.github/workflows/build-macos.yml: -------------------------------------------------------------------------------- 1 | name: macOS build 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | mode: 7 | type: string 8 | required: true 9 | 10 | jobs: 11 | build-macos: 12 | runs-on: macos-latest 13 | env: 14 | XCODE_VERSION: "16.1" 15 | XMAKE_GLOBALDIR: ${{ github.workspace }}/xmake-global 16 | 17 | steps: 18 | - name: Setup Xcode 19 | id: xcode 20 | run: | 21 | sudo xcodes select ${{ env.XCODE_VERSION }} 22 | echo "xcode_path=$(dirname $(dirname $(xcodes select ${{ env.XCODE_VERSION }} -p)))" >> $GITHUB_OUTPUT 23 | 24 | - name: Get current date as package key 25 | id: cache_key 26 | run: echo "key=$(date +'%W')" >> $GITHUB_OUTPUT 27 | 28 | - name: Checkout repository 29 | uses: actions/checkout@v4 30 | 31 | - name: Setup xmake 32 | uses: xmake-io/github-action-setup-xmake@v1 33 | with: 34 | xmake-version: '2.9.5' 35 | actions-cache-folder: .xmake-cache-W${{ steps.cache_key.outputs.key }} 36 | 37 | - name: Update xmake repository 38 | run: xmake repo --update 39 | 40 | - name: Retrieve dependencies hash 41 | id: dep_hash 42 | run: echo "hash=$(xmake l utils.ci.packageskey)" >> $GITHUB_OUTPUT 43 | 44 | - name: Retrieve cached xmake dependencies 45 | uses: actions/cache@v4 46 | with: 47 | path: ${{ env.XMAKE_GLOBALDIR }}/.xmake/packages 48 | key: macOS-${{ steps.dep_hash.outputs.hash }}-W${{ steps.cache_key.outputs.key }} 49 | 50 | - name: Configure xmake 51 | run: xmake f -m ${{ inputs.mode }} --ccache=n -y --target_minver=14.0 --xcode=${{ steps.xcode.outputs.xcode_path }} 52 | 53 | - name: Build 54 | run: | 55 | xmake build swift 56 | xmake 57 | 58 | - name: Install 59 | if: inputs.mode == 'release' 60 | run: | 61 | xmake i -o dist/WhaleConnect.app WhaleConnect 62 | chmod +x dist/WhaleConnect.app/Contents/MacOS/WhaleConnect 63 | 64 | - name: Create DMG 65 | if: inputs.mode == 'release' 66 | run: | 67 | brew install create-dmg 68 | create-dmg --volname WhaleConnect WhaleConnect.dmg dist 69 | 70 | - name: Upload 71 | if: inputs.mode == 'release' 72 | uses: actions/upload-artifact@v4 73 | with: 74 | name: WhaleConnect-macos 75 | path: WhaleConnect.dmg 76 | -------------------------------------------------------------------------------- /src/utils/strings.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include 5 | #include 6 | 7 | #if OS_WINDOWS 8 | #include 9 | #endif 10 | 11 | #include "strings.hpp" 12 | 13 | Strings::SysStr Strings::toSys(std::string_view from) { 14 | #if OS_WINDOWS 15 | // Nothing to convert in an empty string 16 | if (from.empty()) return {}; 17 | 18 | // Size of UTF-8 string in UTF-16 wide encoding 19 | int stringSize = MultiByteToWideChar(CP_UTF8, 0, from.data(), static_cast(from.size()), nullptr, 0); 20 | 21 | // Buffer to contain new string 22 | std::wstring buf(stringSize, '\0'); 23 | 24 | // Convert the string from UTF-8 and return the converted buffer 25 | MultiByteToWideChar(CP_UTF8, 0, from.data(), -1, buf.data(), stringSize); 26 | return buf; 27 | #else 28 | return Strings::SysStr{ from }; 29 | #endif 30 | } 31 | 32 | std::string Strings::fromSys(SysStrView from) { 33 | #if OS_WINDOWS 34 | // Nothing to convert in an empty string 35 | if (from.empty()) return {}; 36 | 37 | // Size of UTF-16 wide string in UTF-8 encoding 38 | int stringSize 39 | = WideCharToMultiByte(CP_UTF8, 0, from.data(), static_cast(from.size()), nullptr, 0, nullptr, nullptr); 40 | 41 | // Buffer to contain new string 42 | std::string buf(stringSize, '\0'); 43 | 44 | // Convert the string to UTF-8 and return the converted buffer 45 | WideCharToMultiByte(CP_UTF8, 0, from.data(), -1, buf.data(), stringSize, nullptr, nullptr); 46 | return buf; 47 | #else 48 | return std::string{ from }; 49 | #endif 50 | } 51 | 52 | std::string Strings::replaceAll(std::string str, std::string_view from, std::string_view to) { 53 | // Adapted from https://stackoverflow.com/a/3418285 54 | 55 | // Preliminary checks 56 | // 1. Nothing to replace in an empty string 57 | // 2. Can't replace an empty string 58 | // 3. If from and to are equal the function call becomes pointless 59 | if (str.empty() || from.empty() || from == to) return str; 60 | 61 | std::size_t start = 0; 62 | while ((start = str.find(from, start)) != std::string::npos) { 63 | str.replace(start, from.size(), to); 64 | start += to.size(); // In case 'to' contains 'from', like replacing 'x' with 'yx' 65 | } 66 | 67 | return str; 68 | } 69 | -------------------------------------------------------------------------------- /src/sockets/delegates/sockethandle.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | #include "delegates.hpp" 9 | #include "traits.hpp" 10 | 11 | namespace Delegates { 12 | // Manages close operations on sockets. 13 | template 14 | class SocketHandle : public HandleDelegate { 15 | using Handle = Traits::SocketHandleType; 16 | static constexpr auto invalidHandle = Traits::invalidSocketHandle(); 17 | 18 | Handle handle; 19 | bool closed = false; 20 | 21 | void closeImpl(); 22 | 23 | public: 24 | SocketHandle() : SocketHandle(invalidHandle) {} 25 | 26 | explicit SocketHandle(Handle handle) : handle(handle) {} 27 | 28 | // Closes the socket. 29 | ~SocketHandle() override { 30 | close(); 31 | } 32 | 33 | SocketHandle(const SocketHandle&) = delete; 34 | 35 | // Constructs an object and transfers ownership from another object. 36 | SocketHandle(SocketHandle&& other) noexcept : handle(other.release()) {} 37 | 38 | SocketHandle& operator=(const SocketHandle&) = delete; 39 | 40 | // Transfers ownership from another object. 41 | SocketHandle& operator=(SocketHandle&& other) noexcept { 42 | reset(other.release()); 43 | return *this; 44 | } 45 | 46 | void close() override { 47 | if (!closed && isValid()) { 48 | closeImpl(); 49 | closed = true; 50 | } 51 | } 52 | 53 | bool isValid() override { 54 | return handle != Traits::invalidSocketHandle(); 55 | } 56 | 57 | void cancelIO() override; 58 | 59 | // Closes the current handle and acquires a new one. 60 | void reset(Handle other = invalidHandle) noexcept { 61 | close(); 62 | handle = other; 63 | } 64 | 65 | // Releases ownership of the managed handle. 66 | Handle release() { 67 | return std::exchange(handle, invalidHandle); 68 | } 69 | 70 | // Accesses the handle. 71 | const Handle& operator*() const { 72 | return handle; 73 | } 74 | 75 | Handle& operator*() { 76 | return handle; 77 | } 78 | }; 79 | } 80 | -------------------------------------------------------------------------------- /src/sockets/delegates/secure/clienttls.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | #include "net/device.hpp" 14 | #include "net/enums.hpp" 15 | #include "sockets/delegates/bidirectional.hpp" 16 | #include "sockets/delegates/client.hpp" 17 | #include "sockets/delegates/delegates.hpp" 18 | #include "sockets/delegates/sockethandle.hpp" 19 | #include "utils/task.hpp" 20 | 21 | namespace Delegates { 22 | // Manages operations on TLS client sockets. 23 | class ClientTLS : public HandleDelegate, public ClientDelegate, public IODelegate { 24 | std::unique_ptr channel; 25 | SocketHandle handle; 26 | Client baseClient{ handle }; 27 | Bidirectional baseIO{ handle }; 28 | 29 | std::queue completedReads; 30 | std::queue pendingWrites; 31 | 32 | // Sends all encrypted TLS data over the socket. 33 | Task<> sendQueued(); 34 | 35 | public: 36 | ~ClientTLS() { 37 | close(); 38 | } 39 | 40 | // Receives raw TLS data and passes it to the internal channel. 41 | Task recvBase(std::size_t size); 42 | 43 | void queueRead(std::string data) { 44 | completedReads.push({ true, false, data, std::nullopt }); 45 | } 46 | 47 | void setAlert(Botan::TLS::Alert newAlert) { 48 | TLSAlert alert{ newAlert.type_string(), newAlert.is_fatal() }; 49 | 50 | if (completedReads.empty()) completedReads.push({ true, false, "", alert }); 51 | else completedReads.back().alert = alert; 52 | } 53 | 54 | void queueWrite(std::string data) { 55 | pendingWrites.push(data); 56 | } 57 | 58 | void close() override; 59 | 60 | bool isValid() override { 61 | return handle.isValid() && channel->is_active(); 62 | } 63 | 64 | void cancelIO() override { 65 | handle.cancelIO(); 66 | } 67 | 68 | Task<> connect(Device device) override; 69 | 70 | Task<> send(std::string data) override; 71 | 72 | Task recv(std::size_t size) override; 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /src/gui/imguiext.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "imguiext.hpp" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | int stringCallback(ImGuiInputTextCallbackData* data) { 13 | // Get user data (assume it's a string pointer) 14 | auto& str = *reinterpret_cast(data->UserData); 15 | 16 | // Resize the string, then set the callback data buffer 17 | str.resize(data->BufTextLen); 18 | data->Buf = str.data(); 19 | return 0; 20 | } 21 | 22 | bool ImGuiExt::inputText(std::string_view label, std::string& s, ImGuiInputTextFlags flags) { 23 | flags |= ImGuiInputTextFlags_CallbackResize; 24 | return ImGui::InputText(label.data(), s.data(), s.capacity() + 1, flags, stringCallback, &s); 25 | } 26 | 27 | bool ImGuiExt::inputTextMultiline(std::string_view label, std::string& s, const ImVec2& size, 28 | ImGuiInputTextFlags flags) { 29 | flags |= ImGuiInputTextFlags_CallbackResize; 30 | return ImGui::InputTextMultiline(label.data(), s.data(), s.capacity() + 1, size, flags, stringCallback, &s); 31 | } 32 | 33 | void ImGuiExt::helpMarker(std::string_view desc) { 34 | // Adapted from imgui_demo.cpp. 35 | ImGui::SameLine(); 36 | ImGui::TextDisabled("(?)"); 37 | if (!ImGui::IsItemHovered()) return; 38 | 39 | ImGui::BeginTooltip(); 40 | ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); 41 | textUnformatted(desc); 42 | ImGui::PopTextWrapPos(); 43 | ImGui::EndTooltip(); 44 | } 45 | 46 | void ImGuiExt::spinner() { 47 | // Text settings 48 | float textSizeHalf = ImGui::GetTextLineHeight() / 2; 49 | ImU32 textColor = ImGui::GetColorU32(ImGuiCol_Text); 50 | 51 | // Current time (multiplied to make the spinner faster) 52 | auto time = static_cast(ImGui::GetTime() * 10); 53 | 54 | // Position to draw the spinner 55 | ImVec2 cursorPos = ImGui::GetCursorScreenPos(); 56 | ImVec2 center{ cursorPos.x + textSizeHalf, cursorPos.y + textSizeHalf }; 57 | 58 | // Draw the spinner, arc from 0 radians to (3pi / 2) radians (270 degrees) 59 | constexpr auto arcLength = static_cast(std::numbers::pi * (3.0 / 2.0)); 60 | ImDrawList& drawList = *ImGui::GetWindowDrawList(); 61 | drawList.PathArcTo(center, textSizeHalf, time, time + arcLength); 62 | drawList.PathStroke(textColor, 0, textSizeHalf / 2); 63 | } 64 | -------------------------------------------------------------------------------- /src/app/fs.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include 5 | 6 | #if OS_WINDOWS 7 | #include 8 | #include 9 | #include 10 | 11 | #include "utils/handleptr.hpp" 12 | #elif OS_MACOS || OS_LINUX 13 | #include 14 | #include 15 | 16 | #include 17 | #include 18 | 19 | #if OS_MACOS 20 | #include 21 | #endif 22 | #endif 23 | 24 | #include "fs.hpp" 25 | 26 | [[noreturn]] void throwBasePathError() { 27 | throw std::runtime_error{ "Failed to get executable path" }; 28 | } 29 | 30 | [[noreturn]] void throwSettingsPathError() { 31 | throw std::runtime_error{ "Failed to get settings path" }; 32 | } 33 | 34 | fs::path AppFS::getBasePath() { 35 | // First get the path to the executable itself 36 | #if OS_WINDOWS 37 | std::wstring path(MAX_PATH, 0); 38 | if (GetModuleFileNameW(nullptr, path.data(), static_cast(path.size())) == 0) throwBasePathError(); 39 | #elif OS_MACOS 40 | std::string path(PATH_MAX, 0); 41 | std::uint32_t size = path.size(); 42 | if (_NSGetExecutablePath(path.data(), &size) == -1) throwBasePathError(); 43 | 44 | // Return path to Resources if inside app bundle 45 | if (path.contains(".app/Contents")) return std::filesystem::path{ path }.parent_path().parent_path() / "Resources"; 46 | #elif OS_LINUX 47 | std::string path(PATH_MAX, 0); 48 | if (readlink("/proc/self/exe", path.data(), path.size()) == -1) throwBasePathError(); 49 | #endif 50 | 51 | // Return the containing directory 52 | return std::filesystem::path{ path }.parent_path(); 53 | } 54 | 55 | fs::path AppFS::getSettingsPath() { 56 | #if OS_WINDOWS 57 | HandlePtr dataPathPtr; 58 | if (SHGetKnownFolderPath(FOLDERID_RoamingAppData, KF_FLAG_CREATE, nullptr, ztd::out_ptr::out_ptr(dataPathPtr)) 59 | != S_OK) 60 | throwSettingsPathError(); 61 | 62 | std::filesystem::path dataPath{ dataPathPtr.get() }; 63 | #elif OS_MACOS || OS_LINUX 64 | const char* homeDir = std::getenv("HOME"); 65 | if (!homeDir) { 66 | if (passwd* pwd = getpwuid(getuid()); pwd) homeDir = pwd->pw_dir; 67 | else throwSettingsPathError(); 68 | } 69 | 70 | #if OS_MACOS 71 | auto dataPath = std::filesystem::path{ homeDir } / "Library" / "Application Support"; 72 | #else 73 | auto dataPath = std::filesystem::path{ homeDir } / ".config"; 74 | #endif 75 | #endif 76 | 77 | auto path = dataPath / "WhaleConnect"; 78 | if (!std::filesystem::exists(path)) std::filesystem::create_directories(path); 79 | return path; 80 | } 81 | -------------------------------------------------------------------------------- /src/sockets/delegates/delegates.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "net/device.hpp" 12 | #include "net/enums.hpp" 13 | #include "utils/task.hpp" 14 | 15 | class Socket; 16 | 17 | using SocketPtr = std::unique_ptr; 18 | 19 | struct TLSAlert { 20 | std::string desc; 21 | bool isFatal; 22 | }; 23 | 24 | struct RecvResult { 25 | bool complete; 26 | bool closed; 27 | std::string data; 28 | std::optional alert; 29 | }; 30 | 31 | struct AcceptResult { 32 | Device device; 33 | SocketPtr socket; 34 | }; 35 | 36 | struct DgramRecvResult { 37 | Device from; 38 | std::string data; 39 | }; 40 | 41 | struct ServerAddress { 42 | std::uint16_t port = 0; 43 | IPType ipType = IPType::None; 44 | }; 45 | 46 | namespace Delegates { 47 | // Manages handle operations. 48 | struct HandleDelegate { 49 | virtual ~HandleDelegate() = default; 50 | 51 | // Closes the socket. 52 | virtual void close() = 0; 53 | 54 | // Checks if the socket is valid. 55 | virtual bool isValid() = 0; 56 | 57 | // Cancels all pending I/O. 58 | virtual void cancelIO() = 0; 59 | }; 60 | 61 | // Manages I/O operations. 62 | struct IODelegate { 63 | virtual ~IODelegate() = default; 64 | 65 | // Sends a string. 66 | // The data is passed as a string to make a copy and prevent dangling pointers in the coroutine. 67 | virtual Task<> send(std::string data) = 0; 68 | 69 | // Receives a string. 70 | virtual Task recv(std::size_t size) = 0; 71 | }; 72 | 73 | // Manages client operations. 74 | struct ClientDelegate { 75 | virtual ~ClientDelegate() = default; 76 | 77 | // Connects to a host. 78 | virtual Task<> connect(Device device) = 0; 79 | }; 80 | 81 | // Manages server operations. 82 | struct ServerDelegate { 83 | virtual ~ServerDelegate() = default; 84 | 85 | // Starts the server and returns server information. 86 | virtual ServerAddress startServer(const Device& serverInfo) = 0; 87 | 88 | // Accepts a client connection. 89 | virtual Task accept() = 0; 90 | 91 | // Receives data from a connectionless client. 92 | virtual Task recvFrom(std::size_t size) = 0; 93 | 94 | // Sends data to a connectionless client. 95 | virtual Task<> sendTo(Device device, std::string data) = 0; 96 | }; 97 | } 98 | -------------------------------------------------------------------------------- /docs/testing.md: -------------------------------------------------------------------------------- 1 | # Testing WhaleConnect 2 | 3 | WhaleConnect's tests are located in the [tests directory](/tests). Certain tests are designed to test socket I/O functionality, so a server and/or remote device is required to run them. 4 | 5 | Due to the nature of the tests (external script is required; actual hardware may be required), these tests are not run in GitHub Actions. 6 | 7 | To build the unit tests, execute `xmake build socket-tests` in the root of the repository. The unit tests use the [Catch2](https://github.com/catchorg/Catch2) testing framework (see its [command-line usage](https://github.com/catchorg/Catch2/blob/devel/docs/command-line.md)). 8 | 9 | ## Test Server 10 | 11 | A Python server script is located in `/tests/scripts`. It should be invoked with `-t [type]`, where `[type]` is the type of the server: `TCP`, `UDP`, `RFCOMM`, or `L2CAP`. 12 | 13 | Multiple instances of the script may be run simultaneously so e.g., both a TCP server and UDP server can be available during testing. 14 | 15 | The server may also be called with the following switches (mutually exclusive): 16 | 17 | - `-i` for interactive mode (accept data from the standard input to send to clients) 18 | - `-e` for echo mode (send back all data that it receives from the client) 19 | 20 | When using the server with the unit tests, use the `-e` switch. 21 | 22 | ## Server Device 23 | 24 | Some tests only need one device involved - for example, Internet Protocol tests can use the loopback address (`127.0.0.1` or `::1`) so they can be run on the same device as the server script. However, Bluetooth does not have such capabilities, so all Bluetooth tests must have a server running on a separate device. Also, Bluetooth sockets in Python may be limited on some platforms, so a Linux system is recommended for running Bluetooth servers. 25 | 26 | ## Settings 27 | 28 | Both the C++ tests and the Python server load configuration information from the same file. See [this document](/tests/settings/readme.md) for more information. 29 | 30 | > [!IMPORTANT] 31 | > If you have separate devices that are running the tests and server script, the same settings file must be available on both. 32 | 33 | ## Benchmarking 34 | 35 | A small HTTP server is located in `/tests/benchmarks`. It responds to clients with the following response: 36 | 37 | ```text 38 | HTTP/1.1 200 OK 39 | Connection: keep-alive 40 | Content-Length: 4 41 | Content-Type: text/html 42 | 43 | test 44 | ``` 45 | 46 | This server can be used to assess the performance of WhaleConnect's core system code through its throughput measurement. It can be built with `xmake build benchmark-server`. 47 | 48 | This server accepts an optional command-line argument: the size of the thread pool. If unspecified, it uses the maximum number of supported threads on the CPU. When started, the server prints the TCP port it is listening on. 49 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include 5 | #include 6 | 7 | #include "app/appcore.hpp" 8 | #include "app/settings.hpp" 9 | #include "components/windowlist.hpp" 10 | #include "gui/about.hpp" 11 | #include "gui/menu.hpp" 12 | #include "gui/menu.state.hpp" 13 | #include "gui/newconn.hpp" 14 | #include "gui/newserver.hpp" 15 | #include "gui/notifications.hpp" 16 | #include "net/btutils.hpp" 17 | #include "os/async.hpp" 18 | #include "os/error.hpp" 19 | 20 | // Contains the app's core logic and functions. 21 | void mainLoop() { 22 | // These variables must be in a separate scope from the resource instances, so these can be destructed before 23 | // cleanup 24 | WindowList connections; // Open windows 25 | WindowList sdpWindows; // Windows for creating Bluetooth connections 26 | WindowList servers; // Servers 27 | 28 | bool quit = false; 29 | while (!quit && AppCore::newFrame()) { 30 | // Handle event loop without waiting, waiting is done by vsync 31 | Async::handleEvents(false); 32 | Menu::drawMenuBar(quit, connections, servers); 33 | 34 | // Application windows 35 | Settings::drawSettingsWindow(Menu::settingsOpen); 36 | drawNewConnectionWindow(Menu::newConnectionOpen, connections, sdpWindows); 37 | drawNewServerWindow(servers, Menu::newServerOpen); 38 | ImGuiExt::drawNotificationsWindow(Menu::notificationsOpen); 39 | drawAboutWindow(Menu::aboutOpen); 40 | drawLinksWindow(Menu::linksOpen); 41 | 42 | connections.update(); 43 | servers.update(); 44 | sdpWindows.update(); 45 | AppCore::render(); 46 | } 47 | } 48 | 49 | #if OS_WINDOWS 50 | int WinMain(HINSTANCE, HINSTANCE, LPSTR, int) 51 | #else 52 | int main(int, char**) 53 | #endif 54 | { 55 | // Create a main application window 56 | if (!AppCore::init()) return 1; 57 | if (Settings::GUI::systemMenu) Menu::setupMenuBar(); 58 | 59 | // OS API resource instances 60 | std::optional btutilsInstance; 61 | 62 | using namespace std::literals; 63 | 64 | // Initialize APIs for sockets and Bluetooth 65 | try { 66 | Async::init(Settings::OS::numThreads, Settings::OS::queueEntries); 67 | btutilsInstance.emplace(); 68 | } catch (const System::SystemError& error) { 69 | ImGuiExt::addNotification("Initialization error "s + error.what(), NotificationType::Error, 0); 70 | } catch (const std::system_error&) { 71 | ImGuiExt::addNotification("Initialization error: Could not initialize thread pool", NotificationType::Error, 0); 72 | } 73 | 74 | // Run app 75 | mainLoop(); 76 | 77 | AppCore::cleanup(); 78 | Async::cleanup(); 79 | return 0; 80 | } 81 | -------------------------------------------------------------------------------- /xmake/download.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | -- SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | function downloadLicenses(installdir) 5 | print("Downloading licenses...") 6 | 7 | -- Licenses are installed to Resources in macOS bundle 8 | local targetDir = is_plat("macosx") and path.join(installdir, "Contents", "Resources") or installdir 9 | 10 | local licenseFiles = { 11 | -- Libraries 12 | ["bluez"] = { "https://raw.githubusercontent.com/bluez/bluez/master/LICENSES/preferred/GPL-2.0" }, 13 | ["botan"] = { "https://raw.githubusercontent.com/randombit/botan/master/license.txt" }, 14 | ["catch2"] = { "https://raw.githubusercontent.com/catchorg/Catch2/devel/LICENSE.txt" }, 15 | ["dear_imgui"] = { "https://raw.githubusercontent.com/ocornut/imgui/master/LICENSE.txt" }, 16 | ["glfw"] = { "https://raw.githubusercontent.com/glfw/glfw/master/LICENSE.md" }, 17 | ["imguitextselect"] = { "https://raw.githubusercontent.com/AidanSun05/ImGuiTextSelect/main/LICENSE.txt" }, 18 | ["libdbus"] = { "https://gitlab.freedesktop.org/dbus/dbus/-/raw/master/LICENSES/GPL-2.0-or-later.txt" }, 19 | ["liburing"] = { 20 | "https://raw.githubusercontent.com/axboe/liburing/master/LICENSE", 21 | "https://raw.githubusercontent.com/axboe/liburing/master/COPYING", 22 | "https://raw.githubusercontent.com/axboe/liburing/master/COPYING.GPL" 23 | }, 24 | ["utfcpp"] = { "https://raw.githubusercontent.com/nemtrif/utfcpp/master/LICENSE" }, 25 | ["ztd.out_ptr"] = { "https://raw.githubusercontent.com/soasis/out_ptr/main/LICENSE" }, 26 | 27 | -- Fonts 28 | ["noto_sans_mono"] = { "https://raw.githubusercontent.com/notofonts/noto-fonts/main/LICENSE" }, 29 | ["remix_icon"] = { "https://raw.githubusercontent.com/Remix-Design/RemixIcon/master/License" } 30 | } 31 | 32 | local http = import("net.http") 33 | for name, urls in pairs(licenseFiles) do 34 | for _, url in ipairs(urls) do 35 | local filename = path.filename(url) 36 | local targetPath = path.join(targetDir, "3rdparty", name, filename) 37 | 38 | if not os.isfile(targetPath) then 39 | print("- %s -> %s", url, targetPath) 40 | http.download(url, targetPath) 41 | end 42 | end 43 | end 44 | end 45 | 46 | function downloadFonts(targetdir) 47 | local fontPath = path.join(targetdir, "NotoSansMono-Regular.ttf") 48 | local iconFontPath = path.join(targetdir, "remixicon.ttf") 49 | 50 | if not os.isfile(fontPath) then 51 | os.ln(os.getenv("NOTO_SANS_MONO_PATH"), fontPath) 52 | end 53 | 54 | if not os.isfile(iconFontPath) then 55 | os.ln(os.getenv("REMIX_ICON_PATH"), iconFontPath) 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /src/net/netutils.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | 9 | #if OS_WINDOWS 10 | #include 11 | #include 12 | #else 13 | #include 14 | #include 15 | #endif 16 | 17 | #include "device.hpp" 18 | #include "enums.hpp" 19 | #include "os/error.hpp" 20 | #include "sockets/delegates/delegates.hpp" 21 | #include "sockets/delegates/sockethandle.hpp" 22 | #include "sockets/delegates/traits.hpp" 23 | #include "utils/handleptr.hpp" 24 | #include "utils/task.hpp" 25 | 26 | // Winsock-specific definitions and their Berkeley equivalents 27 | #if OS_WINDOWS 28 | using AddrInfoType = ADDRINFOW; 29 | using AddrInfoHandle = HandlePtr; 30 | #else 31 | using AddrInfoType = addrinfo; 32 | using AddrInfoHandle = HandlePtr; 33 | #endif 34 | 35 | namespace NetUtils { 36 | // Resolves an address with getaddrinfo. 37 | AddrInfoHandle resolveAddr(const Device& device, bool useDNS = true); 38 | 39 | // Loops through a getaddrinfo result. 40 | template 41 | requires std::same_as, Task<>> 42 | Task<> loopWithAddr(const AddrInfoType* addr, Fn fn) { 43 | std::exception_ptr lastException; 44 | for (auto result = addr; result; result = result->ai_next) { 45 | try { 46 | co_await fn(result); 47 | co_return; 48 | } catch (const System::SystemError& e) { 49 | lastException = std::current_exception(); 50 | 51 | // Leave loop if operation was canceled 52 | if (e.isCanceled()) break; 53 | } 54 | } 55 | 56 | std::rethrow_exception(lastException); 57 | } 58 | 59 | // Loops through a getaddrinfo result. 60 | template 61 | void loopWithAddr(const AddrInfoType* addr, Fn fn) { 62 | std::exception_ptr lastException; 63 | for (auto result = addr; result; result = result->ai_next) { 64 | try { 65 | fn(result); 66 | return; 67 | } catch (const System::SystemError&) { 68 | lastException = std::current_exception(); 69 | } 70 | } 71 | 72 | std::rethrow_exception(lastException); 73 | } 74 | 75 | // Returns address information with getnameinfo. 76 | Device fromAddr(const sockaddr* addr, socklen_t addrLen, ConnectionType type); 77 | 78 | // Returns the port from a sockaddr. 79 | std::uint16_t getPort(Traits::SocketHandleType handle, bool isV4); 80 | 81 | // Starts a server with the specified socket handle. 82 | ServerAddress startServer(const Device& serverInfo, Delegates::SocketHandle& handle); 83 | } 84 | -------------------------------------------------------------------------------- /src/gui/newconnip.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "newconnip.hpp" 5 | 6 | #include 7 | #include 8 | 9 | #include "imguiext.hpp" 10 | #include "newconn.hpp" 11 | #include "components/windowlist.hpp" 12 | #include "net/enums.hpp" 13 | 14 | // Gets the width of a rendered string added with the item inner spacing specified in the Dear ImGui style. 15 | float calcTextWidthWithSpacing(std::string_view text) { 16 | return ImGui::GetStyle().ItemInnerSpacing.x + ImGui::CalcTextSize(text.data()).x; 17 | } 18 | 19 | void drawIPConnectionTab(WindowList& connections) { 20 | if (!ImGui::BeginTabItem("Internet Protocol")) return; 21 | ImGui::BeginChild("Output"); 22 | 23 | using enum ConnectionType; 24 | 25 | static std::string addr; // Server address 26 | static std::uint16_t port = 0; // Server port 27 | static ConnectionType type = TCP; // Type of connection to create 28 | static bool useTLS = false; // If TLS is used for secure connections 29 | 30 | // Widgets 31 | using namespace ImGuiExt::Literals; 32 | 33 | static const char* portLabel = "Port"; 34 | static const char* addressLabel = "Address"; 35 | static const float portWidth = 7_fh; // Port input width (hardcoded) 36 | static const float minAddressWidth = 10_fh; // Address input min width 37 | 38 | // The horizontal space available in the window 39 | float emptySpace = ImGui::GetContentRegionAvail().x // Child window width without scrollbars 40 | - calcTextWidthWithSpacing(addressLabel) // Address input label width 41 | - ImGui::GetStyle().ItemSpacing.x // Space between address and port inputs 42 | - calcTextWidthWithSpacing(portLabel) // Port input label width 43 | - portWidth; // Port input width 44 | 45 | // Server address, set the textbox width to the space not taken up by everything else 46 | // Use std::max to set a minimum size for the texbox; it will not resize past a certain min bound. 47 | ImGui::SetNextItemWidth(std::max(emptySpace, minAddressWidth)); 48 | ImGuiExt::inputText(addressLabel, addr); 49 | 50 | // Server port, keep it on the same line as the textbox if there's enough space 51 | if (emptySpace > minAddressWidth) ImGui::SameLine(); 52 | ImGui::SetNextItemWidth(portWidth); 53 | ImGuiExt::inputScalar(portLabel, port, 1, 10); 54 | 55 | // Connection type selection 56 | ImGuiExt::radioButton("TCP", type, TCP); 57 | ImGuiExt::radioButton("UDP", type, UDP); 58 | 59 | // Connect button 60 | ImGui::Spacing(); 61 | ImGui::BeginDisabled(addr.empty()); 62 | 63 | if (ImGui::Button("Connect")) addConnWindow(connections, useTLS, { type, "", addr, port }, ""); 64 | 65 | ImGui::EndDisabled(); 66 | 67 | // Option to use TLS (TCP only) 68 | if (type == TCP) { 69 | ImGui::SameLine(); 70 | ImGui::Checkbox("Use TLS", &useTLS); 71 | } 72 | 73 | ImGui::EndChild(); 74 | ImGui::EndTabItem(); 75 | } 76 | -------------------------------------------------------------------------------- /src/components/sdpwindow.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "window.hpp" 13 | #include "windowlist.hpp" 14 | #include "net/btutils.hpp" 15 | #include "net/device.hpp" 16 | #include "net/enums.hpp" 17 | #include "os/error.hpp" 18 | 19 | // Handles an SDP inquiry in a GUI window. 20 | class SDPWindow : public Window { 21 | using AsyncSDPInquiry = std::future>; // Results of an SDP search 22 | 23 | Device target; // Target to perform SDP inquiries on and connect to 24 | 25 | // Fields for SDP connections 26 | std::size_t selectedUUID = 0; // UUID selected for an inquiry 27 | bool flushCache = false; // If data should be flushed on the next inquiry 28 | std::string serviceName; // Service name of the selected SDP result, displayed in the connection window title 29 | 30 | // Fields for SDP and manual connection state 31 | ConnectionType connType = ConnectionType::RFCOMM; // Selected connection type 32 | std::uint16_t connPort = 0; // Port to connect to 33 | 34 | // Fields for connection management 35 | WindowList& list; // List of connection window objects to add to when making a new connection 36 | 37 | // SDP inquiry data 38 | // The value this variant currently holds contains data about the SDP inquiry. 39 | // The type of the value represents the inquiry's state. 40 | std::variant // The results of the inquiry when it has completed 45 | > 46 | sdpInquiry; 47 | 48 | // Draws the entries from an SDP lookup with buttons to connect to each in a tree format. 49 | bool drawSDPList(const std::vector& list); 50 | 51 | // Draws the options for connecting to a device with Bluetooth. 52 | void drawConnOptions(std::string_view info); 53 | 54 | // Draws information about the SDP inquiry. 55 | void checkInquiryStatus(); 56 | 57 | // Draws the tab to initiate an SDP inquiry. 58 | void drawSDPTab(); 59 | 60 | // Draws the tab to initiate a connection without SDP. 61 | void drawManualTab(); 62 | 63 | // Checks the status of the inquiry and prevents closing the window if it is running. 64 | void onBeforeUpdate() override; 65 | 66 | // Draws the window contents. 67 | void onUpdate() override; 68 | 69 | public: 70 | // Sets the information needed to create connections. 71 | SDPWindow(std::string_view title, const Device& target, WindowList& list) : 72 | Window(title), target(target), list(list) {} 73 | }; 74 | -------------------------------------------------------------------------------- /xmake/scripts/parse_lock_file.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | -- SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | -- This script parses the xrepo lock file and transforms it into a JSON file. 5 | 6 | import("core.base.json") 7 | import("core.base.semver") 8 | 9 | local allPackages = {} 10 | 11 | function recordPackages(packages, osKey) 12 | for package, info in pairs(packages) do 13 | local name = package:match("^([%w_%-]+)") 14 | if allPackages[name] == nil then 15 | local version = info["version"] 16 | local parsedVersion = semver.is_valid(version) and semver.new(version):shortstr() or version 17 | 18 | local online = info["repo"]["url"]:match("^https://") 19 | 20 | local repoRoot = online 21 | and path.join(val("globaldir"), "repositories", "xmake-repo") 22 | or path.join(val("projectdir"), "xmake") 23 | 24 | allPackages[name] = { 25 | filepath = path.join(repoRoot, "packages", name:sub(1, 1), name, "xmake.lua"), 26 | os = {} 27 | } 28 | 29 | local prevVersion = allPackages[name]["version"] 30 | if prevVersion == nil or (semver.is_valid(prevVersion) and semver.compare(parsedVersion, prevVersion) == 1) then 31 | allPackages[name]["version"] = parsedVersion 32 | end 33 | end 34 | 35 | table.insert(allPackages[name]["os"], osKey) 36 | end 37 | end 38 | 39 | function recordReferencedPackages() 40 | local xmakeFilePath = path.join(os.projectdir(), "xmake.lua") 41 | local packages = {} 42 | 43 | local lines = io.lines(xmakeFilePath) 44 | for line in lines do 45 | local package = line:match("add_packages%((.+)%)") 46 | if package then 47 | local args = package:split(",") 48 | for _, arg in ipairs(args) do 49 | local name = arg:split("\"", { plain = true, strict = true })[2] 50 | packages[#packages + 1] = name 51 | end 52 | end 53 | end 54 | 55 | packages = table.unique(packages) 56 | table.sort(packages) 57 | return packages 58 | end 59 | 60 | function main(...) 61 | local pkgInfo = io.load(path.join(os.projectdir(), "xmake-requires.lock")) 62 | for platform, packages in pairs(pkgInfo) do 63 | if platform ~= "__meta__" then 64 | local os = platform:match("(.+)|") 65 | local osKey = "" 66 | if os == "linux" then 67 | osKey = "L" 68 | elseif os == "macosx" then 69 | osKey = "M" 70 | elseif os == "windows" then 71 | osKey = "W" 72 | end 73 | 74 | recordPackages(packages, osKey) 75 | end 76 | end 77 | 78 | for _, packageInfo in pairs(allPackages) do 79 | table.sort(packageInfo["os"]) 80 | end 81 | 82 | io.save(path.join(os.projectdir(), "build", "packages.txt"), allPackages) 83 | io.save(path.join(os.projectdir(), "build", "referenced.txt"), recordReferencedPackages()) 84 | end 85 | -------------------------------------------------------------------------------- /src/os/bluetooth.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "bluetooth.hpp" 5 | 6 | #include 7 | #include 8 | 9 | using CompletionQueue = std::queue; 10 | 11 | struct SocketQueue { 12 | CompletionQueue pendingReads; 13 | CompletionQueue pendingWrites; 14 | }; 15 | 16 | using SocketQueueMap = std::unordered_map; 17 | 18 | // Queue for Bluetooth sockets 19 | SocketQueueMap btSockets; 20 | 21 | std::unordered_map>> btReads; 22 | std::unordered_map> btAccepts; 23 | 24 | // Gets the queue of pending operations for a socket/Bluetooth channel. 25 | CompletionQueue& getPendingQueue(swift::UInt id, SocketQueueMap& map, IOType ioType) { 26 | auto& queue = map[id]; 27 | 28 | return ioType == IOType::Send ? queue.pendingWrites : queue.pendingReads; 29 | } 30 | 31 | bool bluetoothComplete(unsigned long id, IOType ioType, IOReturn status) { 32 | auto& queue = getPendingQueue(id, btSockets, ioType); 33 | if (queue.empty()) return false; 34 | 35 | auto pending = queue.front(); 36 | queue.pop(); 37 | 38 | auto& result = *pending; 39 | 40 | // Resume caller 41 | result.error = status; 42 | result.coroHandle(); 43 | return true; 44 | } 45 | 46 | void bluetoothReadComplete(unsigned long id, const char* data, std::size_t dataLen) { 47 | btReads[id].emplace(std::in_place, data, dataLen); 48 | 49 | bluetoothComplete(id, IOType::Receive, kIOReturnSuccess); 50 | } 51 | 52 | void bluetoothAcceptComplete(unsigned long id, const void* handle, const Device& device) { 53 | btAccepts[id].emplace(device, *static_cast(handle)); 54 | 55 | bluetoothComplete(id, IOType::Receive, kIOReturnSuccess); 56 | } 57 | 58 | void bluetoothClosed(unsigned long id) { 59 | btReads[id].emplace(); 60 | 61 | // Close events are determined by the receive result, resume the first read operation in the queue 62 | bluetoothComplete(id, IOType::Receive, kIOReturnSuccess); 63 | } 64 | 65 | void clearBluetoothDataQueue(unsigned long id) { 66 | btReads.erase(id); 67 | btAccepts.erase(id); 68 | } 69 | 70 | void AsyncBT::submit(swift::UInt id, IOType ioType, Async::CompletionResult& result) { 71 | getPendingQueue(id, btSockets, ioType).push(&result); 72 | } 73 | 74 | std::optional AsyncBT::getReadResult(swift::UInt id) { 75 | auto data = btReads[id].front(); 76 | btReads[id].pop(); 77 | return data; 78 | } 79 | 80 | AsyncBT::BTAccept AsyncBT::getAcceptResult(swift::UInt id) { 81 | auto client = btAccepts[id].front(); 82 | btAccepts[id].pop(); 83 | return client; 84 | } 85 | 86 | void AsyncBT::cancel(swift::UInt id) { 87 | // Loop through all pending events and send the "aborted" signal 88 | 89 | // clang-format doesn't recognize semicolons after loops 90 | // clang-format off 91 | while (bluetoothComplete(id, IOType::Send, kIOReturnAborted)); 92 | while (bluetoothComplete(id, IOType::Receive, kIOReturnAborted)); 93 | // clang-format on 94 | } 95 | -------------------------------------------------------------------------------- /src/gui/newconn.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "newconn.hpp" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include "imguiext.hpp" 11 | #include "newconnbt.hpp" 12 | #include "newconnip.hpp" 13 | #include "notifications.hpp" 14 | #include "components/connwindow.hpp" 15 | #include "net/device.hpp" 16 | #include "net/enums.hpp" 17 | #include "utils/strings.hpp" 18 | 19 | // Formats a Device instance into a string for use in a ConnWindow title. 20 | std::string formatDevice(bool useTLS, const Device& device, std::string_view extraInfo) { 21 | // Type of the connection 22 | bool isIP = device.type == ConnectionType::TCP || device.type == ConnectionType::UDP; 23 | const char* typeName = getConnectionTypeName(device.type); 24 | auto typeString = useTLS ? std::format("{}+TLS", typeName) : std::string{ typeName }; 25 | 26 | // Bluetooth-based connections are described using the device's name (e.g. "MyESP32"), 27 | // IP-based connections use the device's IP address (e.g. 192.168.0.178). 28 | std::string deviceString = isIP ? device.address : device.name; 29 | 30 | // Newlines may be present in a Bluetooth device name, and if they get into a window's title, anything after the 31 | // first one will get cut off (the title bar can only hold one line). Replace them with left/down arrow icons 32 | // to keep everything on one line. 33 | deviceString = Strings::replaceAll(deviceString, "\n", "\uf306"); 34 | 35 | // Format the values into a string as the title 36 | // The address is always part of the id hash. 37 | // The port is not visible for a Bluetooth connection, instead, it is part of the id hash. 38 | std::string title = isIP 39 | ? std::format("{} Connection - {} port {}##{}", typeString, deviceString, device.port, device.address) 40 | : std::format("{} Connection - {}##{} port {}", typeString, deviceString, device.address, device.port); 41 | 42 | // If there's extra info, it is formatted before the window title. 43 | // If it were to be put after the title, it would be part of the invisible id hash (after the "##"). 44 | return extraInfo.empty() ? title : std::format("({}) {}", extraInfo, title); 45 | } 46 | 47 | void addConnWindow(WindowList& list, bool useTLS, const Device& device, std::string_view extraInfo) { 48 | bool isNew = list.add(formatDevice(useTLS, device, extraInfo), useTLS, device, extraInfo); 49 | 50 | // If the connection exists, show a message 51 | if (!isNew) ImGuiExt::addNotification("This connection is already open.", NotificationType::Warning); 52 | } 53 | 54 | void drawNewConnectionWindow(bool& open, WindowList& connections, WindowList& sdpWindows) { 55 | if (!open) return; 56 | 57 | using namespace ImGuiExt::Literals; 58 | ImGui::SetNextWindowSize(40_fh * 12_fh, ImGuiCond_Appearing); 59 | 60 | if (ImGui::Begin("New Connection", &open) && ImGui::BeginTabBar("ConnectionTypes")) { 61 | drawIPConnectionTab(connections); 62 | drawBTConnectionTab(connections, sdpWindows); 63 | ImGui::EndTabBar(); 64 | } 65 | ImGui::End(); 66 | } 67 | -------------------------------------------------------------------------------- /src/gui/menu.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "menu.hpp" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | #if OS_MACOS 13 | #include 14 | #endif 15 | 16 | #include "imguiext.hpp" 17 | #include "menu.state.hpp" 18 | #include "notifications.hpp" 19 | #include "app/settings.hpp" 20 | #include "components/windowlist.hpp" 21 | 22 | void windowMenu(WindowList& list, const char* desc) { 23 | if (!ImGui::BeginMenu(desc)) return; 24 | 25 | if (list.empty()) ImGui::TextDisabled("%s", std::format("No {}", desc).c_str()); 26 | else 27 | for (const auto& i : list) ImGuiExt::windowMenuItem(i->getTitle()); 28 | 29 | ImGui::EndMenu(); 30 | } 31 | 32 | void Menu::drawMenuBar(bool& quit, WindowList& connections, WindowList& servers) { 33 | if (!ImGui::BeginMainMenuBar()) return; 34 | 35 | ImGuiExt::drawNotificationsMenu(notificationsOpen); 36 | 37 | if constexpr (OS_MACOS) { 38 | if (Settings::GUI::systemMenu) { 39 | ImGui::EndMainMenuBar(); 40 | return; 41 | } 42 | } 43 | 44 | if (ImGui::BeginMenu("File")) { 45 | if (ImGui::MenuItem("Settings", ImGuiExt::shortcut(',').data())) settingsOpen = true; 46 | ImGui::MenuItem("Quit", nullptr, &quit); 47 | ImGui::EndMenu(); 48 | } 49 | 50 | if (ImGui::BeginMenu("View")) { 51 | if (ImGui::MenuItem("New Connection", nullptr, nullptr)) newConnectionOpen = true; 52 | if (ImGui::MenuItem("New Server", nullptr, nullptr)) newServerOpen = true; 53 | if (ImGui::MenuItem("Notifications", nullptr, nullptr)) notificationsOpen = true; 54 | ImGui::EndMenu(); 55 | } 56 | 57 | // List all open connections and servers 58 | windowMenu(connections, "Connections"); 59 | windowMenu(servers, "Servers"); 60 | 61 | if (ImGui::BeginMenu("Help")) { 62 | if (ImGui::MenuItem("About", nullptr, nullptr)) aboutOpen = true; 63 | if (ImGui::MenuItem("Links")) linksOpen = true; 64 | 65 | ImGui::EndMenu(); 66 | } 67 | 68 | ImGui::EndMainMenuBar(); 69 | } 70 | 71 | void Menu::setWindowFocus(const char* title) { 72 | ImGui::SetWindowFocus(title); 73 | } 74 | 75 | void Menu::setupMenuBar() { 76 | #if OS_MACOS 77 | GUIMacOS::setupMenuBar(); 78 | #endif 79 | } 80 | 81 | void Menu::addWindowMenuItem([[maybe_unused]] std::string_view name) { 82 | #if OS_MACOS 83 | GUIMacOS::addWindowMenuItem(std::string{ name }); 84 | #endif 85 | } 86 | 87 | void Menu::removeWindowMenuItem([[maybe_unused]] std::string_view name) { 88 | #if OS_MACOS 89 | GUIMacOS::removeWindowMenuItem(std::string{ name }); 90 | #endif 91 | } 92 | 93 | void Menu::addServerMenuItem([[maybe_unused]] std::string_view name) { 94 | #if OS_MACOS 95 | GUIMacOS::addServerMenuItem(std::string{ name }); 96 | #endif 97 | } 98 | 99 | void Menu::removeServerMenuItem([[maybe_unused]] std::string_view name) { 100 | #if OS_MACOS 101 | GUIMacOS::removeServerMenuItem(std::string{ name }); 102 | #endif 103 | } 104 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | # This file defines the formatting style for WhaleConnect. 2 | # Minimum clang-format version required: 16 3 | 4 | ColumnLimit: 120 5 | ReflowComments: true 6 | Standard: Latest 7 | FixNamespaceComments: false 8 | 9 | # Indentation 10 | IndentWidth: 4 11 | ConstructorInitializerIndentWidth: 4 12 | ContinuationIndentWidth: 4 13 | AccessModifierOffset: -4 14 | UseTab: Never 15 | LambdaBodyIndentation: Signature 16 | NamespaceIndentation: All 17 | IndentAccessModifiers: false 18 | IndentCaseBlocks: false 19 | IndentCaseLabels: true 20 | IndentPPDirectives: None 21 | IndentRequiresClause: false 22 | IndentWrappedFunctionNames: false 23 | 24 | # Empty lines 25 | MaxEmptyLinesToKeep: 1 26 | SeparateDefinitionBlocks: Always 27 | EmptyLineAfterAccessModifier: Never 28 | EmptyLineBeforeAccessModifier: Always 29 | KeepEmptyLinesAtTheStartOfBlocks: false 30 | 31 | # Short blocks 32 | AllowShortBlocksOnASingleLine: Empty 33 | AllowShortCaseLabelsOnASingleLine: false 34 | AllowShortEnumsOnASingleLine: true 35 | AllowShortFunctionsOnASingleLine: Empty 36 | AllowShortIfStatementsOnASingleLine: AllIfsAndElse 37 | AllowShortLambdasOnASingleLine: All 38 | AllowShortLoopsOnASingleLine: true 39 | InsertBraces: false 40 | 41 | # Spacing/padding 42 | Cpp11BracedListStyle: false 43 | SpacesInParentheses: false 44 | SpaceInEmptyParentheses: false 45 | SpaceBeforeParens: Custom 46 | SpaceBeforeParensOptions: 47 | AfterControlStatements: true 48 | AfterRequiresInClause: true 49 | AfterRequiresInExpression: true 50 | SpacesBeforeTrailingComments: 1 51 | SpacesInLineCommentPrefix: 52 | Minimum: 1 53 | Maximum: -1 54 | SpaceAfterLogicalNot: false 55 | SpaceAfterTemplateKeyword: true 56 | SpaceBeforeCaseColon: false 57 | SpaceBeforeCpp11BracedList: false 58 | SpaceBeforeCtorInitializerColon: true 59 | SpaceBeforeInheritanceColon: true 60 | SpaceBeforeRangeBasedForLoopColon: true 61 | SpaceBeforeSquareBrackets: false 62 | SpaceInEmptyBlock: false 63 | SpacesInAngles: Never 64 | SpacesInConditionalStatement: false 65 | SpacesInSquareBrackets: false 66 | 67 | # Alignment 68 | AlignAfterOpenBracket: DontAlign 69 | AlignOperands: DontAlign 70 | AlignTrailingComments: 71 | Kind: Never 72 | PointerAlignment: Left 73 | ReferenceAlignment: Left 74 | 75 | # Packing 76 | BinPackArguments: true 77 | BinPackParameters: true 78 | PackConstructorInitializers: BinPack 79 | 80 | # Line breaks 81 | AllowAllArgumentsOnNextLine: false 82 | AllowAllParametersOfDeclarationOnNextLine: false 83 | AlwaysBreakTemplateDeclarations: Yes 84 | BreakBeforeBinaryOperators: All 85 | BreakBeforeBraces: Attach 86 | BreakBeforeConceptDeclarations: Always 87 | BreakBeforeTernaryOperators: true 88 | BreakConstructorInitializers: AfterColon 89 | RequiresClausePosition: OwnLine 90 | 91 | # Sorting 92 | SortUsingDeclarations: true 93 | SortIncludes: CaseInsensitive 94 | IncludeBlocks: Regroup 95 | IncludeCategories: 96 | - Regex: '<[a-z_]+>' # C++ standard headers 97 | Priority: 1 98 | - Regex: '' # Winsock main header (included before others) 99 | Priority: 2 100 | SortPriority: 1 101 | - Regex: '<.+\..+>' # Library headers 102 | Priority: 2 103 | SortPriority: 2 104 | - Regex: '"[^/]+"' # Project headers (current directory) 105 | Priority: 3 106 | SortPriority: 3 107 | - Regex: '".+"' # Project headers 108 | Priority: 3 109 | SortPriority: 4 110 | -------------------------------------------------------------------------------- /src/sockets/delegates/windows/client.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "sockets/delegates/client.hpp" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #include "net/enums.hpp" 15 | #include "net/netutils.hpp" 16 | #include "os/async.hpp" 17 | #include "os/errcheck.hpp" 18 | #include "utils/strings.hpp" 19 | 20 | void startConnect(SOCKET s, sockaddr* addr, std::size_t len, Async::CompletionResult& result) { 21 | // ConnectEx() requires the socket to be initially bound. 22 | // A sockaddr_storage can be used with all connection types, Internet and Bluetooth. 23 | sockaddr_storage addrBind{ .ss_family = addr->sa_family }; 24 | 25 | // The bind() function will work with sockaddr_storage for any address family. However, with Bluetooth, it expects 26 | // the size parameter to be the size of a Bluetooth address structure. Unlike Internet-based sockets, it will not 27 | // accept a sockaddr_storage size. 28 | // This means the size must be spoofed with Bluetooth sockets. 29 | int addrSize = addr->sa_family == AF_BTH ? sizeof(SOCKADDR_BTH) : sizeof(sockaddr_storage); 30 | 31 | // Bind the socket 32 | check(bind(s, reinterpret_cast(&addrBind), addrSize)); 33 | Async::submit(Async::Connect{ { s, &result }, addr, static_cast(len) }); 34 | } 35 | 36 | void finalizeConnect(SOCKET s) { 37 | // Make the socket behave more like a regular socket connected with connect() 38 | check(setsockopt(s, SOL_SOCKET, SO_UPDATE_CONNECT_CONTEXT, nullptr, 0)); 39 | } 40 | 41 | template <> 42 | Task<> Delegates::Client::connect(Device device) { 43 | auto addr = NetUtils::resolveAddr(device); 44 | 45 | co_await NetUtils::loopWithAddr(addr.get(), [this, type = device.type](const AddrInfoType* result) -> Task<> { 46 | handle.reset(check(socket(result->ai_family, result->ai_socktype, result->ai_protocol))); 47 | 48 | // Add the socket to the async queue 49 | Async::add(*handle); 50 | 51 | // Datagram sockets can be directly connected (ConnectEx doesn't support them) 52 | if (type == ConnectionType::UDP) { 53 | check(::connect(*handle, result->ai_addr, static_cast(result->ai_addrlen))); 54 | } else { 55 | co_await Async::run(std::bind_front(startConnect, *handle, result->ai_addr, result->ai_addrlen)); 56 | finalizeConnect(*handle); 57 | } 58 | }); 59 | } 60 | 61 | template <> 62 | Task<> Delegates::Client::connect(Device device) { 63 | // Only RFCOMM sockets are supported by the Microsoft Bluetooth stack on Windows 64 | if (device.type != ConnectionType::RFCOMM) std::unreachable(); 65 | 66 | handle.reset(check(socket(AF_BTH, SOCK_STREAM, BTHPROTO_RFCOMM))); 67 | Async::add(*handle); 68 | 69 | // Convert the MAC address from string form into integer form 70 | // This is done by removing all colons in the address string, then parsing the resultant string as an 71 | // integer in base-16 (which is how a MAC address is structured). 72 | BTH_ADDR btAddr = std::stoull(Strings::replaceAll(device.address, ":", ""), nullptr, 16); 73 | SOCKADDR_BTH sAddrBT{ .addressFamily = AF_BTH, .btAddr = btAddr, .port = device.port }; 74 | 75 | co_await Async::run(std::bind_front(startConnect, *handle, reinterpret_cast(&sAddrBT), sizeof(sAddrBT))); 76 | finalizeConnect(*handle); 77 | } 78 | -------------------------------------------------------------------------------- /src/os/error.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include 5 | 6 | #if OS_WINDOWS 7 | #include 8 | #else 9 | #include 10 | #include 11 | 12 | #include 13 | #endif 14 | 15 | #if OS_MACOS 16 | #include 17 | #include 18 | #endif 19 | 20 | #include "error.hpp" 21 | 22 | const char* getErrorName(System::ErrorType type) { 23 | using enum System::ErrorType; 24 | switch (type) { 25 | case System: 26 | return "System"; 27 | case AddrInfo: 28 | return "getaddrinfo"; 29 | case IOReturn: 30 | return "IOReturn"; 31 | default: 32 | return "Unknown error type"; 33 | } 34 | } 35 | 36 | System::ErrorCode System::getLastError() { 37 | #if OS_WINDOWS 38 | return GetLastError(); 39 | #else 40 | return errno; 41 | #endif 42 | } 43 | 44 | bool System::isFatal(ErrorCode code) { 45 | // Check if the code is actually an error 46 | // Platform-specific "pending" codes indicate an async operation has not yet finished and not a fatal error. 47 | if (code == 0) return false; 48 | 49 | #if OS_WINDOWS 50 | // Pending I/O for overlapped sockets 51 | if (code == WSA_IO_PENDING) return false; 52 | #elif OS_MACOS 53 | // Pending I/O for non-blocking sockets 54 | if (code == EINPROGRESS) return false; 55 | #endif 56 | 57 | return true; 58 | } 59 | 60 | std::string System::formatSystemError(ErrorCode code, ErrorType type, const std::source_location& location) { 61 | using enum ErrorType; 62 | 63 | // Message buffer 64 | std::string msg; 65 | 66 | switch (type) { 67 | #if OS_WINDOWS 68 | case System: 69 | case AddrInfo: { 70 | // gai_strerror is not recommended on Windows: 71 | // https://learn.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfo#return-value 72 | msg.resize(512); 73 | 74 | // Get the message text 75 | auto flags = FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_MAX_WIDTH_MASK; 76 | DWORD length = FormatMessageA(flags, nullptr, code, LocaleNameToLCID(L"en-US", 0), msg.data(), 77 | static_cast(msg.size()), nullptr); 78 | 79 | msg.resize(length); 80 | break; 81 | } 82 | #else 83 | case System: 84 | msg = std::strerror(code); 85 | break; 86 | case AddrInfo: 87 | msg = gai_strerror(code); 88 | break; 89 | #endif 90 | #if OS_MACOS 91 | case IOReturn: 92 | msg = mach_error_string(code); 93 | break; 94 | #endif 95 | default: 96 | msg = "Unknown error type"; 97 | } 98 | 99 | std::string where = std::format("{}({}:{})", location.file_name(), location.line(), location.column()); 100 | return std::format("{} (type {}, at {}): {}", code, getErrorName(type), where, msg); 101 | } 102 | 103 | bool System::SystemError::isCanceled() const { 104 | #if OS_WINDOWS 105 | if (type == System::ErrorType::System && code == WSA_OPERATION_ABORTED) return true; 106 | #else 107 | if (type == System::ErrorType::System && code == ECANCELED) return true; 108 | #endif 109 | 110 | #if OS_MACOS 111 | // IOBluetooth-specific error 112 | if (type == System::ErrorType::IOReturn && code == kIOReturnAborted) return true; 113 | #endif 114 | 115 | return false; 116 | } 117 | -------------------------------------------------------------------------------- /src/sockets/delegates/macos/server.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "sockets/delegates/server.hpp" 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | #include "net/netutils.hpp" 13 | #include "os/async.hpp" 14 | #include "os/bluetooth.hpp" 15 | #include "os/errcheck.hpp" 16 | #include "os/error.hpp" 17 | #include "sockets/incomingsocket.hpp" 18 | #include "utils/task.hpp" 19 | 20 | template <> 21 | ServerAddress Delegates::Server::startServer(const Device& serverInfo) { 22 | ServerAddress result = NetUtils::startServer(serverInfo, handle); 23 | 24 | Async::prepSocket(*handle); 25 | return result; 26 | } 27 | 28 | template <> 29 | Task Delegates::Server::accept() { 30 | co_await Async::run([this](Async::CompletionResult& result) { 31 | Async::submit(Async::Accept{ { *handle, &result } }); 32 | }); 33 | 34 | sockaddr_storage client; 35 | auto clientAddr = reinterpret_cast(&client); 36 | socklen_t clientLen = sizeof(client); 37 | 38 | SocketHandle fd{ check(::accept(*handle, clientAddr, &clientLen)) }; 39 | Device device = NetUtils::fromAddr(clientAddr, clientLen, ConnectionType::TCP); 40 | 41 | Async::prepSocket(*fd); 42 | co_return { device, std::make_unique>(std::move(fd)) }; 43 | } 44 | 45 | template <> 46 | Task Delegates::Server::recvFrom(std::size_t size) { 47 | sockaddr_storage from; 48 | auto fromAddr = reinterpret_cast(&from); 49 | socklen_t addrSize = sizeof(from); 50 | 51 | co_await Async::run([this](Async::CompletionResult& result) { 52 | Async::submit(Async::ReceiveFrom{ { *handle, &result } }); 53 | }); 54 | 55 | std::string data(size, 0); 56 | auto recvLen = check(recvfrom(*handle, data.data(), data.size(), 0, fromAddr, &addrSize)); 57 | data.resize(recvLen); 58 | 59 | co_return { NetUtils::fromAddr(fromAddr, addrSize, ConnectionType::UDP), data }; 60 | } 61 | 62 | template <> 63 | Task<> Delegates::Server::sendTo(Device device, std::string data) { 64 | auto addr = NetUtils::resolveAddr(device, false); 65 | 66 | co_await NetUtils::loopWithAddr(addr.get(), [this, &data](const AddrInfoType* resolveRes) -> Task<> { 67 | co_await Async::run([this](Async::CompletionResult& result) { 68 | Async::submit(Async::SendTo{ { *handle, &result } }); 69 | }); 70 | check(sendto(*handle, data.data(), data.size(), 0, resolveRes->ai_addr, resolveRes->ai_addrlen)); 71 | }); 72 | } 73 | 74 | template <> 75 | ServerAddress Delegates::Server::startServer(const Device& serverInfo) { 76 | handle.reset(BluetoothMacOS::makeBTServerHandle()); 77 | 78 | bool isL2CAP = serverInfo.type == ConnectionType::L2CAP; 79 | check((*handle)->startServer(isL2CAP, serverInfo.port), checkTrue, [](bool) { return kIOReturnError; }, 80 | System::ErrorType::IOReturn); 81 | return { serverInfo.port }; 82 | } 83 | 84 | template <> 85 | Task Delegates::Server::accept() { 86 | co_await Async::run(std::bind_front(AsyncBT::submit, (*handle)->getHash(), IOType::Receive), 87 | System::ErrorType::IOReturn); 88 | 89 | auto [device, newHandle] = AsyncBT::getAcceptResult((*handle)->getHash()); 90 | SocketHandle fd{ std::move(newHandle) }; 91 | 92 | co_return { device, std::make_unique>(std::move(fd)) }; 93 | } 94 | -------------------------------------------------------------------------------- /src/net/netutils.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "netutils.hpp" 5 | 6 | #include 7 | 8 | #include "enums.hpp" 9 | #include "os/errcheck.hpp" 10 | #include "utils/strings.hpp" 11 | #include "utils/uuids.hpp" 12 | 13 | #if !OS_WINDOWS 14 | constexpr auto GetAddrInfoW = getaddrinfo; 15 | constexpr auto GetNameInfoW = getnameinfo; 16 | #endif 17 | 18 | AddrInfoHandle NetUtils::resolveAddr(const Device& device, bool useDNS) { 19 | bool isUDP = device.type == ConnectionType::UDP; 20 | AddrInfoType hints{ 21 | .ai_flags = useDNS ? 0 : AI_NUMERICHOST, 22 | .ai_family = AF_UNSPEC, 23 | .ai_socktype = isUDP ? SOCK_DGRAM : SOCK_STREAM, 24 | .ai_protocol = isUDP ? IPPROTO_UDP : IPPROTO_TCP, 25 | }; 26 | 27 | // Wide encoding conversions for Windows 28 | Strings::SysStr addrWide = Strings::toSys(device.address); 29 | Strings::SysStr portWide = Strings::toSys(device.port); 30 | 31 | // Resolve the IP 32 | AddrInfoHandle ret; 33 | check(GetAddrInfoW(addrWide.c_str(), portWide.c_str(), &hints, ztd::out_ptr::out_ptr(ret)), checkZero, 34 | useReturnCode, System::ErrorType::AddrInfo); 35 | 36 | return ret; 37 | } 38 | 39 | Device NetUtils::fromAddr(const sockaddr* addr, socklen_t addrLen, ConnectionType type) { 40 | constexpr auto nullChar = Strings::SysStr::value_type{}; 41 | 42 | Strings::SysStr ipStr(NI_MAXHOST, nullChar); 43 | Strings::SysStr portStr(NI_MAXSERV, nullChar); 44 | 45 | auto ipLen = static_cast(ipStr.size()); 46 | auto portLen = static_cast(portStr.size()); 47 | 48 | check(GetNameInfoW(addr, addrLen, ipStr.data(), ipLen, portStr.data(), portLen, NI_NUMERICHOST | NI_NUMERICSERV), 49 | checkZero, useReturnCode, System::ErrorType::AddrInfo); 50 | 51 | // Process returned strings 52 | Strings::stripNull(ipStr); 53 | std::string ip = Strings::fromSys(ipStr); 54 | auto port = static_cast(std::stoi(Strings::fromSys(portStr))); 55 | 56 | return { type, "", ip, port }; 57 | } 58 | 59 | std::uint16_t NetUtils::getPort(Traits::SocketHandleType handle, bool isV4) { 60 | sockaddr_storage addr; 61 | socklen_t localAddrLen = sizeof(addr); 62 | check(getsockname(handle, reinterpret_cast(&addr), &localAddrLen)); 63 | 64 | std::uint16_t port 65 | = isV4 ? reinterpret_cast(&addr)->sin_port : reinterpret_cast(&addr)->sin6_port; 66 | return UUIDs::byteSwap(port); 67 | } 68 | 69 | ServerAddress NetUtils::startServer(const Device& serverInfo, Delegates::SocketHandle& handle) { 70 | auto resolved = resolveAddr(serverInfo); 71 | bool isTCP = serverInfo.type == ConnectionType::TCP; 72 | bool isV4 = false; 73 | 74 | NetUtils::loopWithAddr(resolved.get(), [&handle, &isV4, isTCP](const AddrInfoType* result) { 75 | // Only AF_INET/AF_INET6 are supported 76 | switch (result->ai_family) { 77 | case AF_INET: 78 | isV4 = true; 79 | break; 80 | case AF_INET6: 81 | isV4 = false; 82 | break; 83 | default: 84 | return; 85 | } 86 | 87 | handle.reset(check(socket(result->ai_family, result->ai_socktype, result->ai_protocol))); 88 | 89 | // Bind and listen 90 | check(bind(*handle, result->ai_addr, static_cast(result->ai_addrlen))); 91 | if (isTCP) check(listen(*handle, SOMAXCONN)); 92 | }); 93 | 94 | return { getPort(*handle, isV4), isV4 ? IPType::IPv4 : IPType::IPv6 }; 95 | } 96 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # WhaleConnect Contributing Guidelines 2 | 3 | Thank you for contributing to WhaleConnect. Please review these contribution guidelines and be mindful of the [code of conduct](https://github.com/WhaleConnect/.github/blob/main/CODE_OF_CONDUCT.md) in your contributions. 4 | 5 | ## Opening Issues and Discussions 6 | 7 | We encourage using GitHub as a centralized place for community engagement. Feel free to submit bug reports, feature requests, or questions, but please follow these steps before doing so: 8 | 9 | - Ensure your issue has not already been submitted. 10 | - You can search by [issue label](https://github.com/WhaleConnect/whaleconnect/labels) to effectively find related information. 11 | - Be sure to include closed issues in your search in case your issue has already been resolved. 12 | - Ensure you have filled out the correct issue template. 13 | - Provide a clear description of your issue in the title. 14 | 15 | ### Bug Reports (GitHub Issues) 16 | 17 | - If you have found a security concern, **please do not submit it in GitHub Issues or Discussions.** Instead, refer to the project's [security policy](https://github.com/WhaleConnect/.github/blob/main/SECURITY.md). 18 | - Make sure your environment is officially supported ([System Requirements](readme.md#minimum-hardware-requirements)). 19 | - Currently supported OS+architecture combinations are: Windows+x64, Linux+x64, macOS+ARM64. 20 | - Check if the bug still exists on the main branch. 21 | - Provide a clear, reproducible example with sufficient detail. This includes all steps necessary to reproduce the bug and any environment/settings configuration that might be related to the behavior. 22 | - Include relevant screenshots, video captures, or data output to explain the bug. 23 | 24 | ### Feature Requests (GitHub Issues) 25 | 26 | - Clearly explain the proposed feature and how it can be useful. 27 | - Include user interface designs or other mockups if you think they will help in your explanation. 28 | - If you are willing to, feel free to write your own code, open a pull request, and mention it in your issue. We always welcome external contributions. 29 | 30 | ### Questions (GitHub Discussions) 31 | 32 | - Clearly explain what you want to accomplish using the software and if it is suitable for your desired task. 33 | - Be mindful of the [XY Problem](https://xyproblem.info/) and make sure you describe your *original* goals and expectations. This will help us answer your question more easily. 34 | - Include anything you have already tried and any errors or failures you have encountered. 35 | 36 | ## Opening Pull Requests 37 | 38 | We accept code contributions through GitHub Pull Requests. Please follow these guidelines in your contribution: 39 | 40 | - Create a separate branch for your pull request so it can be modified later, if necessary. 41 | - If you are adding a new feature, include a screen recording or screenshot to quickly demonstrate it. 42 | - Follow the project's coding style: 43 | - Variables and functions are in `camelCase`. 44 | - Classes, structs, namespaces, and type-aliases are in `PascalCase`. 45 | - This project uses modern C++ features and idioms: C++-style casts, RAII, coroutines, etc. 46 | - The code should compile cleanly with no warnings (other than those explicitly silenced in `xmake.lua`). 47 | - Ensure your patch passes the [test cases](testing.md). 48 | 49 | WhaleConnect uses the following tools for formatting: 50 | 51 | - **EditorConfig:** Basic file format standards (UTF-8, indentation character, trailing newline, etc.) 52 | - **Clang Format:** Formatting for C++ code. 53 | - **SwiftFormat:** Formatting for Swift code. 54 | 55 | GitHub Actions is set up to check the formatting of C++ files using Clang Format. 56 | 57 | By submitting a code contribution to WhaleConnect, you agree to have your contribution become part of the repository and be distributed under the project's [license](../COPYING). 58 | -------------------------------------------------------------------------------- /src/components/connwindow.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "connwindow.hpp" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | 15 | #include "app/settings.hpp" 16 | #include "gui/imguiext.hpp" 17 | #include "gui/menu.hpp" 18 | #include "net/device.hpp" 19 | #include "net/enums.hpp" 20 | #include "os/error.hpp" 21 | #include "sockets/clientsocket.hpp" 22 | #include "sockets/clientsockettls.hpp" 23 | #include "sockets/delegates/delegates.hpp" 24 | 25 | SocketPtr makeClientSocket(bool useTLS, ConnectionType type) { 26 | using enum ConnectionType; 27 | 28 | switch (type) { 29 | case TCP: 30 | if (useTLS) return std::make_unique(); 31 | [[fallthrough]]; 32 | case UDP: 33 | return std::make_unique(); 34 | case L2CAP: 35 | case RFCOMM: 36 | return std::make_unique(); 37 | default: 38 | std::unreachable(); 39 | } 40 | } 41 | 42 | ConnWindow::ConnWindow(std::string_view title, bool useTLS, const Device& device, std::string_view) : 43 | Window(title), socket(makeClientSocket(useTLS, device.type)) { 44 | if (Settings::GUI::systemMenu) Menu::addWindowMenuItem(getTitle()); 45 | connect(device); 46 | } 47 | 48 | ConnWindow::~ConnWindow() { 49 | if (Settings::GUI::systemMenu) Menu::removeWindowMenuItem(getTitle()); 50 | socket->cancelIO(); 51 | } 52 | 53 | Task<> ConnWindow::connect(Device device) try { 54 | // Connect the socket 55 | console.addInfo("Connecting..."); 56 | co_await socket->connect(device); 57 | 58 | console.addInfo("Connected."); 59 | connected = true; 60 | } catch (const System::SystemError& error) { 61 | console.errorHandler(error); 62 | } catch (const Botan::TLS::TLS_Exception& error) { 63 | console.addError(error.what()); 64 | } 65 | 66 | Task<> ConnWindow::sendHandler(std::string s) try { 67 | co_await socket->send(s); 68 | } catch (const System::SystemError& error) { 69 | console.errorHandler(error); 70 | } catch (const Botan::TLS::TLS_Exception& error) { 71 | console.addError(error.what()); 72 | } 73 | 74 | Task<> ConnWindow::readHandler() try { 75 | if (!connected || pendingRecv) co_return; 76 | pendingRecv = true; 77 | 78 | auto [complete, closed, data, alert] = co_await socket->recv(console.getRecvSize()); 79 | 80 | if (complete) { 81 | if (closed) { 82 | // Peer closed connection 83 | console.addInfo("Remote host closed connection."); 84 | socket->close(); 85 | connected = false; 86 | } else { 87 | console.addText(data); 88 | } 89 | } 90 | 91 | if (alert) { 92 | std::string desc = "ALERT"; 93 | ImVec4 color{ 0, 0.6f, 0, 1 }; 94 | if (alert->isFatal) { 95 | console.addMessage(std::format("FATAL: {}", alert->desc), desc, color); 96 | connected = false; 97 | } else { 98 | console.addMessage(alert->desc, desc, color); 99 | } 100 | } 101 | pendingRecv = false; 102 | } catch (const System::SystemError& error) { 103 | console.errorHandler(error); 104 | } catch (const Botan::TLS::TLS_Exception& error) { 105 | console.addError(error.what()); 106 | } 107 | 108 | void ConnWindow::onBeforeUpdate() { 109 | using namespace ImGuiExt::Literals; 110 | 111 | ImGui::SetNextWindowSize(35_fh * 20_fh, ImGuiCond_Appearing); 112 | readHandler(); 113 | } 114 | 115 | void ConnWindow::onUpdate() { 116 | if (auto sendString = console.updateWithTextbox()) sendHandler(*sendString); 117 | } 118 | -------------------------------------------------------------------------------- /src/components/ioconsole.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "ioconsole.hpp" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | #include "gui/imguiext.hpp" 13 | #include "utils/strings.hpp" 14 | 15 | void IOConsole::drawControls() { 16 | // Popup for more options 17 | if (ImGui::BeginPopup("options")) { 18 | // Options for the input textbox 19 | ImGui::Separator(); 20 | ImGui::MenuItem("Send echoing", nullptr, &sendEchoing); 21 | ImGui::MenuItem("Clear texbox on send", nullptr, &clearTextboxOnSubmit); 22 | ImGui::MenuItem("Add final line ending", nullptr, &addFinalLineEnding); 23 | 24 | using namespace ImGuiExt::Literals; 25 | 26 | ImGui::Separator(); 27 | ImGui::SetNextItemWidth(4_fh); 28 | ImGuiExt::inputScalar("Receive size", recvSizeTmp); 29 | 30 | if (ImGui::IsItemDeactivatedAfterEdit()) { 31 | if (recvSizeTmp == 0) recvSizeTmp = recvSize; // Reset invalid sizes 32 | else recvSize = recvSizeTmp; 33 | } 34 | 35 | ImGui::EndPopup(); 36 | } 37 | 38 | // Line ending combobox 39 | // The code used to calculate where to put the combobox is derived from 40 | // https://github.com/ocornut/imgui/issues/4157#issuecomment-843197490 41 | using namespace ImGuiExt::Literals; 42 | 43 | float comboWidth = 10_fh; 44 | ImGui::SameLine(); 45 | ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (ImGui::GetContentRegionAvail().x - comboWidth)); 46 | ImGui::SetNextItemWidth(comboWidth); 47 | ImGui::Combo("##lineEnding", ¤tLE, "Newline\0Carriage return\0Both\0"); 48 | } 49 | 50 | std::optional IOConsole::updateWithTextbox() { 51 | std::optional ret; 52 | 53 | // Apply foxus to textbox 54 | // An InputTextMultiline is an InputText contained within a child window so focus must be set before rendering it to 55 | // apply focus to the InputText. 56 | if (focusOnTextbox) { 57 | ImGui::SetKeyboardFocusHere(); 58 | focusOnTextbox = false; 59 | } 60 | 61 | // Textbox 62 | using namespace ImGuiExt::Literals; 63 | 64 | float textboxHeight = 4_fh; // Number of lines that can be displayed 65 | ImVec2 size{ ImGuiExt::fill, textboxHeight }; 66 | ImGuiInputTextFlags flags = ImGuiInputTextFlags_CtrlEnterForNewLine | ImGuiInputTextFlags_EnterReturnsTrue; 67 | 68 | if (ImGuiExt::inputTextMultiline("##input", textBuf, size, flags)) { 69 | // Line ending 70 | std::array endings{ "\n", "\r", "\r\n" }; 71 | auto selectedEnding = endings[currentLE]; 72 | 73 | // InputTextMultiline() always uses \n as a line ending, replace all occurences of \n with the selected ending 74 | std::string sendString = Strings::replaceAll(textBuf, "\n", selectedEnding); 75 | 76 | // Add a final line ending if set 77 | if (addFinalLineEnding) sendString += selectedEnding; 78 | 79 | // Invoke the callback function if the string is not empty 80 | if (!sendString.empty()) { 81 | if (sendEchoing) addMessage(sendString, "SENT ", { 0.28f, 0.67f, 0.68f, 1 }); 82 | 83 | ret = sendString; 84 | } 85 | 86 | // Blank out input textbox 87 | if (clearTextboxOnSubmit) textBuf.clear(); 88 | 89 | focusOnTextbox = true; 90 | } 91 | 92 | update("console"); 93 | drawControls(); 94 | 95 | return ret; 96 | } 97 | 98 | void IOConsole::errorHandler(System::SystemError error) { 99 | // Check for non-fatal errors, then add error line to console 100 | // Don't handle errors caused by I/O cancellation 101 | if (error && !error.isCanceled()) addError(error.what()); 102 | } 103 | -------------------------------------------------------------------------------- /tests/benchmarks/server.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "net/enums.hpp" 13 | #include "os/async.hpp" 14 | #include "os/error.hpp" 15 | #include "sockets/delegates/delegates.hpp" 16 | #include "sockets/serversocket.hpp" 17 | #include "utils/task.hpp" 18 | 19 | struct Client { 20 | SocketPtr sock; 21 | bool done; 22 | 23 | Client(SocketPtr&& sock, bool done) : sock(std::move(sock)), done(done) {} 24 | }; 25 | 26 | thread_local std::list clients; 27 | 28 | Task<> loop(SocketPtr& ptr) { 29 | static const char* response = "HTTP/1.1 200 OK\r\nConnection: keep-alive\r\nContent-Length: 4\r\nContent-Type: " 30 | "text/html\r\n\r\ntest\r\n\r\n"; 31 | 32 | co_await Async::queueToThread(); 33 | Client& client = clients.emplace_front(std::move(ptr), false); 34 | 35 | while (true) { 36 | try { 37 | auto result = co_await client.sock->recv(1024); 38 | if (result.closed) break; 39 | 40 | if (result.data.ends_with("\r\n\r\n")) co_await client.sock->send(response); 41 | } catch (const System::SystemError&) { 42 | break; 43 | } 44 | } 45 | client.done = true; 46 | } 47 | 48 | Task<> accept(const ServerSocket& sock, bool& pendingAccept) try { 49 | auto [_, client] = co_await sock.accept(); 50 | pendingAccept = false; 51 | co_await loop(client); 52 | } catch (const System::SystemError&) { 53 | pendingAccept = false; 54 | } 55 | 56 | void run() { 57 | const ServerSocket s; 58 | const std::uint16_t port = s.startServer({ ConnectionType::TCP, "", "0.0.0.0", 0 }).port; 59 | std::cout << "port = " << port << "\n"; 60 | 61 | bool pendingAccept = false; 62 | 63 | // Run for 10 seconds 64 | using namespace std::literals; 65 | const auto start = std::chrono::steady_clock::now(); 66 | while (true) { 67 | bool timeout = std::chrono::steady_clock::now() - start > 10s; 68 | if (timeout) { 69 | s.cancelIO(); 70 | s.close(); 71 | } 72 | 73 | Async::handleEvents(); 74 | if (timeout) break; 75 | 76 | if (!pendingAccept) { 77 | pendingAccept = true; 78 | accept(s, pendingAccept); 79 | } 80 | } 81 | } 82 | 83 | int main(int argc, char** argv) { 84 | // Get number of threads from first command line argument 85 | unsigned int numThreads = 0; 86 | if (argc > 1) { 87 | char* arg = argv[1]; 88 | std::from_chars_result res = std::from_chars(arg, arg + std::strlen(arg), numThreads); 89 | if (res.ec != std::errc{}) std::cout << "Invalid number of threads specified.\n"; 90 | } 91 | 92 | unsigned int realNumThreads = Async::init(numThreads, 2048); 93 | std::cout << "Running with " << realNumThreads << " threads.\n"; 94 | 95 | run(); 96 | 97 | // Cancel remaining work on all threads 98 | Async::queueToThreadEx({}, []() -> Task { 99 | for (auto i = clients.begin(); i != clients.end(); i++) 100 | if (!i->done) i->sock->cancelIO(); 101 | 102 | co_return false; 103 | }); 104 | 105 | std::latch threadWaiter{ realNumThreads - 1 }; 106 | Async::queueToThreadEx({}, [&threadWaiter]() -> Task { 107 | if (clients.empty()) { 108 | threadWaiter.count_down(); 109 | co_return false; 110 | } 111 | 112 | std::erase_if(clients, [](const Client& client) { return client.done; }); 113 | co_return true; 114 | }); 115 | 116 | threadWaiter.wait(); 117 | Async::cleanup(); 118 | } 119 | -------------------------------------------------------------------------------- /src/os/async.linux.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "async.hpp" 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include "errcheck.hpp" 14 | #include "utils/overload.hpp" 15 | 16 | void handleOperation(io_uring& ring, const Async::Operation& next) { 17 | io_uring_sqe* sqe = io_uring_get_sqe(&ring); 18 | 19 | Overload visitor{ 20 | [=](const Async::Connect& op) { 21 | io_uring_prep_connect(sqe, op.handle, op.addr, op.addrLen); 22 | io_uring_sqe_set_data(sqe, op.result); 23 | }, 24 | [=](const Async::Accept& op) { 25 | io_uring_prep_accept(sqe, op.handle, op.addr, op.addrLen, 0); 26 | io_uring_sqe_set_data(sqe, op.result); 27 | }, 28 | [=](const Async::Send& op) { 29 | io_uring_prep_send(sqe, op.handle, op.data.data(), op.data.size(), MSG_NOSIGNAL); 30 | io_uring_sqe_set_data(sqe, op.result); 31 | }, 32 | [=](const Async::SendTo& op) { 33 | io_uring_prep_sendto(sqe, op.handle, op.data.data(), op.data.size(), MSG_NOSIGNAL, op.addr, op.addrLen); 34 | io_uring_sqe_set_data(sqe, op.result); 35 | }, 36 | [=](const Async::Receive& op) { 37 | io_uring_prep_recv(sqe, op.handle, op.data.data(), op.data.size(), MSG_NOSIGNAL); 38 | io_uring_sqe_set_data(sqe, op.result); 39 | }, 40 | [=](const Async::ReceiveFrom& op) { 41 | io_uring_prep_recvmsg(sqe, op.handle, op.msg, MSG_NOSIGNAL); 42 | io_uring_sqe_set_data(sqe, op.result); 43 | }, 44 | [=](const Async::Shutdown& op) { 45 | io_uring_prep_shutdown(sqe, op.handle, SHUT_RDWR); 46 | io_uring_sqe_set_data(sqe, nullptr); 47 | }, 48 | [=](const Async::Close& op) { 49 | io_uring_prep_close(sqe, op.handle); 50 | io_uring_sqe_set_data(sqe, nullptr); 51 | }, 52 | [=](const Async::Cancel& op) { 53 | io_uring_prep_cancel_fd(sqe, op.handle, IORING_ASYNC_CANCEL_ALL); 54 | io_uring_sqe_set_data(sqe, nullptr); 55 | }, 56 | }; 57 | 58 | std::visit(visitor, next); 59 | } 60 | 61 | Async::EventLoop::EventLoop(unsigned int, unsigned int queueEntries) { 62 | io_uring_params params; 63 | std::memset(¶ms, 0, sizeof(params)); 64 | params.flags = IORING_SETUP_SINGLE_ISSUER; 65 | 66 | check(io_uring_queue_init_params(queueEntries, &ring, ¶ms), checkZero, useReturnCodeNeg); 67 | } 68 | 69 | Async::EventLoop::~EventLoop() { 70 | io_uring_queue_exit(&ring); 71 | } 72 | 73 | void Async::EventLoop::runOnce(bool wait) { 74 | __kernel_timespec timeout{ 0, wait ? 200000000 : 0 }; 75 | io_uring_cqe* cqe = nullptr; 76 | 77 | if (operations.empty()) { 78 | if (numOperations == 0) return; 79 | 80 | if (io_uring_wait_cqe_timeout(&ring, &cqe, &timeout) < 0) return; 81 | } else { 82 | // There are queued operations, process them 83 | for (const auto& i : operations) handleOperation(ring, i); 84 | numOperations += operations.size(); 85 | operations.clear(); 86 | 87 | // Submit to io_uring and wait for next CQE 88 | if (io_uring_submit_and_wait_timeout(&ring, &cqe, 1, &timeout, nullptr) < 0) return; 89 | } 90 | 91 | if (!cqe) return; 92 | 93 | void* userData = io_uring_cqe_get_data(cqe); 94 | io_uring_cqe_seen(&ring, cqe); 95 | numOperations--; 96 | 97 | if (!userData) return; 98 | 99 | // Fill in completion result information 100 | auto& result = *reinterpret_cast(userData); 101 | if (cqe->res < 0) result.error = -cqe->res; 102 | else result.res = cqe->res; 103 | 104 | result.coroHandle(); 105 | } 106 | -------------------------------------------------------------------------------- /tests/src/https.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "helpers/helpers.hpp" 13 | #include "net/enums.hpp" 14 | #include "sockets/clientsockettls.hpp" 15 | #include "sockets/delegates/delegates.hpp" 16 | #include "utils/task.hpp" 17 | 18 | // Matcher to check for specific TLS error cases. 19 | struct TLSErrorMatcher : Catch::Matchers::MatcherGenericBase { 20 | std::string expectedError; 21 | 22 | TLSErrorMatcher(std::string_view expectedError) : expectedError(expectedError) {} 23 | 24 | bool match(const Botan::TLS::TLS_Exception& e) const { 25 | return e.what() == expectedError; 26 | } 27 | 28 | std::string describe() const override { 29 | return "Is a TLS error"; 30 | } 31 | }; 32 | 33 | TEST_CASE("HTTPS") { 34 | // Security check with howsmyssl.com 35 | SECTION("TLS check") { 36 | runSync([]() -> Task<> { 37 | ClientSocketTLS sock; 38 | co_await sock.connect({ ConnectionType::TCP, "", "www.howsmyssl.com", 443 }); 39 | 40 | // Send HTTP API request 41 | co_await sock.send("GET /a/check HTTP/1.1\r\nHost: www.howsmyssl.com\r\nConnection: close\r\n\r\n"); 42 | 43 | // Read response until socket closed 44 | std::string response; 45 | while (true) { 46 | RecvResult result = co_await sock.recv(1024); 47 | if (result.complete) response += result.data; 48 | 49 | if (!result.alert) continue; 50 | CHECK(result.alert->desc == "close_notify"); 51 | 52 | // Socket closure should immediately follow close alert 53 | bool closed = (co_await sock.recv(1024)).closed; 54 | CHECK(closed); 55 | break; 56 | } 57 | 58 | // Check HTTP response 59 | CHECK(response.starts_with("HTTP/1.1 200 OK")); 60 | 61 | // Check attributes 62 | CHECK(response.contains("\"ephemeral_keys_supported\":true")); 63 | CHECK(response.contains("\"session_ticket_supported\":true")); 64 | CHECK(response.contains("\"insecure_cipher_suites\":{}")); 65 | CHECK(response.contains("\"tls_version\":\"TLS 1.3\"")); 66 | CHECK(response.contains("\"rating\":\"Probably Okay\"")); 67 | }); 68 | } 69 | 70 | // Error handling checks with badssl.com 71 | 72 | SECTION("Self-signed certificate") { 73 | auto operation = []() -> Task<> { 74 | ClientSocketTLS sock; 75 | co_await sock.connect({ ConnectionType::TCP, "", "self-signed.badssl.com", 443 }); 76 | }; 77 | 78 | std::string expectedError = "Certificate validation failure: Cannot establish trust"; 79 | CHECK_THROWS_MATCHES(runSync(operation), Botan::TLS::TLS_Exception, TLSErrorMatcher{ expectedError }); 80 | } 81 | 82 | SECTION("Expired certificate") { 83 | auto operation = []() -> Task<> { 84 | ClientSocketTLS sock; 85 | co_await sock.connect({ ConnectionType::TCP, "", "expired.badssl.com", 443 }); 86 | }; 87 | 88 | std::string expectedError = "Certificate validation failure: Certificate has expired"; 89 | CHECK_THROWS_MATCHES(runSync(operation), Botan::TLS::TLS_Exception, TLSErrorMatcher{ expectedError }); 90 | } 91 | 92 | SECTION("Handshake failure") { 93 | runSync([]() -> Task<> { 94 | ClientSocketTLS sock; 95 | co_await sock.connect({ ConnectionType::TCP, "", "rc4.badssl.com", 443 }); 96 | auto alert = *(co_await sock.recv(1024)).alert; // No data is actually being received 97 | CHECK(alert.isFatal); 98 | CHECK(alert.desc == "handshake_failure"); 99 | }); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/components/console.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | 16 | // Text panel output with colors and other information. 17 | class Console { 18 | // Item in console output. 19 | struct ConsoleItem { 20 | bool canUseHex; // If the item gets displayed as hexadecimal when the option is set 21 | std::string text; // Text string 22 | std::string textHex; // Text in hexadecimal format (UTF-8 encoded) 23 | ImVec4 color; // Color 24 | std::string timestamp; // Time added 25 | std::optional hoverText; // Tooltip text 26 | }; 27 | 28 | // State 29 | bool scrollToEnd = false; // If the console is force-scrolled to the end 30 | float yScrollPos = 0; // Scroll position in vertical axis 31 | 32 | // Options 33 | bool autoscroll = true; // If console autoscrolls when new data is put 34 | bool showTimestamps = false; // If timestamps are shown in the output 35 | bool showHex = false; // If items are shown in hexadecimal 36 | 37 | std::vector items; // Items in console output 38 | 39 | // Text selection manager 40 | TextSelect textSelect{ std::bind_front(&Console::getLineAtIdx, this), 41 | std::bind_front(&Console::getNumLines, this) }; 42 | 43 | // Forces subsequent text to go on a new line. 44 | void forceNextLine() { 45 | // If there are no items, new text will have to be on its own line. 46 | if (items.empty()) return; 47 | 48 | std::string& lastItem = items.back().text; 49 | if (!lastItem.ends_with('\n')) lastItem += '\n'; 50 | } 51 | 52 | // Adds text to the console. Does not make it go on its own line. 53 | void add(std::string_view s, const ImVec4& color, bool canUseHex, std::string_view hoverText); 54 | 55 | // Draws the timestamps to the left of the content. 56 | void drawTimestamps(); 57 | 58 | // Draws the contents of the right-click context menu. 59 | void drawContextMenu(); 60 | 61 | // Draws widgets for each option for use in a menu. 62 | void drawOptions(); 63 | 64 | // Gets the line at an index. 65 | std::string_view getLineAtIdx(std::size_t i) const { 66 | const ConsoleItem& item = items[i]; 67 | return showHex && item.canUseHex ? item.textHex : item.text; 68 | } 69 | 70 | // Gets the number of lines in the output. 71 | std::size_t getNumLines() const { 72 | return items.size(); 73 | } 74 | 75 | public: 76 | // Draws the output pane. 77 | void update(std::string_view id); 78 | 79 | // Adds text to the console. Accepts multiline strings. 80 | // The color of the text can be set, as well as an optional string to show before each line. 81 | // If canUseHex is set to false, the text will never be displayed as hexadecimal. 82 | void addText(std::string_view s, std::string_view pre = "", const ImVec4& color = {}, bool canUseHex = true, 83 | std::string_view hoverText = ""); 84 | 85 | // Adds a message with a given color and description. 86 | void addMessage(std::string_view s, std::string_view desc, const ImVec4& color) { 87 | forceNextLine(); 88 | addText(s, std::format("[{}] ", desc), color, false); 89 | forceNextLine(); 90 | } 91 | 92 | // Adds a red error message. 93 | void addError(std::string_view s) { 94 | // Error messages in red 95 | forceNextLine(); 96 | addMessage(s, "ERROR", { 1.0f, 0.4f, 0.4f, 1.0f }); 97 | forceNextLine(); 98 | } 99 | 100 | // Adds a yellow information message. 101 | void addInfo(std::string_view s) { 102 | // Information in yellow 103 | forceNextLine(); 104 | addMessage(s, "INFO ", { 1.0f, 0.8f, 0.6f, 1.0f }); 105 | forceNextLine(); 106 | } 107 | 108 | // Clears the output. 109 | void clear() { 110 | items.clear(); 111 | } 112 | }; 113 | -------------------------------------------------------------------------------- /src/sockets/delegates/secure/clienttls.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "clienttls.hpp" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | class CredentialsManager : public Botan::Credentials_Manager { 21 | Botan::System_Certificate_Store systemCertStore; 22 | 23 | public: 24 | std::vector trusted_certificate_authorities(const std::string&, 25 | const std::string&) override { 26 | return { &systemCertStore }; 27 | } 28 | }; 29 | 30 | class TLSCallbacks : public Botan::TLS::Callbacks { 31 | Delegates::ClientTLS& io; 32 | 33 | public: 34 | explicit TLSCallbacks(Delegates::ClientTLS& io) : io(io) {} 35 | 36 | // The TLS channel is used as an adapter that takes unencrypted data and outputs encrypted data into a send queue 37 | void tls_emit_data(std::span buf) override { 38 | io.queueWrite({ reinterpret_cast(buf.data()), buf.size() }); 39 | } 40 | 41 | void tls_record_received(std::uint64_t, std::span buf) override { 42 | io.queueRead({ reinterpret_cast(buf.data()), buf.size() }); 43 | } 44 | 45 | void tls_alert(Botan::TLS::Alert alert) override { 46 | io.setAlert(alert); 47 | if (alert.is_fatal()) io.close(); // Fatal alerts deactivate connections 48 | } 49 | 50 | std::chrono::milliseconds tls_verify_cert_chain_ocsp_timeout() const override { 51 | using namespace std::literals; 52 | return 2000ms; 53 | } 54 | }; 55 | 56 | Task<> Delegates::ClientTLS::sendQueued() { 57 | // Send encrypted data until queue is empty 58 | while (!pendingWrites.empty()) { 59 | std::string data = pendingWrites.front(); 60 | pendingWrites.pop(); 61 | co_await baseIO.send(data); 62 | } 63 | } 64 | 65 | Task Delegates::ClientTLS::recvBase(std::size_t size) { 66 | auto recvResult = co_await baseIO.recv(size); 67 | 68 | if (recvResult.closed) channel->close(); 69 | else channel->received_data(reinterpret_cast(recvResult.data.data()), recvResult.data.size()); 70 | 71 | co_return recvResult.closed; 72 | } 73 | 74 | void Delegates::ClientTLS::close() { 75 | if (channel && channel->is_active()) channel->close(); // Send close message to peer (must be before closing handle) 76 | 77 | handle.close(); 78 | } 79 | 80 | Task<> Delegates::ClientTLS::connect(Device device) { 81 | co_await baseClient.connect(device); 82 | 83 | const auto rng = std::make_shared(); 84 | channel = std::make_unique(std::make_shared(*this), 85 | std::make_shared(rng), std::make_shared(), 86 | std::make_shared(), rng, Botan::TLS::Server_Information{ device.address, device.port }); 87 | 88 | // Perform TLS handshake until channel is active 89 | do { 90 | // Client initiates handshake to server; send before receiving 91 | co_await sendQueued(); 92 | bool closed = co_await recvBase(1024); 93 | if (closed) break; 94 | } while (!channel->is_active() && !channel->is_closed()); 95 | } 96 | 97 | Task<> Delegates::ClientTLS::send(std::string data) { 98 | if (channel) { 99 | channel->send(data); 100 | co_await sendQueued(); 101 | } 102 | } 103 | 104 | Task Delegates::ClientTLS::recv(std::size_t size) { 105 | // A record may take multiple receive calls to come in 106 | if (completedReads.empty()) { 107 | if (co_await recvBase(size)) co_return { true, true, "", std::nullopt }; 108 | 109 | co_return { false, false, "", std::nullopt }; 110 | } 111 | 112 | auto queuedData = completedReads.front(); 113 | completedReads.pop(); 114 | co_return queuedData; 115 | } 116 | -------------------------------------------------------------------------------- /paper/paper.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "WhaleConnect: A General-Purpose, Cross-Platform Network Communication Application" 3 | tags: 4 | - C++ 5 | - IoT 6 | - Network communication 7 | - Multithreading 8 | - Cross-platform 9 | - GUI 10 | authors: 11 | - name: Aidan Sun 12 | orcid: 0009-0000-6282-2497 13 | affiliation: 1 14 | affiliations: 15 | - name: Independent Researcher, United States 16 | index: 1 17 | date: 28 June 2024 18 | bibliography: paper.bib 19 | --- 20 | 21 | # Summary 22 | 23 | WhaleConnect, a general-purpose and cross-platform network communication application, aims to overcome existing network communication challenges, such as data security, efficiency when handling parallel connections, and compatibility of communication between diverse technologies. It runs on Windows, macOS, and Linux, and it supports various communication protocols. Highly scalable communication is achieved through parallel processing and operating system features, and the application has an intuitive graphical user interface. Through these features, WhaleConnect is designed to foster the development of network-enabled systems and applications, such as the Internet of Things, an emerging and rapidly growing field that involves network communication and data transfer between smart devices. 24 | 25 | # Statement of need 26 | 27 | Computer networking is a vital component of modern computing that allows devices such as computers to communicate and share information. Having a global market size growth of 22% in 2021 to hit $157.9 billion, one emerging paradigm employing computer networking is the Internet of Things (IoT). IoT combines energy-efficient microcontrollers with sensors and actuators to create interconnected smart devices, such as smart traffic signals and automatic home lighting systems [@Elgazzar_Khalil_Alghamdi_Badr_Abdelkader_Elewah_Buyya_2022]. However, some crucial challenges in the development of IoT-based systems are security (ensuring the integrity of user data), scalability (supporting a large number of connected devices without degrading the performance of the system), and interoperability (being able to support the exchange of information across different technologies and platforms) [@Kumar_Tiwari_Zymbler_2019]. Many tools currently implement network communication for simple IoT projects, though none completely address these challenges. 28 | 29 | This application, WhaleConnect, is an open source network communication tool that supports Windows, macOS, and Linux. It implements communication through TCP and UDP over the Internet Protocol and L2CAP and RFCOMM over Bluetooth, enabling interoperability with a wide range of IoT and wireless-enabled devices. It also supports the TLS protocol for reliable data security and encryption. Additionally, it can function as both a client and a server, allowing it to provide services and information to some devices and request them from others to offer a complete solution for control. Scalable parallel communication is enabled by leveraging resources provided by the operating system. 30 | 31 | All functionality is exposed through a graphical user interface (GUI), offering a seamless and intuitive user experience, especially when managing multiple connections over various protocols and devices. The user interface also offers other useful functions that can aid in diagnostics, such as displaying timestamps, hex dumps, and logs of sent data. Overall, WhaleConnect addresses many key challenges in the field of IoT, promoting further research and development in this rapidly-growing field. WhaleConnect aims to be user-friendly and widely used by researchers, developers, hobbyists, and the industry. 32 | 33 | # Architecture 34 | 35 | WhaleConnect uses multithreading in conjunction with high-performance kernel functions — IOCP [@IO_Completion_Ports] on Windows, kqueue [@kqueue] and IOBluetooth [@IOBluetooth] on macOS, and io_uring via liburing [@Axboe_liburing_library_for] on Linux — to fully benefit from computer hardware and offer a high degree of scalability when managing multiple connections at once. In addition, it uses coroutines, introduced in the C++ 2020 standard [@Coroutines_Cpp20], to efficiently manage concurrent connections within each thread. \autoref{FIG:Architecture} presents an architecture diagram displaying the interactions between threads, the operating system's kernel, and the user interface. 36 | 37 | ![Architecture diagram of WhaleConnect \label{FIG:Architecture}](architecture.png) 38 | 39 | # Acknowledgements 40 | 41 | This research has received no external funding. 42 | 43 | # References 44 | -------------------------------------------------------------------------------- /src/gui/newconnbt.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "newconnbt.hpp" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | 16 | #include "components/sdpwindow.hpp" 17 | #include "components/windowlist.hpp" 18 | #include "net/btutils.hpp" 19 | #include "net/device.hpp" 20 | #include "os/error.hpp" 21 | #include "utils/overload.hpp" 22 | 23 | void sortTable(std::vector& devices) { 24 | // A sort is only needed for 2 or more entries 25 | if (devices.size() < 2) return; 26 | 27 | // Get sort specs 28 | auto sortSpecs = ImGui::TableGetSortSpecs(); 29 | if (!sortSpecs) return; 30 | if (!sortSpecs->SpecsDirty) return; 31 | 32 | // Get sort data 33 | auto spec = sortSpecs->Specs[0]; 34 | auto proj = spec.ColumnIndex == 0 ? &Device::name : &Device::address; 35 | 36 | // Sort according to user specified direction 37 | if (spec.SortDirection == ImGuiSortDirection_Ascending) std::ranges::stable_sort(devices, std::less{}, proj); 38 | else std::ranges::stable_sort(devices, std::greater{}, proj); 39 | 40 | sortSpecs->SpecsDirty = false; 41 | } 42 | 43 | // Draws a menu composed of the paired Bluetooth devices. 44 | const Device* drawPairedDevices(std::vector& devices, bool needsSort) { 45 | // Using a pointer so the return value can be nullable without copying large objects. 46 | const Device* ret = nullptr; 47 | 48 | // Setup table 49 | int numCols = 3; 50 | ImGuiTableFlags flags 51 | = ImGuiTableFlags_Borders | ImGuiTableFlags_Sortable | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY; 52 | if (!ImGui::BeginTable("paired", numCols, flags)) return ret; 53 | 54 | ImGui::TableSetupColumn("Name"); 55 | ImGui::TableSetupColumn("Address"); 56 | ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_NoSort); 57 | ImGui::TableSetupScrollFreeze(numCols, 1); 58 | ImGui::TableHeadersRow(); 59 | 60 | // The default sort is ascending according to column 0 61 | if (needsSort) ImGui::TableSetColumnSortDirection(0, ImGuiSortDirection_Ascending, false); 62 | sortTable(devices); 63 | 64 | // Render each table row 65 | for (const auto& i : devices) { 66 | ImGui::TableNextRow(); 67 | ImGui::TableNextColumn(); 68 | ImGui::Text("%s", i.name.c_str()); 69 | 70 | ImGui::TableNextColumn(); 71 | ImGui::Text("%s", i.address.c_str()); 72 | 73 | ImGui::TableNextColumn(); 74 | ImGui::PushID(i.address.c_str()); 75 | if (ImGui::Button("Connect")) ret = &i; 76 | ImGui::PopID(); 77 | } 78 | 79 | ImGui::EndTable(); 80 | return ret; 81 | } 82 | 83 | void drawBTConnectionTab(WindowList& connections, WindowList& sdpWindows) { 84 | if (!ImGui::BeginTabItem("Bluetooth")) return; 85 | 86 | static std::variant, System::SystemError> pairedDevices; 87 | bool needsSort = false; 88 | 89 | // Get paired devices if the button is clicked or if the variant currently holds nothing 90 | if (ImGui::Button("Refresh List") || pairedDevices.index() == 0) { 91 | try { 92 | pairedDevices = BTUtils::getPaired(); 93 | needsSort = true; 94 | } catch (const System::SystemError& error) { 95 | pairedDevices = error; 96 | } 97 | } 98 | 99 | // Check paired devices list 100 | Overload visitor{ 101 | [](std::monostate) { /* Nothing to do */ }, 102 | [needsSort, &connections, &sdpWindows](std::vector& deviceList) { 103 | // Check if the device list is empty 104 | if (deviceList.empty()) { 105 | ImGui::Text("No paired devices."); 106 | return; 107 | } 108 | 109 | ImGui::Spacing(); 110 | 111 | // There are devices, display them 112 | if (auto device = drawPairedDevices(deviceList, needsSort)) { 113 | std::string title = std::format("Connect To {}##{}", device->name, device->address); 114 | sdpWindows.add(title, *device, connections); 115 | } 116 | }, 117 | [](const System::SystemError& error) { 118 | // Error occurred 119 | ImGui::TextWrapped("Error %s", error.what()); 120 | }, 121 | }; 122 | 123 | std::visit(visitor, pairedDevices); 124 | ImGui::EndTabItem(); 125 | } 126 | -------------------------------------------------------------------------------- /tests/scripts/server.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | # SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | # This script is intended to support testing of WhaleConnect. 5 | 6 | import argparse 7 | import configparser 8 | import pathlib 9 | import socket 10 | import sys 11 | 12 | # Command-line arguments to set server configuration 13 | parser = argparse.ArgumentParser() 14 | parser.add_argument("-t", "--transport", type=str, required=True) 15 | 16 | mode = parser.add_mutually_exclusive_group(required=True) 17 | mode.add_argument("-i", "--interactive", action="store_true") 18 | mode.add_argument("-e", "--echo", action="store_true") 19 | 20 | args = parser.parse_args() 21 | is_interactive = args.interactive 22 | is_echo = args.echo 23 | 24 | # Load settings from INI 25 | config = configparser.ConfigParser() 26 | config.read(pathlib.Path(__file__).parent.parent / "settings" / "settings.ini") 27 | 28 | match args.transport: 29 | case "TCP": 30 | SOCKET_TYPE = socket.SOCK_STREAM 31 | SOCKET_FAMILY = socket.AF_INET6 32 | SOCKET_PROTO = -1 33 | PORT = int(config["ip"]["tcpPort"]) 34 | HOST = "::" 35 | case "UDP": 36 | SOCKET_TYPE = socket.SOCK_DGRAM 37 | SOCKET_FAMILY = socket.AF_INET6 38 | SOCKET_PROTO = -1 39 | PORT = int(config["ip"]["udpPort"]) 40 | HOST = "::" 41 | case "RFCOMM": 42 | if sys.platform == "linux": 43 | SOCKET_TYPE = socket.SOCK_STREAM 44 | SOCKET_FAMILY = socket.AF_BLUETOOTH 45 | SOCKET_PROTO = socket.BTPROTO_RFCOMM 46 | PORT = int(config["bluetooth"]["rfcommPort"]) 47 | HOST = socket.BDADDR_ANY 48 | else: 49 | raise Exception("RFCOMM is only available on Linux") 50 | case "L2CAP": 51 | if sys.platform == "linux": 52 | SOCKET_TYPE = socket.SOCK_SEQPACKET 53 | SOCKET_FAMILY = socket.AF_BLUETOOTH 54 | SOCKET_PROTO = socket.BTPROTO_L2CAP 55 | PORT = int(config["bluetooth"]["l2capPSM"]) 56 | HOST = socket.BDADDR_ANY 57 | else: 58 | raise Exception("L2CAP is only available on Linux") 59 | case _: 60 | raise Exception("Unsupported transport") 61 | 62 | 63 | # Handles I/O for a TCP server. 64 | def server_loop_tcp(s: socket.socket) -> None: 65 | # Wait for new client connections 66 | conn, addr = s.accept() 67 | peer_addr = addr[0] 68 | 69 | print(f"New connection from {peer_addr}") 70 | 71 | with conn: 72 | while True: 73 | # Wait for new data to be received 74 | data = conn.recv(1024) 75 | if data: 76 | print(f"Received: {data.decode()}") 77 | 78 | if is_interactive: 79 | input_str = input("> ") 80 | conn.sendall(input_str.encode()) 81 | elif is_echo: 82 | # Send back data 83 | conn.sendall(data) 84 | else: 85 | # Connection closed by client 86 | print(f"Disconnected from {peer_addr}") 87 | conn.close() 88 | break 89 | 90 | 91 | # Handles I/O for a UDP server. 92 | def server_loop_udp(s: socket.socket) -> None: 93 | data, addr = s.recvfrom(1024) 94 | if data: 95 | print(f"Received: {data} (from {addr[0]})") 96 | 97 | if is_interactive: 98 | input_str = input("> ") 99 | s.sendto(input_str.encode(), addr) 100 | elif is_echo: 101 | s.sendto(data, addr) 102 | 103 | 104 | if is_interactive: 105 | print("Running in interactive mode.") 106 | elif is_echo: 107 | print("Running in echo mode.") 108 | 109 | with socket.socket(SOCKET_FAMILY, SOCKET_TYPE, SOCKET_PROTO) as s: 110 | # Enable dual-stack server so IPv4 and IPv6 clients can connect 111 | if SOCKET_FAMILY == socket.AF_INET6: 112 | s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) 113 | 114 | s.bind((HOST, PORT)) 115 | 116 | # Listen on connection-based sockets 117 | is_dgram = SOCKET_TYPE == socket.SOCK_DGRAM 118 | if not is_dgram: 119 | s.listen() 120 | 121 | print(f"Server is active on port {PORT}") 122 | 123 | # Handle clients until termination 124 | while True: 125 | try: 126 | if is_dgram: 127 | server_loop_udp(s) 128 | else: 129 | server_loop_tcp(s) 130 | except IOError as e: 131 | print("I/O Error:", e) 132 | break 133 | except KeyboardInterrupt as e: 134 | print("Interrupted") 135 | break 136 | -------------------------------------------------------------------------------- /src/utils/settingsparser.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2025 Aidan Sun and the WhaleConnect contributors 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | #include "settingsparser.hpp" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "uuids.hpp" 16 | 17 | std::string trim(std::string_view s) { 18 | std::size_t first = s.find_first_not_of(" "); 19 | std::size_t last = s.find_last_not_of(" "); 20 | 21 | std::size_t start = first == std::string::npos ? 0 : first; 22 | std::size_t end = last == std::string::npos ? 0 : last - start + 1; 23 | 24 | return std::string{ s.substr(start, end) }; 25 | } 26 | 27 | template <> 28 | ParseResult parse(std::string_view data) { 29 | const char* p = data.data(); 30 | int lengthIdx = 0; 31 | 32 | auto extractHex = [&](auto& arg) { 33 | // Lengths of UUID segments in characters 34 | static constexpr std::array expectedLengths{ 8, 4, 4, 4, 12 }; 35 | 36 | auto [ptrTmp, ec] = std::from_chars(p, data.data() + data.size(), arg, 16); 37 | bool success = ec == std::errc{}; 38 | bool lengthValid = ptrTmp - p == expectedLengths[lengthIdx++]; 39 | bool delimValid = *ptrTmp == '-' || lengthIdx == 5; // At delimiter or end of string 40 | 41 | if (success) arg = UUIDs::byteSwap(arg); 42 | 43 | p = ptrTmp + 1; 44 | return success && lengthValid && delimValid; 45 | }; 46 | 47 | auto process = [&](auto&... args) { return (extractHex(args) && ...); }; 48 | 49 | std::uint32_t data1; 50 | std::uint16_t data2; 51 | std::uint16_t data3; 52 | std::uint16_t data4; 53 | std::uint64_t data5; 54 | 55 | UUIDs::UUID128 ret; 56 | if (process(data1, data2, data3, data4, data5)) { 57 | std::memcpy(ret.data(), &data1, 4); 58 | std::memcpy(ret.data() + 4, &data2, 2); 59 | std::memcpy(ret.data() + 6, &data3, 2); 60 | std::memcpy(ret.data() + 8, &data4, 2); 61 | std::memcpy(ret.data() + 10, reinterpret_cast(&data5) + 2, 6); 62 | return ret; 63 | } 64 | return std::nullopt; 65 | } 66 | 67 | template <> 68 | std::string stringify(const UUIDs::UUID128& in) { 69 | const auto data1 = UUIDs::byteSwap(*reinterpret_cast(in.data())); 70 | const auto data2 = UUIDs::byteSwap(*reinterpret_cast(in.data() + 4)); 71 | const auto data3 = UUIDs::byteSwap(*reinterpret_cast(in.data() + 6)); 72 | const auto data4 = UUIDs::byteSwap(*reinterpret_cast(in.data() + 8)); 73 | 74 | return std::format("{:08X}-{:04X}-{:04X}-{:04X}-{:012X}", data1, data2, data3, data4 >> 48, 75 | data4 & 0xFFFFFFFFFFFFULL); 76 | } 77 | 78 | void SettingsParser::load(const std::filesystem::path& filePath) { 79 | std::ifstream f{ filePath }; 80 | if (!f.is_open()) return; 81 | 82 | std::string line; 83 | std::string section; 84 | std::optional possibleArrayKey; 85 | 86 | while (std::getline(f, line)) { 87 | // Ignore comments and empty lines 88 | if (trim(line).empty() || line.front() == ';') continue; 89 | 90 | // Extract section name 91 | if (line.front() == '[' && line.back() == ']') { 92 | section = line.substr(1, line.size() - 2); 93 | possibleArrayKey = std::nullopt; 94 | continue; 95 | } 96 | 97 | // Parse each line as a key:string = value:string pair 98 | auto parsed = parse>(line); 99 | bool indented = line.starts_with(" "); 100 | if (!parsed || indented) { 101 | // Add indented lines to an array 102 | if (indented && possibleArrayKey) data[{ section, *possibleArrayKey }] += "\n" + line; 103 | 104 | // Skip invalid parses 105 | continue; 106 | } 107 | 108 | const auto& [key, value] = *parsed; 109 | data[{ section, key }] = value; 110 | 111 | // If there is no value, it is possible the next line starts an array 112 | possibleArrayKey = value.empty() ? std::optional{ key } : std::nullopt; 113 | } 114 | } 115 | 116 | void SettingsParser::write(const std::filesystem::path& filePath) const { 117 | std::ofstream f{ filePath }; 118 | 119 | std::string currentKey; 120 | for (const auto& [keys, value] : data) { 121 | const auto& [section, key] = keys; 122 | if (section != currentKey) { 123 | f << "[" << section << "]\n"; 124 | currentKey = section; 125 | } 126 | 127 | f << stringify(std::pair{ key, value }) << "\n"; 128 | } 129 | } 130 | --------------------------------------------------------------------------------