├── .gitignore ├── tests ├── exec.sh ├── exec.bat ├── certs │ ├── ca.crt │ ├── alien │ │ ├── ca.crt │ │ ├── client.key │ │ ├── server.key │ │ ├── client.crt │ │ └── server.crt │ ├── client.key │ ├── server.key │ ├── client.crt │ └── server.crt ├── exec_tests.cpp ├── smime_tests.cpp ├── utils_tests.cpp ├── udp_tests.cpp ├── tcp_tests.cpp ├── ssl_tests.cpp └── plexus_tests.cpp ├── debian ├── copyright ├── README.Debian └── changelog ├── src └── plexus │ ├── udp.cpp │ ├── network.h │ ├── tcp.cpp │ ├── utils.h │ ├── plexus.h │ ├── utils.cpp │ ├── binder.cpp │ ├── socket.h │ ├── features.h │ ├── smime.cpp │ ├── exec.cpp │ ├── plexus.cpp │ ├── main.cpp │ └── stun.cpp ├── vcpkg.json ├── cmake └── plexus-config.cmake.in ├── CHANGELOG.md ├── README.md ├── LICENSE.txt └── CMakeLists.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .cache/** 3 | *.d 4 | *.o 5 | build/** 6 | .idea -------------------------------------------------------------------------------- /tests/exec.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ $# -lt 6 ]]; then 4 | echo "not enough arguments" 5 | exit 1 6 | fi 7 | 8 | loc_ip=$1 9 | loc_port=$2 10 | map_ip=$3 11 | map_port=$4 12 | rem_ip=$5 13 | rem_port=$6 14 | ifconfig= 15 | 16 | lhs=${map_ip}${map_port} 17 | rhs=${rem_ip}${rem_port} 18 | 19 | if [ $lhs \> $rhs ]; then 20 | ifconfig="10.0.0.3 255.255.255.0" 21 | else 22 | ifconfig="10.0.0.2 255.255.255.0" 23 | fi 24 | 25 | openvpn --dev tap --ifconfig ${ifconfig} --local ${loc_ip} --lport ${loc_port} --remote ${rem_ip} --rport ${rem_port} 26 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: plexus 3 | Source: https://github.com/novemus/plexus 4 | 5 | Files: * 6 | Copyright: 2023 Novemus Band. 7 | License: Apache-2.0 8 | Licensed under the Apache License, Version 2.0 (the "License"). 9 | You may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | . 13 | On Debian systems, the complete text of the Apache License Version 2.0 14 | can be found in `/usr/share/common-licenses/Apache-2.0'. 15 | -------------------------------------------------------------------------------- /debian/README.Debian: -------------------------------------------------------------------------------- 1 | plexus for Debian 2 | ================= 3 | 4 | This package contains the 'plexus' utility. 5 | 6 | The 'plexus' utility provides the ability to establish a direct network 7 | connection between your UDP applications running on machines 8 | located behind NATs. To do this, plexus implements the well-known 9 | UDP hole punching technique, using the STUN server and email service 10 | to exchange public IP addresses between the app instances running 11 | on local and remote hosts. 12 | 13 | https://github.com/novemus/plexus 14 | 15 | -- Novemus Band Tue, 26 Mar 2024 21:50:00 +0300 16 | -------------------------------------------------------------------------------- /tests/exec.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set argc=0 4 | for %%x in (%*) do ( 5 | set /a argc+=1 6 | ) 7 | 8 | if %argc% lss 6 ( 9 | echo "not enough arguments" 10 | goto end 11 | ) 12 | 13 | set loc_ip=%1 14 | set loc_port=%2 15 | set map_ip=%3 16 | set map_port=%4 17 | set rem_ip=%5 18 | set rem_port=%6 19 | set ifconfig= 20 | 21 | set lhs=%map_ip%%map_port% 22 | set rhs=%rem_ip%%rem_port% 23 | 24 | if "%lhs%" GTR "%rhs%" ( 25 | set ifconfig=10.0.0.3 255.255.255.0 26 | ) else ( 27 | set ifconfig=10.0.0.2 255.255.255.0 28 | ) 29 | 30 | "C:/Program Files/OpenVPN/bin/openvpn.exe" --dev tap --ifconfig %ifconfig% --local %loc_ip% --lport %loc_port% --remote %rem_ip% --rport %rem_port% 31 | 32 | :end 33 | -------------------------------------------------------------------------------- /src/plexus/udp.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Novemus Band. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | */ 10 | 11 | #include 12 | 13 | namespace plexus { namespace network { 14 | 15 | std::shared_ptr create_udp_transport(boost::asio::io_context& io, const boost::asio::ip::udp::endpoint& bind) 16 | { 17 | auto socket = std::make_shared(bind.protocol(), io); 18 | socket->bind(bind); 19 | 20 | return socket; 21 | } 22 | 23 | }} 24 | -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | "boost-system", 4 | "boost-asio", 5 | "boost-test", 6 | "boost-lexical-cast", 7 | "boost-program-options", 8 | "boost-tokenizer", 9 | "boost-bind", 10 | "boost-date-time", 11 | "boost-coroutine", 12 | "fmt", 13 | "pkgconf", 14 | { 15 | "name": "shiftmedia-libgnutls", 16 | "platform": "windows" 17 | }, 18 | { 19 | "name": "libgnutls", 20 | "platform": "osx" 21 | }, 22 | "openssl", 23 | "msgpack", 24 | "nettle", 25 | "argon2", 26 | "jsoncpp" 27 | ], 28 | "builtin-baseline": "ef7dbf94b9198bc58f45951adcf1f041fcbc5ea0" 29 | } -------------------------------------------------------------------------------- /tests/certs/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDLzCCAhegAwIBAgIJAICKZyic69fHMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV 3 | BAMMCVBsZXh1cyBDQTAeFw0yMjA0MjMyMzIwNTFaFw0zMjA0MjAyMzIwNTFaMBQx 4 | EjAQBgNVBAMMCVBsZXh1cyBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 5 | ggEBAOUgeLWkTHhV51P3txR9yKwTcUgjLWIknH2qT2rflPyesFqFJham/8oVFEan 6 | v7Gw5ki3nTYil0Y6fAWACcIGcUhiqlPGfu8s8xCjyZlj3laPc8N7Xm3xtPc+pmVC 7 | 8I6EN/cyD0Tj1gIntmU8j3rEWlldt9sWe/1SKb2KMLKCfvnJcO/i+BrOKcylVYHB 8 | 0kSc+FKBl/0mrsc+rjgCMAQ3eZfGu4DVx6fTRO8u5Ru1+6Sp6V/tOBq22KYB81n1 9 | 8BrwVq/c8j7JZT0W845c49VrwEwE9Os2tgk0OvPCZm69L8Vwj4tsyE0KBhVzkAvZ 10 | mJ0L6ZevaM3dH35K6CsNRzhyrdECAwEAAaOBgzCBgDAdBgNVHQ4EFgQUEI5Zz/Z3 11 | 7lCgfZuOHLe7lGyNQGMwRAYDVR0jBD0wO4AUEI5Zz/Z37lCgfZuOHLe7lGyNQGOh 12 | GKQWMBQxEjAQBgNVBAMMCVBsZXh1cyBDQYIJAICKZyic69fHMAwGA1UdEwQFMAMB 13 | Af8wCwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBCwUAA4IBAQDWTKhUj3WfITC7kf63 14 | +PM3IEwYOv33jBjgo7t/d91wuB1EOEek2ezK0PgWyLeVXRcWwjRZ9GY3XS7WLO6D 15 | LNXYahwjOiRc/yZIQwbALsIn5IFe+Od6cD0iKw/1GoMEQ16la5R/6sfVlt1dV6Ga 16 | 9gJQKsTNiq0a1dAh9sp6/IyZxrVVxCrVESotL2jqXamQtQIUTahJVIikoWP/Et8y 17 | eHMq6SltgpGuTUJvMMhpYsZQVJ5TWQD1rERp4vaOXi4aJFdd9GbK6iEK9z6nkg3c 18 | BneETnRV4DgxOQYdEyywKLEzEDsdo6PO7f5HvPOnuyAJNzQr5NLManVuZyYOYyMH 19 | yDsR 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /tests/exec_tests.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Novemus Band. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | */ 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | BOOST_AUTO_TEST_CASE(exec) 18 | { 19 | std::filesystem::remove("out.txt"); 20 | #ifdef _WIN32 21 | BOOST_REQUIRE_NO_THROW(plexus::exec("C:\\Windows\\System32\\cmd.exe", "/c echo line", std::filesystem::current_path().string(), "out.txt", true)); 22 | #else 23 | BOOST_REQUIRE_NO_THROW(plexus::exec("/bin/sh", "-c \"echo line\"", std::filesystem::current_path().string(), "out.txt", true)); 24 | #endif 25 | 26 | BOOST_REQUIRE(std::filesystem::exists(std::filesystem::path("out.txt"))); 27 | 28 | std::ifstream file("out.txt"); 29 | std::string text((std::istreambuf_iterator(file)), std::istreambuf_iterator()); 30 | 31 | BOOST_REQUIRE_EQUAL(text, "line\n"); 32 | 33 | file.close(); 34 | BOOST_REQUIRE(std::filesystem::remove("out.txt")); 35 | } 36 | -------------------------------------------------------------------------------- /tests/certs/alien/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDUTCCAjmgAwIBAgIUaWy1+hRVVtysZ9iNTWkVE3Dszx8wDQYJKoZIhvcNAQEL 3 | BQAwGDEWMBQGA1UEAwwNUGxleHVzLVJTQSBDQTAeFw0yMjA0MjYxNDE3MjlaFw0z 4 | MjA0MjMxNDE3MjlaMBgxFjAUBgNVBAMMDVBsZXh1cy1SU0EgQ0EwggEiMA0GCSqG 5 | SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCeP3c7hVPdZddA9XqzYis28DRL8+qEpf0u 6 | Rl3Wr7bPUTtDl60aNt8KcNimlmutRi0x+xdlBJuoqhooYFqLHGrQHQ9iTbt2obmj 7 | grCLifDpPgFraePF+GikFGnt/JjSip2ivsrs4Sla2i2VBq05DMd0OiHzLdNHzown 8 | nrwipT0LTx6NiCmlPp6pzMnIHkNPLye6av3L74ien34d4ddlrhqUT1mrn4Q10Tor 9 | 9JPCZWmL8v6eymaOF3hZ5E/GUkQPaXl8u48ALbSnXZoIsm5ns1z+Oi00wM4t3xV8 10 | dvq4+1eIUdTL5CAAoPL1VtfTnq7ioHKNAeDGIUR+Ar+oMjDnVC4pAgMBAAGjgZIw 11 | gY8wHQYDVR0OBBYEFKKI8gT3biaU8Yv3sOynN4qY834hMFMGA1UdIwRMMEqAFKKI 12 | 8gT3biaU8Yv3sOynN4qY834hoRykGjAYMRYwFAYDVQQDDA1QbGV4dXMtUlNBIENB 13 | ghRpbLX6FFVW3Kxn2I1NaRUTcOzPHzAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIB 14 | BjANBgkqhkiG9w0BAQsFAAOCAQEASs+6/1AGUKtm3B4zxZ4YnQV2s86uctezz+a/ 15 | 4DhFPtJuu3hNKsV7pMaE/VHnBHn2cho99YzcD8uhsDq0RdkiwlDzWrJJe5MKBJai 16 | z37Qo8Tdv7uESQdgVvZuriEiG65Rwtug1h86jQZkxSW9tdIz3ranrTuCw6tuAEie 17 | 7ww7I/ItrStG4T0mTqiRzTKkKKwO0bL9mYMnvqw7pkW8XJpTXiZl2hZcyIV2G8Hj 18 | 8/V/VI+6XYbeagsn2jPYHA+eA28SinD0BInNZKXwV6Z+Kc5lJPTgyq4EEopvsAev 19 | Twp/kPavdbFROsfyQJSirBOrvqwDTpZSVDTyPae3YuitVpVFIw== 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /cmake/plexus-config.cmake.in: -------------------------------------------------------------------------------- 1 | @PACKAGE_INIT@ 2 | 3 | macro(import_targets type) 4 | if(NOT EXISTS "${CMAKE_CURRENT_LIST_DIR}/plexus-${type}-targets.cmake") 5 | set(${CMAKE_FIND_PACKAGE_NAME}_NOT_FOUND_MESSAGE "plexus ${type} libraries were requested but not found") 6 | set(${CMAKE_FIND_PACKAGE_NAME}_FOUND OFF) 7 | return() 8 | endif() 9 | 10 | include("${CMAKE_CURRENT_LIST_DIR}/plexus-${type}-targets.cmake") 11 | endmacro() 12 | 13 | if(NOT TARGET plexus::libplexus) 14 | set(_type "") 15 | 16 | if(DEFINED PLEXUS_USE_SHARED_LIB) 17 | if(PLEXUS_USE_SHARED_LIB) 18 | set(_type "shared") 19 | else() 20 | set(_type "static") 21 | endif() 22 | elseif(BUILD_SHARED_LIBS AND EXISTS "${CMAKE_CURRENT_LIST_DIR}/plexus-shared-targets.cmake") 23 | set(_type "shared") 24 | elseif(EXISTS "${CMAKE_CURRENT_LIST_DIR}/plexus-static-targets.cmake") 25 | set(_type "static") 26 | else() 27 | set(_type "shared") 28 | endif() 29 | 30 | import_targets(${_type}) 31 | check_required_components(plexus) 32 | 33 | include(CMakeFindDependencyMacro) 34 | find_dependency(Boost CONFIG COMPONENTS system coroutine) 35 | find_dependency(wormhole COMPONENTS libwormhole) 36 | find_dependency(tubus COMPONENTS libtubus) 37 | 38 | get_target_property(plexus_INCLUDE_DIRS plexus::libplexus INTERFACE_INCLUDE_DIRECTORIES) 39 | set(plexus_LIBRARY plexus::libplexus) 40 | 41 | set(plexus_FOUND ON) 42 | unset(_type) 43 | endif() 44 | -------------------------------------------------------------------------------- /src/plexus/network.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Novemus Band. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | */ 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | namespace plexus { namespace network { 21 | 22 | constexpr int64_t default_tcp_timeout_ms = 10000; 23 | constexpr int64_t default_udp_timeout_ms = 1600; 24 | 25 | using tcp_socket = asio_socket; 26 | using ssl_socket = asio_socket, default_tcp_timeout_ms>; 27 | using udp_socket = asio_socket; 28 | 29 | std::shared_ptr create_ssl_client(boost::asio::io_context& io, const boost::asio::ip::tcp::endpoint& remote, const std::string& cert = "", const std::string& key = "", const std::string& ca = ""); 30 | std::shared_ptr create_tcp_client(boost::asio::io_context& io, const boost::asio::ip::tcp::endpoint& remote, const boost::asio::ip::tcp::endpoint& local); 31 | std::shared_ptr create_udp_transport(boost::asio::io_context& io, const boost::asio::ip::udp::endpoint& local = boost::asio::ip::udp::endpoint()); 32 | 33 | }} 34 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | plexus (3.2.0-1) unstable; urgency=low 2 | 3 | * Added functions to investigate NAT, forward and receive service advent. 4 | 5 | -- Novemus Band Thu, 28 Aug 2025 20:05:00 +0300 6 | 7 | plexus (3.1.4-1) unstable; urgency=low 8 | 9 | * Increased reliability of DHT rendezvous. 10 | 11 | -- Novemus Band Sun, 23 Mar 2025 17:32:00 +0300 12 | 13 | plexus (3.1.3-1) unstable; urgency=low 14 | 15 | * Fixed handshake race when using DHT rendezvous. 16 | 17 | -- Novemus Band Sat, 8 Mar 2025 10:02:30 +0300 18 | 19 | plexus (3.1.2-1) unstable; urgency=low 20 | 21 | * The CMake project has been reorganized. 22 | 23 | -- Novemus Band Wed, 26 Feb 2025 08:00:00 +0300 24 | 25 | plexus (3.1-3+deb12u3) unstable; urgency=low 26 | 27 | * Fix of DHT connectivity bugs 28 | 29 | -- Novemus Band Sun, 17 Nov 2024 22:43:20 +0300 30 | 31 | plexus (3.1-2+deb12u3) unstable; urgency=low 32 | 33 | * Set default value to --exec-args argument. 34 | 35 | -- Novemus Band Thu, 8 Aug 2024 00:07:00 +0300 36 | 37 | plexus (3.1-1+deb12u3) unstable; urgency=low 38 | 39 | * The ability to use DHT as the rendezvous. 40 | 41 | -- Novemus Band Thu, 1 Aug 2024 21:00:00 +0300 42 | 43 | plexus (3.0-2+deb12u3) unstable; urgency=low 44 | 45 | * Build with default boost library. 46 | * SMIME mailing fix. 47 | 48 | -- Novemus Band Fri, 31 May 2024 23:59:00 +0300 49 | 50 | plexus (3.0-1+deb12u3) unstable; urgency=low 51 | 52 | * Initial debian package. 53 | 54 | -- Novemus Band Tue, 26 Mar 2024 21:50:00 +0300 55 | -------------------------------------------------------------------------------- /tests/certs/client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCuagsI9tloFxG5 3 | flTrH0S4lH2SdsDgBLQkRmoA6n0uGK9wYbWiLmCuzQrZzqQqgK9He11hwtEKWDrm 4 | zss3RwgO+Lv5v/nx82El4qsAKKzb+lZEiZ0oEbUN2p/9s8OGcvWzLUK+a4m0/X4F 5 | EI/FTkv2CvFejYPPKf4yY8dprBR26xD3Z5qK0FDBRbMUzAngjPkGTQyzmXcwvQD+ 6 | phaj+1IrwseZCVmF0359SNNoR645MX2UYyF9de8FiArlYmtKJmRzVgCpnX/P4L1w 7 | uOe5QxJcbr1LwtSYt+hFO/8BGYIPQiPTPyHExcLH7atUR25WeCN2mgOYjZmhb5nB 8 | dMeRVDYjAgMBAAECggEAYCEQTfOH5gf4++y2LxT/0l/9SfFr4syU4P1yRWj4b6Sp 9 | ZV5lNqsrHIzkcyGid86PTO2gjVZL0tDIPhgyBxh+R4ouSLvXrdP/clsY9rB0fZXf 10 | YE0csk34JVefILyognzwEL5ccAljT777ax0ysIOz+DsVx5Bl2pOBiBJYgVSXvuZt 11 | TYq8c4hOi2VQaePu60QuRkzfdPGTEkoyt9FLwAxbkUG3i8m3LlyJ4pVnaDNCjV9n 12 | VXl0RZcDV2pa9AckxmYi3/L/tGlE57aEkkWyFpNVZ8BpjPj8qfr83x3h19OZyW2H 13 | Oh3dj8vSxvF6eQr+PiOgMoJKyPWfNaiB01vcNEll4QKBgQDbiJ9rSWdsSO4AeXor 14 | COsZ3C86b7mPieXzpmK2zkLbCiu+u/qU+NEcWI/xJDrNTOVpsaMtLnv9RmobXZ3L 15 | 20NevDozQYpLV9IcCvAfBrAwXLCFvETcYKCRjeChNRXvJGW6aBhrEC6KCpdBN3YZ 16 | iK3Q1thxnGEkrkfp+zD3dLXS8QKBgQDLYsace6BfCF8ZIUgDbr1d2K63Mzgia6Uk 17 | tFHkZnE4NLWf8I9fu8tkLwg7tADlUAtEOM/x7xcjKcaVC1CG/DjgNW7wUMzwd3AD 18 | 0C1D2Clo7u0augiDt+1VWtAUhgRJDbFaE7xmSADsrGII4quW5FQurSXL76BTY7el 19 | tiLuVonyUwKBgBi6XOqizAmAdX6MMFyklobFLHSynP40FDjLpxbcSN+PV7oWRnV9 20 | SWCeASiyYgU6AiVTowO11rWCtvT6KIoL2pzTKuMXINK3w6zw9ncoM7Re3GtT95Wz 21 | NKLwjjraVewn92V10DpPCk72Pm9UJfNeV+XMLXkumiqdjvAmIS/eb5ShAoGAAn30 22 | VwiAv4lXuPQacMhTk6nLAj2ja3CMNnXrJvRGYNzseMg2yYKAjHqzDCA5fpXLfYuA 23 | qMM4MlsZrsvTscO3Yq/k6xbiW/PqsHFFjLZCU6HRz+kFn6+KPeh98jJuLQL48FSb 24 | k1lAgaXfEaLNXWzwxx2h/+ywtzTalDI2HVt8FzcCgYBuZIvIg14075paZU7Oo2Qz 25 | eFvGU3V2F3TCxN0ppnQ3fivYDg2xXguJdthuWHGntXs6wIURoO19Pz05nf9Azj91 26 | 8MaWAEBwrhz27sf/yuGk9k+yB/3CvlugEhotSxnsMazLvVMLTatH3zAayC1euC76 27 | 9wn6SfxMUvKs5b+UCDKFiw== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /tests/certs/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDBnqD8D2TwCsm+ 3 | WkeJcmkyfPqx5YDwXHjLpXQwiACL8bd0KK6CaqTAKBvX0yyEehAfPlgJi/DXHUQ6 4 | t5YrUtjRtZsCeYBqNV0A3n5Rc2mym4IY/t46OfVUcjbbfOz2eYP9qC0XbYW1uj9p 5 | Df5qOv7x8+XOztdkOHIgXuvi2F14rPVlBxJeKzCN5Jr9xCzFOLRMBOxCvx1nJERi 6 | ei2IIIcc0A4YPMKKSXRlF05v3unvufuDqxaF++12p0OSepBjeuad5xYpilK7Gg+/ 7 | Er+E0NL0fPVcGZN8kJFMSlsAKTLj4P5oEMwMaB4ZcVlihq0d4yU0XgnnZs7rmQ7K 8 | zRpwOG9fAgMBAAECggEAH6luD+oAipRpCzy+KAbSlxQC9uZd4HBuvHaQ+oKy/C+J 9 | 3W/toZwI1vd4HiIFTKCdVE6gnOcKelLp3mQ5JVqF5D07XPs7eaLCU6sOowkphrCR 10 | ZL8A3iquKKIIwHm8Uk3qvKcswIEDdYVAa1CDgyGkoTJOKTR/2n60gwCiuFTDbEEq 11 | 7OLpMu1d9o+J/Okf9hqmLNbMtlqOQusLJyYbIf540ah1sPDtbi/SyNV/yNsoiX0P 12 | ZidQR9FVid8z1XbUpV0AK3KFrt1Bwq7DUqL8kaT25zznVCqMlEo974pJohhDdwbO 13 | kjJWV93nj0aVF5OEbWYl53FfEPlIbq4h3jDoNyBisQKBgQD9nmhSkRfbqxXB2tlu 14 | N9RJsDkqHA93AC/iZwYHaiF6FiiDNwKbrT1y5PlWJZpwUzrdHPRBykZPHkW8H9jB 15 | mhwCaTM4NzwwxdH5gt7ZpFFkxifb2rrzUx/RZ7czPN8Z2nKz+sJTW6hBsZ4fx52t 16 | OWuWovj4A1OmXV+YE0a6DNfuaQKBgQDDcAI82thPCQofkbo1XLgiezC9m97ogsap 17 | KWgaXLvWJ1+V7hZuc29mymxy3wPToITtEaJx3FMlitZMEmwPCewsBjOQta5Dl8Y2 18 | KbTiZfF6pTM84XiY4U4bjbcgHBrV298MXiPqUT3/37ZDzRtOXTo1BjiOQ4UjikUD 19 | GD/4ksFGhwKBgQDyTqPxLrqSnzRcT9EooqJp/LnA56pG0aDuN+vkrZtqXz05kBcn 20 | 0S5CVU165tZUttAobP9Y0/60nIGXqP2Yirpz+zp6XRtepcFlF4n+eiVhmIiAXsv6 21 | qdJeaS74/xFQTAsGtW9v4MQyb5ZZWofk9lU6SF6B5fvHmLI6Q0BxhM/Q8QKBgDRM 22 | B71DDnjvwU67TymyWu+XKW7Dg/2R6RkhJGNRh04CETjskgIST7Ag4K5IcRyzKn7J 23 | pj9zpqg4I6oNj+7iR9lr+2Zwx2X8+iRKOsYOY3TfOfvDleLK/3yuCl/ERYYOpPwJ 24 | QPEVtlJ/CaXQ/8qTddz0gjPHa5wcBuWSGqQomSGHAoGBAOrsu2FJWixN3SGmViXI 25 | xn5b+EQ0U7rZO56nlrfWge9C4y64N0UlziyZwRg9eXXxDBoObRecUSBxjPyABYct 26 | pbaKtM9179O85QeRMH71BUpGdwNDG3BJM3wAh6iwzFIFWEh70HHiwX+UlZ95wrvl 27 | Maub7gDdY0179jrQvtttoUa4 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /tests/certs/alien/client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCxENUvbgZLwN2E 3 | Xm/t2k+v8AgixvBLnOribpq0wObQop92BKDRStrCr55FdoR0vZOEKEcjmb3Ce1j/ 4 | fRRx2nKC2Sz3xMPiZEhpQTJhs4iQDzhfzOBSRG7wVRZJBm8eWQ/ni9NTQVWlSIXs 5 | 3BwET/DTNgFw/FcDPDvn1UItMcoqkLxgC7OnaYvOo1IaTah16DrqOa2b5skK1ZI4 6 | bcNOCOD/XmO28PDvDWQqzQ2Xu9sCAyU1zKoyG9aMbDInJQ7MeuhMbHAp97xhhTck 7 | f4e40cfH+3ago1MRgpRuLFYfuwezLmDnBVPAvxX3NSq9uqEd6kDzSyof5sDwlZ6b 8 | cm8b6PM1AgMBAAECggEAYvc7kJT4Y+LabR+zg8fffkC062sx57E+/2w+gATJfZdH 9 | dVlE4XXrK6Go3r5Nbkp/ZwB0Mou9qjPzHGm2waQNGOF15s5Nhf3zExFjrYQBMjOv 10 | mKwUPScOsrl8CTjVZqr03sAiT4hW7FjKsgacijR8MDEhQRtpLt+9w+hIYqZPjtYD 11 | ynRGPZWeu50I7z1LzSth0vR6UvxoRWZ8QLgDhM1OpmtDb25lovLNx9YO4I+8EjZR 12 | ANAwjLITwzpJ3LxdscqV+W62/GBeyOeCd95PY2cfNqTpW5BKZxkhw3yH6UMF99lO 13 | ta/PpX2UUBmCXdzZO5WhukQX9vgpM9n2h2YgkIGJCQKBgQDpvcOR85l6tf9FT+wt 14 | 3wXbOCp8BSAoPe92CtZZtCFFmJ06w+ehYvVUVszrkwHQEu/b595Dkm6yu1fGKxtd 15 | vRdhWl9mZlQM7o+dJtsvBIFZV7FiijRFiwOqnJCyeAi5ExUIr/5/DwIFmlHYZk5N 16 | 4fPJDESzv6HwVOCI2MHNtNARswKBgQDB7Wk2OPboieAGKcpU5wmhRrH30r+sJswv 17 | G/3lC7f5o+9mH+Cyw9HVTKR2c/E8YxY6JRD8ZtMyn7XiMzS2M+kK/e1VgFPknGo4 18 | nPQQ50JPSL7uaiXvqNP5n4YsguahdvCjS7W8xP25yam7CchxpYBupmTdl+KF6Wot 19 | IrKWbQXjdwKBgERkah0Ckh39srutLwYdA7GpNYUkWsxrku2PLjkBI9shVKAlQllO 20 | g0oyO7OSSC2B3P0TU02iQxG5D23EnzVGZtjtgIyN1HoUM6FgBTI0t0RVDGow1iqj 21 | CmeZeB1AUg0VMEpidKzoz0akaN5Q0Qio3JQvKssNf2y66RghlFOEXH1RAoGABC5f 22 | N1kREcBvQRLDlwMINR72FVbI5ShM/LMIzVhsVgiPz87eJFyOeJ/XZ+pmCBxGzWgh 23 | Lp/068F/xWm/ODJ0UtVYuaAzrtkkfSX9KSQjTNxPqYpGWR11vIsQ9IK6AD81PwAR 24 | 4gkJQ2kyrKKwXKrp0vjpcdGlg6edr21xFkcxek8CgYARyyuwm/5MMxqKVLR+5l8i 25 | o5tAGabm330/LV8801A3smzNptSD0uk6Wmrs2trZUSl42tcwyFobFdi2QOCly7gB 26 | 2+SSAmeE0NSxlQgZTREMQgzgj7jL9Z86/5Lr80gxnNR5YrZMtdeoVfhjFuIubZ2t 27 | 2tgr3S6DcfbkU377ON+nPg== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /tests/certs/alien/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDfCp3PgSgJd28z 3 | hUp4MLAeAXsQtccMSCONzO/Cbh0ZV6bMN8y/NobBu3ASgbEeAQQ3qJ0KAAbyIPp8 4 | UZzGXn3+7jW325HoLsVdnujLpjkPU49zbXQzW/b95LbQLYmM5KLBZSQ6FRBA9RW0 5 | WbG59U/nlTr6MPB38OsWUMi3+rQvWEYd8diO8+KKuT3wojZDqdJU5NVAAumtToSr 6 | RbPQYUxB9PP+uEpB3P40AN03cbV4gwhJohw0Iv+wfkU3ltT4Dx3i6l/VVKGbS2Mh 7 | zOBnI99yXxDtrN0+2649xUd6ubVjPsk5/my9UTuYz/4g31qqZUvzG7IU3cacEn0t 8 | +a5E8+71AgMBAAECggEAai7DsmXqiZcyeXSbX1zOe3p4/BuOB2rQglFuU0HY2JfB 9 | 8Lcf4mgy/0gcsrLLf108hMLhAEHDDMJmfYjc+hNCKHt/V2hQrhiWHVvpuAoKYCGH 10 | yYvyJgFCNN0Nq04CjfDLCwb7mCsu8b2794A7/CutPrLswO9Li85GHPmYlP3liiO4 11 | uPBi2NrN0gizaWd2dhwNWf/HOJ9Hn66YN/X5PlYxXpdc5NPf6jQ7HGARv4loxtcO 12 | JLqsmk8JgIB3sZofvtdWs42cyEEFLJBfqJXhkVrJZAPUVkYYt6jqiSkZaJ5M1qA1 13 | 0FQQJuwCbW9EYaf2UW0mRu97kaLDoefZYfBtkiOSgQKBgQDvGQOytVAjbMi3kAfN 14 | W9mYBQAgnIolVs6nyV4dh/MVzMRWO6kW65TfwWqcgOOfJCGrdL903KoXWpQzl9gK 15 | qIGcMls7iONUcWwfZNKsGawoQ8TuoQLVVA2Ns2wvk6Wt+eZN5RgvrYxdhdOK0kR3 16 | 6o/NYOK8pDcKAcg5sG2zCNCfTwKBgQDuzwemsXyR2cArHnqkAJrmHFK1wEzqrkaw 17 | t8cmhstLSthuiBuAi/EqpuBxSmsZFaD+V9Ue5hSTqZcpobMYWZp1QoJnmSEb896j 18 | ocGrcoTjCkKAFU8PjMs4jEmUvN+aRpSBwpHzlbVMqt2VkGhG1yx4oUXNyJ+8TUO8 19 | +i8rQ1lcewKBgArBUU1Eoqo1RoIbVH6EVzFjaDIPWJd9ZCZgBH/HkcO1Rk+qt1x0 20 | lGnMRYNsuNB7y5fu1H3pPy11wZzgg7NXCR2BlmEDM1alE3wGCQP806WsBrvFOl9m 21 | PIXXQFKTjaxZxWZTm9bpM5sxXeastq++OWG/rfNWX7NviRYvPYAEMX45AoGBAOPA 22 | 0RQ6e4qCONMmy2I0LYa+hmdOkkzTjxmk/CsTLTmOlSG9bzVGweeipbWGeuO+i/9m 23 | dxxz5DX77O3IiIz1j+i7WLFDCp+lErXOZG2F15TMySVsrS0wYvR4XwiapUp2e9XR 24 | Ye8E3ZbT8mZdBgZh2sjlTjR3XVhx++N0W54QKl6XAoGBANXde5q3aHFfD4FeHHrj 25 | pIFQipEXJwhZx9xdcZ+ce4P2sYTWwD2eGShcT5L0nQ93rXgA6itxpTQv9ZA/ZDmO 26 | mRv3R2NWsAE5FhaMCVrPyzfUBC/eZOfEPgn7UlPNyI+PigBKbBIFLgGCj5zlC1tH 27 | tnvuvBGij/wcqtVdPnCQj/SS 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /tests/smime_tests.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Novemus Band. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | */ 10 | 11 | #include 12 | #include 13 | 14 | BOOST_AUTO_TEST_CASE(smime) 15 | { 16 | std::string message = "Hello, Plexus!"; 17 | std::string verified_smime; 18 | std::string signed_smime; 19 | std::string encrypted_smime; 20 | std::string decrypted_smime; 21 | 22 | BOOST_REQUIRE_NO_THROW(signed_smime = plexus::utils::smime_sign(message, "./certs/client.crt", "./certs/client.key")); 23 | BOOST_REQUIRE_NO_THROW(encrypted_smime = plexus::utils::smime_encrypt(signed_smime, "./certs/server.crt")); 24 | 25 | BOOST_REQUIRE_NO_THROW(decrypted_smime = plexus::utils::smime_decrypt(encrypted_smime, "./certs/server.crt", "./certs/server.key")); 26 | BOOST_REQUIRE_NO_THROW(verified_smime = plexus::utils::smime_verify(decrypted_smime, "./certs/client.crt", "./certs/ca.crt")); 27 | BOOST_REQUIRE_EQUAL(verified_smime, message); 28 | 29 | BOOST_REQUIRE_NO_THROW(verified_smime = plexus::utils::smime_verify(decrypted_smime, "./certs/client.crt", "")); 30 | BOOST_REQUIRE_EQUAL(verified_smime, message); 31 | 32 | BOOST_REQUIRE_THROW(plexus::utils::smime_sign(message, "./certs/client.crt", "./certs/server.key"), std::runtime_error); 33 | BOOST_REQUIRE_THROW(plexus::utils::smime_encrypt(signed_smime, "./certs/server.key"), std::runtime_error); 34 | BOOST_REQUIRE_THROW(plexus::utils::smime_decrypt(encrypted_smime, "./certs/client.crt", "./certs/client.key"), std::runtime_error); 35 | BOOST_REQUIRE_THROW(plexus::utils::smime_verify(decrypted_smime, "./certs/server.crt", "./certs/ca.crt"), std::runtime_error); 36 | BOOST_REQUIRE_THROW(plexus::utils::smime_verify(decrypted_smime, "./certs/server.crt", ""), std::runtime_error); 37 | } 38 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 3.2.0 (Aug 28, 2025) 4 | 5 | - Added functions to investigate NAT, forward and receive service advent. 6 | 7 | ## 3.1.4 (Mar 23, 2025) 8 | 9 | - Increased reliability of DHT rendezvous. 10 | 11 | ## 3.1.3 (Mar 8, 2025) 12 | 13 | - Fixed handshake race when using DHT rendezvous. 14 | 15 | ## 3.1.2 (Feb 26, 2025) 16 | 17 | - The CMake project has been reorganized. 18 | 19 | ## 3.1.1 (Nov 17, 2024) 20 | 21 | - Link with opendht version 3.2 to stabilize the DHT rendezvous 22 | 23 | ## 3.1.0 (Aug 1, 2024) 24 | 25 | - The ability to use DHT as the rendezvous. 26 | 27 | ## 3.0.1 (May 31, 2024) 28 | 29 | - Build with default boost library. 30 | - S/MIME mailing fix. 31 | 32 | ## 3.0.0 (March 30, 2024) 33 | 34 | - Added the ability to accept several peers. Implemented the library build. 35 | 36 | ## 2.2.1 (December 18, 2023) 37 | 38 | - Added the debian build package files and improved CMake build scripts. 39 | 40 | ## 2.2.0 (October 8, 2023) 41 | 42 | - Updating `wormhole` tool to 1.1.0 version. 43 | 44 | ## 2.1.2 (May 13, 2023) 45 | 46 | - Updating `wormhole` tool to 1.0.2 version. 47 | 48 | ## 2.1.1 (February 20, 2023) 49 | 50 | - The `wormhole` tool is integrated to provide the possibility of forwarding a remote TCP service to a local interface. 51 | - The ability to specify an custom arguments for executable command, including using *plexus* wildcards. 52 | - Support for ini-like configuration files. 53 | 54 | ## 2.1.0 (September 2, 2022) 55 | 56 | - Upgraded handshake message format and added the experimental ability to punch TCP hole by SYN packet tracing. 57 | 58 | ## 2.0.1 (August 15, 2022) 59 | 60 | - Accumulated fixes and improvements. 61 | 62 | ## 2.0.0 (August 4, 2022) 63 | 64 | - Altered the procedure for punching a *passage* between peers and changed the format of plexus messages. 65 | 66 | ## 1.0.0 (July 15, 2022) 67 | 68 | - First release of the `plexus`. This tool is designed to make the possibility of establishing a direct network connection between applications running on machines located behind NATs. 69 | -------------------------------------------------------------------------------- /tests/utils_tests.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Novemus Band. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | */ 10 | 11 | #define BOOST_TEST_MODULE plexus_tests 12 | 13 | #include 14 | #include 15 | 16 | BOOST_AUTO_TEST_CASE(hexadecimal) 17 | { 18 | const uint8_t value[] = { 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef }; 19 | BOOST_CHECK_EQUAL(plexus::utils::to_hexadecimal(value, sizeof(value)), "1234567890abcdef"); 20 | } 21 | 22 | BOOST_AUTO_TEST_CASE(base64) 23 | { 24 | const std::string raw = "Lorem ipsum dolor sit amet.<>"; 25 | const std::string base64 = "TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQuPDw/IT8hPz4+"; 26 | const std::string base64nl = "TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQuPDw/IT8hPz4+\n"; 27 | const std::string base64url = "TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQuPDw_IT8hPz4-"; 28 | 29 | BOOST_CHECK_EQUAL(plexus::utils::to_base64(raw.c_str(), raw.size()), base64nl); 30 | BOOST_CHECK_EQUAL(plexus::utils::to_base64_no_nl(raw.c_str(), raw.size()), base64); 31 | BOOST_CHECK_EQUAL(plexus::utils::to_base64_url(raw.c_str(), raw.size()), base64url); 32 | 33 | BOOST_CHECK_EQUAL(plexus::utils::from_base64(base64.c_str(), base64.size()), raw); 34 | BOOST_CHECK_EQUAL(plexus::utils::from_base64(base64nl.c_str(), base64nl.size()), raw); 35 | BOOST_CHECK_EQUAL(plexus::utils::from_base64_url(base64url.c_str(), base64url.size()), raw); 36 | } 37 | 38 | BOOST_AUTO_TEST_CASE(format) 39 | { 40 | boost::posix_time::ptime time(boost::gregorian::date(2022, boost::gregorian::Mar, 20), boost::posix_time::time_duration(1, 2, 3, 456789)); 41 | BOOST_CHECK_EQUAL(plexus::utils::format("%d-%m-%Y %H:%M:%S.%f", time), "20-03-2022 01:02:03.456789"); 42 | BOOST_CHECK_EQUAL(plexus::utils::format("Epoch: %d-%m-%Y %H:%M:%S", std::chrono::system_clock::time_point()), "Epoch: 01-01-1970 00:00:00"); 43 | BOOST_CHECK_EQUAL(plexus::utils::format("%d %f %x %s", 1, 2.3f, 15, "string"), "1 2.300000 f string"); 44 | } 45 | -------------------------------------------------------------------------------- /src/plexus/tcp.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Novemus Band. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | */ 10 | 11 | #include 12 | #include 13 | 14 | namespace plexus { namespace network { 15 | 16 | class ssl_socket_impl : public ssl_socket 17 | { 18 | boost::asio::ssl::context m_ssl; 19 | 20 | public: 21 | 22 | ssl_socket_impl(const boost::asio::ip::tcp::endpoint& remote, boost::asio::io_context& io, boost::asio::ssl::context&& ssl) 23 | : ssl_socket(remote, io, ssl) 24 | , m_ssl(std::move(ssl)) 25 | { 26 | ssl_socket::lowest_layer().set_option(boost::asio::socket_base::keep_alive(true)); 27 | } 28 | }; 29 | 30 | std::shared_ptr create_tcp_client(boost::asio::io_context& io, const boost::asio::ip::tcp::endpoint& remote, const boost::asio::ip::tcp::endpoint& local) 31 | { 32 | auto socket = std::make_shared(remote, io); 33 | socket->set_option(boost::asio::socket_base::keep_alive(true)); 34 | return socket; 35 | } 36 | 37 | std::shared_ptr create_ssl_client(boost::asio::io_context& io, const boost::asio::ip::tcp::endpoint& remote, const std::string& cert, const std::string& key, const std::string& ca) 38 | { 39 | boost::asio::ssl::context ssl = boost::asio::ssl::context(boost::asio::ssl::context::sslv23); 40 | 41 | ssl.set_options(boost::asio::ssl::context::default_workarounds | boost::asio::ssl::context::sslv23_client); 42 | if (!cert.empty() && !key.empty()) 43 | { 44 | ssl.use_certificate_file(cert, boost::asio::ssl::context::pem); 45 | ssl.use_private_key_file(key, boost::asio::ssl::context::pem); 46 | } 47 | 48 | if (!ca.empty()) 49 | { 50 | ssl.set_verify_mode(boost::asio::ssl::verify_peer | boost::asio::ssl::verify_fail_if_no_peer_cert | boost::asio::ssl::verify_client_once ); 51 | ssl.load_verify_file(ca); 52 | } 53 | 54 | return std::make_shared(remote, io, std::move(ssl)); 55 | } 56 | 57 | }} 58 | -------------------------------------------------------------------------------- /tests/udp_tests.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Novemus Band. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | */ 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | BOOST_AUTO_TEST_CASE(sync_udp_exchange) 18 | { 19 | boost::asio::io_context io; 20 | boost::asio::spawn(io, [&](boost::asio::yield_context yield) 21 | { 22 | const boost::asio::ip::udp::endpoint lep(boost::asio::ip::make_address("127.0.0.1"), 1234); 23 | const boost::asio::ip::udp::endpoint rep(boost::asio::ip::make_address("127.0.0.1"), 4321); 24 | 25 | auto lend = plexus::network::create_udp_transport(io, lep); 26 | auto rend = plexus::network::create_udp_transport(io, rep); 27 | 28 | std::string wb = "Hello, Plexus!"; 29 | std::string rb; 30 | 31 | rb.resize(wb.size()); 32 | 33 | BOOST_REQUIRE_NO_THROW(lend->send_to(boost::asio::buffer(wb), rep, yield)); 34 | 35 | BOOST_REQUIRE_NO_THROW(BOOST_REQUIRE_EQUAL(rb.size(), rend->receive_from(boost::asio::buffer(rb), lep, yield))); 36 | BOOST_REQUIRE_EQUAL(wb, rb); 37 | 38 | BOOST_REQUIRE_NO_THROW(rend->send_to(boost::asio::buffer(wb), lep, yield)); 39 | 40 | BOOST_REQUIRE_NO_THROW(BOOST_REQUIRE_EQUAL(rb.size(), lend->receive_from(boost::asio::buffer(rb), rep, yield))); 41 | BOOST_REQUIRE_EQUAL(wb, rb); 42 | 43 | BOOST_REQUIRE_THROW(lend->receive_from(boost::asio::buffer(rb), rep, yield), boost::system::system_error); 44 | }, boost::asio::detached); 45 | 46 | io.run(); 47 | } 48 | 49 | BOOST_AUTO_TEST_CASE(async_udp_exchange) 50 | { 51 | boost::asio::io_context io; 52 | 53 | const boost::asio::ip::udp::endpoint lep(boost::asio::ip::make_address("127.0.0.1"), 1234); 54 | const boost::asio::ip::udp::endpoint rep(boost::asio::ip::make_address("127.0.0.1"), 4321); 55 | 56 | auto lend = plexus::network::create_udp_transport(io, lep); 57 | auto rend = plexus::network::create_udp_transport(io, rep); 58 | 59 | auto work = [](boost::asio::yield_context yield, std::shared_ptr udp, const boost::asio::ip::udp::endpoint& peer) 60 | { 61 | std::string wb = "Hello, Plexus!"; 62 | std::string rb; 63 | 64 | rb.resize(wb.size()); 65 | 66 | BOOST_REQUIRE_NO_THROW(udp->send_to(boost::asio::buffer(wb), peer, yield)); 67 | BOOST_REQUIRE_NO_THROW(udp->send_to(boost::asio::buffer(wb), peer, yield)); 68 | BOOST_REQUIRE_NO_THROW(udp->send_to(boost::asio::buffer(wb), peer, yield)); 69 | 70 | BOOST_REQUIRE_NO_THROW(BOOST_REQUIRE_EQUAL(rb.size(), udp->receive_from(boost::asio::buffer(rb), peer, yield))); 71 | BOOST_REQUIRE_EQUAL(wb, rb); 72 | 73 | BOOST_REQUIRE_NO_THROW(BOOST_REQUIRE_EQUAL(rb.size(), udp->receive_from(boost::asio::buffer(rb), peer, yield))); 74 | BOOST_REQUIRE_EQUAL(wb, rb); 75 | 76 | BOOST_REQUIRE_NO_THROW(BOOST_REQUIRE_EQUAL(rb.size(), udp->receive_from(boost::asio::buffer(rb), peer, yield))); 77 | BOOST_REQUIRE_EQUAL(wb, rb); 78 | }; 79 | 80 | boost::asio::spawn(io, [work, lend, rep](boost::asio::yield_context yield) 81 | { 82 | work(yield, lend, rep); 83 | }, boost::asio::detached); 84 | 85 | boost::asio::spawn(io, [work, rend, lep](boost::asio::yield_context yield) 86 | { 87 | work(yield, rend, lep); 88 | }, boost::asio::detached); 89 | 90 | io.run(); 91 | } 92 | -------------------------------------------------------------------------------- /src/plexus/utils.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Novemus Band. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | */ 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | namespace plexus { namespace utils { 27 | 28 | std::string to_hexadecimal(const void* data, size_t len); 29 | std::string to_base64(const void* data, size_t length); 30 | std::string to_base64_no_nl(const void* data, size_t length); 31 | std::string to_base64_url(const void* data, size_t length); 32 | std::string from_base64(const char* data, size_t length); 33 | std::string from_base64_url(const char* data, size_t length); 34 | std::string format(const char* fmt, ...); 35 | std::string format(const char* fmt, const boost::posix_time::ptime& time); 36 | std::string format(const char* fmt, const std::chrono::system_clock::time_point& time); 37 | std::string smime_sign(const std::string& msg, const std::string& cert, const std::string& key); 38 | std::string smime_verify(const std::string& msg, const std::string& cert, const std::string& ca); 39 | std::string smime_encrypt(const std::string& msg, const std::string& cert); 40 | std::string smime_decrypt(const std::string& msg, const std::string& cert, const std::string& key); 41 | std::string get_email_address(const std::string& email); 42 | 43 | template var_t random() 44 | { 45 | std::random_device dev; 46 | std::mt19937_64 gen(dev()); 47 | return static_cast(gen()); 48 | } 49 | 50 | template var_t getenv(const std::string& name, const var_t& def) 51 | { 52 | try 53 | { 54 | const char *env = std::getenv(name.c_str()); 55 | return env ? boost::lexical_cast(env) : def; 56 | } 57 | catch (const boost::bad_lexical_cast& ex) 58 | { 59 | _err_ << ex.what(); 60 | } 61 | 62 | return def; 63 | } 64 | 65 | template 66 | endpoint parse_endpoint(const std::string& url, const std::string& service) 67 | { 68 | if (url.empty() && service.empty()) 69 | return endpoint(); 70 | 71 | boost::asio::io_context io; 72 | typename endpoint::protocol_type::resolver resolver(io); 73 | 74 | std::smatch match; 75 | if (std::regex_search(url, match, std::regex("^(\\w+://)?\\[([a-zA-Z0-9:]+)\\]:(\\d+).*"))) 76 | return *resolver.resolve(match[2].str(), match[3].str()).begin(); 77 | 78 | if (std::regex_search(url, match, std::regex("^(\\w+)://\\[([a-zA-Z0-9:]+)\\].*"))) 79 | return *resolver.resolve(match[2].str(), match[1].str()).begin(); 80 | 81 | if (std::regex_search(url, match, std::regex("^\\[([a-zA-Z0-9:]+)\\].*"))) 82 | return *resolver.resolve(match[1].str(), service).begin(); 83 | 84 | if (std::regex_search(url, match, std::regex("^(\\w+://)?([\\w\\.]+):(\\d+).*"))) 85 | return *resolver.resolve(match[2].str(), match[3].str()).begin(); 86 | 87 | if (std::regex_search(url, match, std::regex("^(\\w+)://([\\w\\.]+).*"))) 88 | return *resolver.resolve(match[2].str(), match[1].str()).begin(); 89 | 90 | return *resolver.resolve(url, service).begin(); 91 | } 92 | 93 | } 94 | 95 | template 96 | std::ostream& operator<<(std::ostream& stream, const boost::asio::ip::basic_endpoint& endpoint) 97 | { 98 | if (stream.rdbuf()) 99 | return stream << endpoint.address().to_string() << ":" << endpoint.port(); 100 | return stream; 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /tests/certs/client.crt: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 5 | a2:cc:2f:d1:df:8b:32:79:fb:5f:cb:5d:d2:7a:32:ea 6 | Signature Algorithm: sha256WithRSAEncryption 7 | Issuer: CN=Plexus CA 8 | Validity 9 | Not Before: Apr 23 23:28:33 2022 GMT 10 | Not After : Apr 20 23:28:33 2032 GMT 11 | Subject: CN=client 12 | Subject Public Key Info: 13 | Public Key Algorithm: rsaEncryption 14 | Public-Key: (2048 bit) 15 | Modulus: 16 | 00:ae:6a:0b:08:f6:d9:68:17:11:b9:7e:54:eb:1f: 17 | 44:b8:94:7d:92:76:c0:e0:04:b4:24:46:6a:00:ea: 18 | 7d:2e:18:af:70:61:b5:a2:2e:60:ae:cd:0a:d9:ce: 19 | a4:2a:80:af:47:7b:5d:61:c2:d1:0a:58:3a:e6:ce: 20 | cb:37:47:08:0e:f8:bb:f9:bf:f9:f1:f3:61:25:e2: 21 | ab:00:28:ac:db:fa:56:44:89:9d:28:11:b5:0d:da: 22 | 9f:fd:b3:c3:86:72:f5:b3:2d:42:be:6b:89:b4:fd: 23 | 7e:05:10:8f:c5:4e:4b:f6:0a:f1:5e:8d:83:cf:29: 24 | fe:32:63:c7:69:ac:14:76:eb:10:f7:67:9a:8a:d0: 25 | 50:c1:45:b3:14:cc:09:e0:8c:f9:06:4d:0c:b3:99: 26 | 77:30:bd:00:fe:a6:16:a3:fb:52:2b:c2:c7:99:09: 27 | 59:85:d3:7e:7d:48:d3:68:47:ae:39:31:7d:94:63: 28 | 21:7d:75:ef:05:88:0a:e5:62:6b:4a:26:64:73:56: 29 | 00:a9:9d:7f:cf:e0:bd:70:b8:e7:b9:43:12:5c:6e: 30 | bd:4b:c2:d4:98:b7:e8:45:3b:ff:01:19:82:0f:42: 31 | 23:d3:3f:21:c4:c5:c2:c7:ed:ab:54:47:6e:56:78: 32 | 23:76:9a:03:98:8d:99:a1:6f:99:c1:74:c7:91:54: 33 | 36:23 34 | Exponent: 65537 (0x10001) 35 | X509v3 extensions: 36 | X509v3 Basic Constraints: 37 | CA:FALSE 38 | X509v3 Subject Key Identifier: 39 | A0:46:4F:D8:22:A9:80:7B:77:B8:1A:89:4F:72:9D:01:58:DA:2C:F0 40 | X509v3 Authority Key Identifier: 41 | keyid:10:8E:59:CF:F6:77:EE:50:A0:7D:9B:8E:1C:B7:BB:94:6C:8D:40:63 42 | DirName:/CN=Plexus CA 43 | serial:80:8A:67:28:9C:EB:D7:C7 44 | 45 | X509v3 Extended Key Usage: 46 | TLS Web Client Authentication 47 | X509v3 Key Usage: 48 | Digital Signature 49 | Signature Algorithm: sha256WithRSAEncryption 50 | 38:f7:08:2c:59:75:51:49:48:b7:23:33:cd:25:24:c4:79:2e: 51 | 0e:47:7e:a8:95:20:41:25:ef:35:09:5f:43:d9:ba:10:53:18: 52 | 99:35:26:9e:b1:8f:e1:b7:52:4b:6b:88:d6:a7:46:ea:28:02: 53 | 98:78:62:d1:52:86:99:ac:a5:ab:a6:01:09:53:a3:b4:a5:12: 54 | c2:19:60:47:a3:75:5e:39:71:67:a2:1d:5d:6e:b4:37:fd:53: 55 | 76:83:38:36:8b:1b:f8:b3:ce:4c:35:96:c2:0b:ef:d4:88:a9: 56 | 91:3a:8b:65:8a:e2:31:e5:4f:b1:56:0e:a8:c1:59:25:ab:b7: 57 | ad:ff:6e:3f:3b:50:d0:4d:35:30:8a:48:33:cc:9f:06:2f:ab: 58 | 5a:5f:7d:13:d0:24:7a:11:6c:65:83:99:c0:8e:fd:ea:9a:f5: 59 | 93:69:3f:37:5f:4b:ea:6c:13:1a:07:5a:3f:ba:28:ba:be:cc: 60 | a6:25:d4:7a:c4:cc:72:df:98:d5:0c:cc:fd:24:07:1b:e9:7e: 61 | 15:93:93:83:62:a6:90:45:c5:ba:25:b0:86:6b:85:43:bd:b6: 62 | af:0f:19:52:e4:41:1f:06:c8:ea:92:c6:cb:b0:56:72:c5:29: 63 | d4:3e:58:4f:cc:f4:74:c3:da:12:6f:93:93:d8:35:e0:59:df: 64 | 29:2d:85:63 65 | -----BEGIN CERTIFICATE----- 66 | MIIDRjCCAi6gAwIBAgIRAKLML9HfizJ5+1/LXdJ6MuowDQYJKoZIhvcNAQELBQAw 67 | FDESMBAGA1UEAwwJUGxleHVzIENBMB4XDTIyMDQyMzIzMjgzM1oXDTMyMDQyMDIz 68 | MjgzM1owETEPMA0GA1UEAwwGY2xpZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A 69 | MIIBCgKCAQEArmoLCPbZaBcRuX5U6x9EuJR9knbA4AS0JEZqAOp9LhivcGG1oi5g 70 | rs0K2c6kKoCvR3tdYcLRClg65s7LN0cIDvi7+b/58fNhJeKrACis2/pWRImdKBG1 71 | Ddqf/bPDhnL1sy1CvmuJtP1+BRCPxU5L9grxXo2Dzyn+MmPHaawUdusQ92eaitBQ 72 | wUWzFMwJ4Iz5Bk0Ms5l3ML0A/qYWo/tSK8LHmQlZhdN+fUjTaEeuOTF9lGMhfXXv 73 | BYgK5WJrSiZkc1YAqZ1/z+C9cLjnuUMSXG69S8LUmLfoRTv/ARmCD0Ij0z8hxMXC 74 | x+2rVEduVngjdpoDmI2ZoW+ZwXTHkVQ2IwIDAQABo4GVMIGSMAkGA1UdEwQCMAAw 75 | HQYDVR0OBBYEFKBGT9giqYB7d7gaiU9ynQFY2izwMEQGA1UdIwQ9MDuAFBCOWc/2 76 | d+5QoH2bjhy3u5RsjUBjoRikFjAUMRIwEAYDVQQDDAlQbGV4dXMgQ0GCCQCAimco 77 | nOvXxzATBgNVHSUEDDAKBggrBgEFBQcDAjALBgNVHQ8EBAMCB4AwDQYJKoZIhvcN 78 | AQELBQADggEBADj3CCxZdVFJSLcjM80lJMR5Lg5HfqiVIEEl7zUJX0PZuhBTGJk1 79 | Jp6xj+G3UktriNanRuooAph4YtFShpmspaumAQlTo7SlEsIZYEejdV45cWeiHV1u 80 | tDf9U3aDODaLG/izzkw1lsIL79SIqZE6i2WK4jHlT7FWDqjBWSWrt63/bj87UNBN 81 | NTCKSDPMnwYvq1pffRPQJHoRbGWDmcCO/eqa9ZNpPzdfS+psExoHWj+6KLq+zKYl 82 | 1HrEzHLfmNUMzP0kBxvpfhWTk4NippBFxbolsIZrhUO9tq8PGVLkQR8GyOqSxsuw 83 | VnLFKdQ+WE/M9HTD2hJvk5PYNeBZ3ykthWM= 84 | -----END CERTIFICATE----- 85 | -------------------------------------------------------------------------------- /tests/certs/alien/client.crt: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 5 | 5d:56:a0:b4:d2:2b:6b:04:91:d9:29:22:6f:e4:83:01 6 | Signature Algorithm: sha256WithRSAEncryption 7 | Issuer: CN=Plexus-RSA CA 8 | Validity 9 | Not Before: Apr 26 14:18:13 2022 GMT 10 | Not After : Apr 23 14:18:13 2032 GMT 11 | Subject: CN=client 12 | Subject Public Key Info: 13 | Public Key Algorithm: rsaEncryption 14 | RSA Public-Key: (2048 bit) 15 | Modulus: 16 | 00:b1:10:d5:2f:6e:06:4b:c0:dd:84:5e:6f:ed:da: 17 | 4f:af:f0:08:22:c6:f0:4b:9c:ea:e2:6e:9a:b4:c0: 18 | e6:d0:a2:9f:76:04:a0:d1:4a:da:c2:af:9e:45:76: 19 | 84:74:bd:93:84:28:47:23:99:bd:c2:7b:58:ff:7d: 20 | 14:71:da:72:82:d9:2c:f7:c4:c3:e2:64:48:69:41: 21 | 32:61:b3:88:90:0f:38:5f:cc:e0:52:44:6e:f0:55: 22 | 16:49:06:6f:1e:59:0f:e7:8b:d3:53:41:55:a5:48: 23 | 85:ec:dc:1c:04:4f:f0:d3:36:01:70:fc:57:03:3c: 24 | 3b:e7:d5:42:2d:31:ca:2a:90:bc:60:0b:b3:a7:69: 25 | 8b:ce:a3:52:1a:4d:a8:75:e8:3a:ea:39:ad:9b:e6: 26 | c9:0a:d5:92:38:6d:c3:4e:08:e0:ff:5e:63:b6:f0: 27 | f0:ef:0d:64:2a:cd:0d:97:bb:db:02:03:25:35:cc: 28 | aa:32:1b:d6:8c:6c:32:27:25:0e:cc:7a:e8:4c:6c: 29 | 70:29:f7:bc:61:85:37:24:7f:87:b8:d1:c7:c7:fb: 30 | 76:a0:a3:53:11:82:94:6e:2c:56:1f:bb:07:b3:2e: 31 | 60:e7:05:53:c0:bf:15:f7:35:2a:bd:ba:a1:1d:ea: 32 | 40:f3:4b:2a:1f:e6:c0:f0:95:9e:9b:72:6f:1b:e8: 33 | f3:35 34 | Exponent: 65537 (0x10001) 35 | X509v3 extensions: 36 | X509v3 Basic Constraints: 37 | CA:FALSE 38 | X509v3 Subject Key Identifier: 39 | FE:F7:07:85:52:C9:F6:C7:06:75:CD:34:BE:C8:41:5C:1A:F8:82:D1 40 | X509v3 Authority Key Identifier: 41 | keyid:A2:88:F2:04:F7:6E:26:94:F1:8B:F7:B0:EC:A7:37:8A:98:F3:7E:21 42 | DirName:/CN=Plexus-RSA CA 43 | serial:69:6C:B5:FA:14:55:56:DC:AC:67:D8:8D:4D:69:15:13:70:EC:CF:1F 44 | 45 | X509v3 Extended Key Usage: 46 | TLS Web Client Authentication 47 | X509v3 Key Usage: 48 | Digital Signature 49 | Signature Algorithm: sha256WithRSAEncryption 50 | 0b:7f:4e:9e:63:4c:9e:ee:d7:30:39:54:16:8c:d7:01:13:11: 51 | 17:18:b3:b6:ea:4a:3d:1c:39:2e:a9:0e:81:2b:fd:fc:09:8d: 52 | 8e:9f:67:9e:dc:b6:de:a5:5a:2e:dd:62:61:3d:a1:a6:2a:74: 53 | fb:24:b2:0f:28:41:48:78:7b:22:14:43:93:f9:36:bc:eb:bc: 54 | 36:4a:fd:2c:8f:7d:1f:31:50:11:2c:c2:ce:7d:21:e4:2a:5a: 55 | 55:e4:e2:06:77:d5:22:0b:dc:50:b9:a2:31:2b:4a:c3:d9:f1: 56 | 06:88:90:54:42:7c:56:4f:9b:48:6d:a3:63:f8:97:ee:58:f9: 57 | b4:c6:b8:63:49:dd:df:e3:3e:04:28:29:e2:de:7a:1f:43:55: 58 | 25:89:d5:59:b0:06:c2:e3:b8:ed:c2:c9:45:3f:d1:8f:1a:ae: 59 | f7:86:75:12:1c:77:f4:ed:3e:3c:d8:e1:b3:c4:c1:9f:0a:10: 60 | 84:58:f6:a2:4e:35:0d:eb:97:54:b0:4f:d8:ab:2e:63:30:8a: 61 | 2f:22:9b:45:3e:bf:6a:73:62:19:0a:23:0f:21:7e:c9:72:7d: 62 | 9d:f0:60:f0:99:ac:7f:93:22:63:29:d3:01:3f:fa:52:d0:02: 63 | 2d:d9:3d:e2:42:86:11:f8:cc:fb:25:b7:3f:fd:96:b6:4a:01: 64 | f3:df:47:87 65 | -----BEGIN CERTIFICATE----- 66 | MIIDWDCCAkCgAwIBAgIQXVagtNIrawSR2Skib+SDATANBgkqhkiG9w0BAQsFADAY 67 | MRYwFAYDVQQDDA1QbGV4dXMtUlNBIENBMB4XDTIyMDQyNjE0MTgxM1oXDTMyMDQy 68 | MzE0MTgxM1owETEPMA0GA1UEAwwGY2xpZW50MIIBIjANBgkqhkiG9w0BAQEFAAOC 69 | AQ8AMIIBCgKCAQEAsRDVL24GS8DdhF5v7dpPr/AIIsbwS5zq4m6atMDm0KKfdgSg 70 | 0Urawq+eRXaEdL2ThChHI5m9wntY/30Ucdpygtks98TD4mRIaUEyYbOIkA84X8zg 71 | UkRu8FUWSQZvHlkP54vTU0FVpUiF7NwcBE/w0zYBcPxXAzw759VCLTHKKpC8YAuz 72 | p2mLzqNSGk2odeg66jmtm+bJCtWSOG3DTgjg/15jtvDw7w1kKs0Nl7vbAgMlNcyq 73 | MhvWjGwyJyUOzHroTGxwKfe8YYU3JH+HuNHHx/t2oKNTEYKUbixWH7sHsy5g5wVT 74 | wL8V9zUqvbqhHepA80sqH+bA8JWem3JvG+jzNQIDAQABo4GkMIGhMAkGA1UdEwQC 75 | MAAwHQYDVR0OBBYEFP73B4VSyfbHBnXNNL7IQVwa+ILRMFMGA1UdIwRMMEqAFKKI 76 | 8gT3biaU8Yv3sOynN4qY834hoRykGjAYMRYwFAYDVQQDDA1QbGV4dXMtUlNBIENB 77 | ghRpbLX6FFVW3Kxn2I1NaRUTcOzPHzATBgNVHSUEDDAKBggrBgEFBQcDAjALBgNV 78 | HQ8EBAMCB4AwDQYJKoZIhvcNAQELBQADggEBAAt/Tp5jTJ7u1zA5VBaM1wETERcY 79 | s7bqSj0cOS6pDoEr/fwJjY6fZ57ctt6lWi7dYmE9oaYqdPsksg8oQUh4eyIUQ5P5 80 | NrzrvDZK/SyPfR8xUBEsws59IeQqWlXk4gZ31SIL3FC5ojErSsPZ8QaIkFRCfFZP 81 | m0hto2P4l+5Y+bTGuGNJ3d/jPgQoKeLeeh9DVSWJ1VmwBsLjuO3CyUU/0Y8arveG 82 | dRIcd/TtPjzY4bPEwZ8KEIRY9qJONQ3rl1SwT9irLmMwii8im0U+v2pzYhkKIw8h 83 | fslyfZ3wYPCZrH+TImMp0wE/+lLQAi3ZPeJChhH4zPsltz/9lrZKAfPfR4c= 84 | -----END CERTIFICATE----- 85 | -------------------------------------------------------------------------------- /tests/certs/server.crt: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 5 | 58:52:ee:1d:bc:5e:6a:e0:92:73:a3:f9:49:16:f7:10 6 | Signature Algorithm: sha256WithRSAEncryption 7 | Issuer: CN=Plexus CA 8 | Validity 9 | Not Before: Apr 23 23:26:58 2022 GMT 10 | Not After : Apr 20 23:26:58 2032 GMT 11 | Subject: CN=server 12 | Subject Public Key Info: 13 | Public Key Algorithm: rsaEncryption 14 | Public-Key: (2048 bit) 15 | Modulus: 16 | 00:c1:9e:a0:fc:0f:64:f0:0a:c9:be:5a:47:89:72: 17 | 69:32:7c:fa:b1:e5:80:f0:5c:78:cb:a5:74:30:88: 18 | 00:8b:f1:b7:74:28:ae:82:6a:a4:c0:28:1b:d7:d3: 19 | 2c:84:7a:10:1f:3e:58:09:8b:f0:d7:1d:44:3a:b7: 20 | 96:2b:52:d8:d1:b5:9b:02:79:80:6a:35:5d:00:de: 21 | 7e:51:73:69:b2:9b:82:18:fe:de:3a:39:f5:54:72: 22 | 36:db:7c:ec:f6:79:83:fd:a8:2d:17:6d:85:b5:ba: 23 | 3f:69:0d:fe:6a:3a:fe:f1:f3:e5:ce:ce:d7:64:38: 24 | 72:20:5e:eb:e2:d8:5d:78:ac:f5:65:07:12:5e:2b: 25 | 30:8d:e4:9a:fd:c4:2c:c5:38:b4:4c:04:ec:42:bf: 26 | 1d:67:24:44:62:7a:2d:88:20:87:1c:d0:0e:18:3c: 27 | c2:8a:49:74:65:17:4e:6f:de:e9:ef:b9:fb:83:ab: 28 | 16:85:fb:ed:76:a7:43:92:7a:90:63:7a:e6:9d:e7: 29 | 16:29:8a:52:bb:1a:0f:bf:12:bf:84:d0:d2:f4:7c: 30 | f5:5c:19:93:7c:90:91:4c:4a:5b:00:29:32:e3:e0: 31 | fe:68:10:cc:0c:68:1e:19:71:59:62:86:ad:1d:e3: 32 | 25:34:5e:09:e7:66:ce:eb:99:0e:ca:cd:1a:70:38: 33 | 6f:5f 34 | Exponent: 65537 (0x10001) 35 | X509v3 extensions: 36 | X509v3 Basic Constraints: 37 | CA:FALSE 38 | X509v3 Subject Key Identifier: 39 | 6D:85:4B:63:DD:1C:A6:A9:D3:1C:A4:64:52:3F:AF:5B:46:B7:6B:1A 40 | X509v3 Authority Key Identifier: 41 | keyid:10:8E:59:CF:F6:77:EE:50:A0:7D:9B:8E:1C:B7:BB:94:6C:8D:40:63 42 | DirName:/CN=Plexus CA 43 | serial:80:8A:67:28:9C:EB:D7:C7 44 | 45 | X509v3 Extended Key Usage: 46 | TLS Web Server Authentication 47 | X509v3 Key Usage: 48 | Digital Signature, Key Encipherment 49 | X509v3 Subject Alternative Name: 50 | DNS:server 51 | Signature Algorithm: sha256WithRSAEncryption 52 | 6e:77:24:e9:71:a0:17:71:75:0b:52:ea:fa:80:96:c7:b2:9c: 53 | 62:fb:70:13:d9:ce:9a:17:30:4e:d4:0d:ae:e4:8e:96:b2:2d: 54 | 2a:b1:da:6a:e2:53:89:30:92:dd:cd:ad:5f:9b:b6:a9:55:df: 55 | d9:24:52:35:9d:80:6e:98:ef:4b:27:9f:14:14:b6:9a:0e:4d: 56 | 74:4b:bd:b2:b8:7e:71:94:0e:cd:d6:b9:d5:4d:b7:bc:56:aa: 57 | b6:c8:54:78:e8:2f:20:31:45:dd:d5:56:af:dd:42:4a:4f:ca: 58 | ec:40:a4:c3:b7:f3:23:b2:23:9c:b8:9f:b8:f4:a2:b9:7b:39: 59 | 54:9a:51:a3:42:1a:3d:17:a5:fb:9e:c1:95:de:8e:28:7c:40: 60 | 1b:7f:41:1a:e9:ea:b6:f8:42:47:2e:4b:05:05:57:66:49:b6: 61 | fb:c9:09:e6:5f:12:01:03:3d:21:81:83:c1:da:ef:c9:c6:e4: 62 | 4a:c2:3c:fc:c9:87:4d:18:5b:50:35:7d:ba:36:c6:85:5a:a8: 63 | e7:8f:72:3c:6e:13:95:55:f3:28:f9:1f:55:b7:96:fe:94:66: 64 | 59:7c:a6:4b:b3:4e:06:2f:09:a8:0c:f6:11:02:62:2b:9d:6e: 65 | a8:94:b9:e4:c7:fb:47:d7:d1:4e:06:b0:87:bc:05:f5:fb:9a: 66 | 3c:ac:93:02 67 | -----BEGIN CERTIFICATE----- 68 | MIIDWDCCAkCgAwIBAgIQWFLuHbxeauCSc6P5SRb3EDANBgkqhkiG9w0BAQsFADAU 69 | MRIwEAYDVQQDDAlQbGV4dXMgQ0EwHhcNMjIwNDIzMjMyNjU4WhcNMzIwNDIwMjMy 70 | NjU4WjARMQ8wDQYDVQQDDAZzZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw 71 | ggEKAoIBAQDBnqD8D2TwCsm+WkeJcmkyfPqx5YDwXHjLpXQwiACL8bd0KK6CaqTA 72 | KBvX0yyEehAfPlgJi/DXHUQ6t5YrUtjRtZsCeYBqNV0A3n5Rc2mym4IY/t46OfVU 73 | cjbbfOz2eYP9qC0XbYW1uj9pDf5qOv7x8+XOztdkOHIgXuvi2F14rPVlBxJeKzCN 74 | 5Jr9xCzFOLRMBOxCvx1nJERiei2IIIcc0A4YPMKKSXRlF05v3unvufuDqxaF++12 75 | p0OSepBjeuad5xYpilK7Gg+/Er+E0NL0fPVcGZN8kJFMSlsAKTLj4P5oEMwMaB4Z 76 | cVlihq0d4yU0XgnnZs7rmQ7KzRpwOG9fAgMBAAGjgagwgaUwCQYDVR0TBAIwADAd 77 | BgNVHQ4EFgQUbYVLY90cpqnTHKRkUj+vW0a3axowRAYDVR0jBD0wO4AUEI5Zz/Z3 78 | 7lCgfZuOHLe7lGyNQGOhGKQWMBQxEjAQBgNVBAMMCVBsZXh1cyBDQYIJAICKZyic 79 | 69fHMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAsGA1UdDwQEAwIFoDARBgNVHREECjAI 80 | ggZzZXJ2ZXIwDQYJKoZIhvcNAQELBQADggEBAG53JOlxoBdxdQtS6vqAlseynGL7 81 | cBPZzpoXME7UDa7kjpayLSqx2mriU4kwkt3NrV+btqlV39kkUjWdgG6Y70snnxQU 82 | tpoOTXRLvbK4fnGUDs3WudVNt7xWqrbIVHjoLyAxRd3VVq/dQkpPyuxApMO38yOy 83 | I5y4n7j0orl7OVSaUaNCGj0XpfuewZXejih8QBt/QRrp6rb4QkcuSwUFV2ZJtvvJ 84 | CeZfEgEDPSGBg8Ha78nG5ErCPPzJh00YW1A1fbo2xoVaqOePcjxuE5VV8yj5H1W3 85 | lv6UZll8pkuzTgYvCagM9hECYiudbqiUueTH+0fX0U4GsIe8BfX7mjyskwI= 86 | -----END CERTIFICATE----- 87 | -------------------------------------------------------------------------------- /tests/certs/alien/server.crt: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 5 | 66:f1:c2:60:2d:87:38:29:cc:68:42:0f:b4:54:da:91 6 | Signature Algorithm: sha256WithRSAEncryption 7 | Issuer: CN=Plexus-RSA CA 8 | Validity 9 | Not Before: Apr 26 14:18:26 2022 GMT 10 | Not After : Apr 23 14:18:26 2032 GMT 11 | Subject: CN=server 12 | Subject Public Key Info: 13 | Public Key Algorithm: rsaEncryption 14 | RSA Public-Key: (2048 bit) 15 | Modulus: 16 | 00:df:0a:9d:cf:81:28:09:77:6f:33:85:4a:78:30: 17 | b0:1e:01:7b:10:b5:c7:0c:48:23:8d:cc:ef:c2:6e: 18 | 1d:19:57:a6:cc:37:cc:bf:36:86:c1:bb:70:12:81: 19 | b1:1e:01:04:37:a8:9d:0a:00:06:f2:20:fa:7c:51: 20 | 9c:c6:5e:7d:fe:ee:35:b7:db:91:e8:2e:c5:5d:9e: 21 | e8:cb:a6:39:0f:53:8f:73:6d:74:33:5b:f6:fd:e4: 22 | b6:d0:2d:89:8c:e4:a2:c1:65:24:3a:15:10:40:f5: 23 | 15:b4:59:b1:b9:f5:4f:e7:95:3a:fa:30:f0:77:f0: 24 | eb:16:50:c8:b7:fa:b4:2f:58:46:1d:f1:d8:8e:f3: 25 | e2:8a:b9:3d:f0:a2:36:43:a9:d2:54:e4:d5:40:02: 26 | e9:ad:4e:84:ab:45:b3:d0:61:4c:41:f4:f3:fe:b8: 27 | 4a:41:dc:fe:34:00:dd:37:71:b5:78:83:08:49:a2: 28 | 1c:34:22:ff:b0:7e:45:37:96:d4:f8:0f:1d:e2:ea: 29 | 5f:d5:54:a1:9b:4b:63:21:cc:e0:67:23:df:72:5f: 30 | 10:ed:ac:dd:3e:db:ae:3d:c5:47:7a:b9:b5:63:3e: 31 | c9:39:fe:6c:bd:51:3b:98:cf:fe:20:df:5a:aa:65: 32 | 4b:f3:1b:b2:14:dd:c6:9c:12:7d:2d:f9:ae:44:f3: 33 | ee:f5 34 | Exponent: 65537 (0x10001) 35 | X509v3 extensions: 36 | X509v3 Basic Constraints: 37 | CA:FALSE 38 | X509v3 Subject Key Identifier: 39 | 84:69:D2:A5:CF:B7:79:CE:E4:86:13:42:7B:06:B6:51:AD:B5:77:CF 40 | X509v3 Authority Key Identifier: 41 | keyid:A2:88:F2:04:F7:6E:26:94:F1:8B:F7:B0:EC:A7:37:8A:98:F3:7E:21 42 | DirName:/CN=Plexus-RSA CA 43 | serial:69:6C:B5:FA:14:55:56:DC:AC:67:D8:8D:4D:69:15:13:70:EC:CF:1F 44 | 45 | X509v3 Extended Key Usage: 46 | TLS Web Server Authentication 47 | X509v3 Key Usage: 48 | Digital Signature, Key Encipherment 49 | X509v3 Subject Alternative Name: 50 | DNS:server 51 | Signature Algorithm: sha256WithRSAEncryption 52 | 52:61:60:c0:78:20:b3:6f:e6:c0:5e:77:1a:a3:4a:f6:0e:70: 53 | c9:de:19:a1:0f:df:c9:c6:e2:32:eb:ef:c2:27:ad:45:39:5d: 54 | 79:85:b9:d5:30:e0:5f:c8:9a:67:a9:53:8f:39:3e:e7:c7:55: 55 | 6f:d3:96:4d:9f:e2:42:5b:dd:95:e3:ab:ee:bd:06:50:a5:c9: 56 | 92:44:da:8a:3a:70:34:ff:f8:bd:51:b5:cf:a3:64:0e:30:76: 57 | c6:9b:b0:71:61:d3:05:33:27:81:7f:bc:97:07:87:86:48:0a: 58 | 0c:b7:ea:88:30:5c:8f:ae:3f:b4:dd:51:4e:03:a1:a3:1d:a2: 59 | 36:a8:55:1c:43:55:28:96:dc:4f:2a:09:e5:79:1c:a5:40:ea: 60 | 08:0a:ab:01:1b:ee:6e:71:c0:32:80:0e:11:79:dc:4c:d9:4d: 61 | a4:6c:49:cd:4b:4c:60:35:de:b0:4c:db:99:a3:b6:5c:12:23: 62 | 1c:5b:3f:1f:f2:03:f5:14:c4:ef:08:74:56:57:d3:ad:09:e3: 63 | c9:af:72:fa:b6:48:ee:c1:c0:f9:64:50:f5:d6:b0:61:3b:28: 64 | 53:96:23:35:f7:48:d8:16:9b:2e:65:46:a8:9a:22:61:61:1f: 65 | 98:b7:ab:88:3e:54:4d:fa:57:77:82:a8:c2:3a:7a:ac:9e:71: 66 | a3:13:e7:0c 67 | -----BEGIN CERTIFICATE----- 68 | MIIDazCCAlOgAwIBAgIQZvHCYC2HOCnMaEIPtFTakTANBgkqhkiG9w0BAQsFADAY 69 | MRYwFAYDVQQDDA1QbGV4dXMtUlNBIENBMB4XDTIyMDQyNjE0MTgyNloXDTMyMDQy 70 | MzE0MTgyNlowETEPMA0GA1UEAwwGc2VydmVyMIIBIjANBgkqhkiG9w0BAQEFAAOC 71 | AQ8AMIIBCgKCAQEA3wqdz4EoCXdvM4VKeDCwHgF7ELXHDEgjjczvwm4dGVemzDfM 72 | vzaGwbtwEoGxHgEEN6idCgAG8iD6fFGcxl59/u41t9uR6C7FXZ7oy6Y5D1OPc210 73 | M1v2/eS20C2JjOSiwWUkOhUQQPUVtFmxufVP55U6+jDwd/DrFlDIt/q0L1hGHfHY 74 | jvPiirk98KI2Q6nSVOTVQALprU6Eq0Wz0GFMQfTz/rhKQdz+NADdN3G1eIMISaIc 75 | NCL/sH5FN5bU+A8d4upf1VShm0tjIczgZyPfcl8Q7azdPtuuPcVHerm1Yz7JOf5s 76 | vVE7mM/+IN9aqmVL8xuyFN3GnBJ9LfmuRPPu9QIDAQABo4G3MIG0MAkGA1UdEwQC 77 | MAAwHQYDVR0OBBYEFIRp0qXPt3nO5IYTQnsGtlGttXfPMFMGA1UdIwRMMEqAFKKI 78 | 8gT3biaU8Yv3sOynN4qY834hoRykGjAYMRYwFAYDVQQDDA1QbGV4dXMtUlNBIENB 79 | ghRpbLX6FFVW3Kxn2I1NaRUTcOzPHzATBgNVHSUEDDAKBggrBgEFBQcDATALBgNV 80 | HQ8EBAMCBaAwEQYDVR0RBAowCIIGc2VydmVyMA0GCSqGSIb3DQEBCwUAA4IBAQBS 81 | YWDAeCCzb+bAXncao0r2DnDJ3hmhD9/JxuIy6+/CJ61FOV15hbnVMOBfyJpnqVOP 82 | OT7nx1Vv05ZNn+JCW92V46vuvQZQpcmSRNqKOnA0//i9UbXPo2QOMHbGm7BxYdMF 83 | MyeBf7yXB4eGSAoMt+qIMFyPrj+03VFOA6GjHaI2qFUcQ1UoltxPKgnleRylQOoI 84 | CqsBG+5uccAygA4RedxM2U2kbEnNS0xgNd6wTNuZo7ZcEiMcWz8f8gP1FMTvCHRW 85 | V9OtCePJr3L6tkjuwcD5ZFD11rBhOyhTliM190jYFpsuZUaomiJhYR+Yt6uIPlRN 86 | +ld3gqjCOnqsnnGjE+cM 87 | -----END CERTIFICATE----- 88 | -------------------------------------------------------------------------------- /src/plexus/plexus.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Novemus Band. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | */ 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | namespace plexus { 20 | 21 | using namespace boost::asio::ip; 22 | 23 | struct traverse 24 | { 25 | enum binding 26 | { 27 | independent = 0, 28 | port_dependent = 1, 29 | address_dependent = 2, 30 | address_and_port_dependent = 3 31 | }; 32 | 33 | struct 34 | { 35 | bool nat : 1; 36 | bool hairpin : 1; 37 | bool random_port : 1; 38 | bool variable_address : 1; 39 | binding mapping : 2; 40 | binding filtering : 2; 41 | } 42 | traits; 43 | 44 | udp::endpoint inner_endpoint; 45 | udp::endpoint outer_endpoint; 46 | }; 47 | 48 | struct identity 49 | { 50 | std::string owner; 51 | std::string pin; 52 | }; 53 | 54 | struct cryptoid 55 | { 56 | std::string certfile; 57 | std::string keyfile; 58 | }; 59 | 60 | struct reference 61 | { 62 | udp::endpoint endpoint; 63 | uint64_t puzzle = 0; 64 | }; 65 | 66 | struct emailer 67 | { 68 | tcp::endpoint smtp; 69 | tcp::endpoint imap; 70 | std::string login; 71 | std::string password; 72 | std::string cert; 73 | std::string key; 74 | std::string ca; 75 | }; 76 | 77 | struct dhtnode 78 | { 79 | std::string bootstrap; // bootstrap URL 80 | uint16_t port = 4222; // node port 81 | uint32_t network = 0; // network id 82 | }; 83 | 84 | using rendezvous = std::variant; 85 | 86 | struct options 87 | { 88 | std::string app; // application id 89 | std::string repo; // path to application repository 90 | udp::endpoint stun; // endpoint of public stun server 91 | udp::endpoint bind; // local endpoint to bind the application 92 | uint16_t hops; // ttl of the udp-hole punching packet 93 | rendezvous mediator; // signaling service to trigger peer connections 94 | }; 95 | 96 | using connector = std::function; 101 | 102 | using collector = std::function; 105 | 106 | using observer = std::function; 108 | 109 | using fallback = std::function; 112 | 113 | LIBPLEXUS_EXPORT 114 | void explore_network(boost::asio::io_context& io, const udp::endpoint& bind, const udp::endpoint& stun, const std::function& handler, const std::function& failure) noexcept(true); 115 | LIBPLEXUS_EXPORT 116 | void forward_advent(boost::asio::io_context& io, const rendezvous& mediator, const std::string& app, const std::string& repo, const identity& host, const identity& peer, const observer& handler, const fallback& failure) noexcept(true); 117 | LIBPLEXUS_EXPORT 118 | void receive_advent(boost::asio::io_context& io, const rendezvous& mediator, const std::string& app, const std::string& repo, const identity& host, const identity& peer, const observer& handler, const fallback& failure) noexcept(true); 119 | LIBPLEXUS_EXPORT 120 | void spawn_accept(boost::asio::io_context& io, const options& config, const identity& host, const identity& peer, const connector& connect, const fallback& failure = nullptr) noexcept(true); 121 | LIBPLEXUS_EXPORT 122 | void spawn_invite(boost::asio::io_context& io, const options& config, const identity& host, const identity& peer, const connector& connect, const fallback& failure = nullptr) noexcept(true); 123 | LIBPLEXUS_EXPORT 124 | void spawn_accept(boost::asio::io_context& io, const options& config, const identity& host, const identity& peer, const collector& collect, const fallback& failure = nullptr) noexcept(true); 125 | LIBPLEXUS_EXPORT 126 | void spawn_invite(boost::asio::io_context& io, const options& config, const identity& host, const identity& peer, const collector& collect, const fallback& failure = nullptr) noexcept(true); 127 | 128 | } 129 | -------------------------------------------------------------------------------- /tests/tcp_tests.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Novemus Band. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | */ 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | class tcp_echo_session : public std::enable_shared_from_this 20 | { 21 | enum { max_length = 1024 }; 22 | 23 | char m_data[max_length]; 24 | boost::asio::ip::tcp::socket m_socket; 25 | 26 | public: 27 | 28 | tcp_echo_session(boost::asio::io_context &io) 29 | : m_socket(io) 30 | { 31 | } 32 | 33 | boost::asio::ip::tcp::socket& socket() 34 | { 35 | return m_socket; 36 | } 37 | 38 | void start() 39 | { 40 | m_socket.async_read_some( 41 | boost::asio::buffer(m_data, max_length), 42 | boost::bind(&tcp_echo_session::handle_read, shared_from_this(), boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred) 43 | ); 44 | } 45 | 46 | protected: 47 | 48 | void handle_read(const boost::system::error_code &error, size_t transferred) 49 | { 50 | if (!error) 51 | { 52 | m_socket.async_write_some( 53 | boost::asio::buffer(m_data, transferred), 54 | boost::bind(&tcp_echo_session::handle_write, shared_from_this(), boost::asio::placeholders::error) 55 | ); 56 | } 57 | } 58 | 59 | void handle_write(const boost::system::error_code &error) 60 | { 61 | if (!error) 62 | { 63 | m_socket.async_read_some( 64 | boost::asio::buffer(m_data, max_length), 65 | boost::bind(&tcp_echo_session::handle_read, shared_from_this(), boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred) 66 | ); 67 | } 68 | } 69 | }; 70 | 71 | namespace { 72 | 73 | class tcp_echo_server 74 | { 75 | boost::asio::io_context& m_io; 76 | boost::asio::ip::tcp::acceptor m_acceptor; 77 | 78 | public: 79 | 80 | tcp_echo_server(boost::asio::io_context& io, unsigned short port) 81 | : m_io(io) 82 | , m_acceptor(m_io, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port)) 83 | { 84 | boost::asio::post(m_io, boost::bind(&tcp_echo_server::start_accept, this)); 85 | } 86 | 87 | protected: 88 | 89 | void start_accept() 90 | { 91 | auto session = std::make_shared(m_io); 92 | m_acceptor.async_accept( 93 | session->socket(), 94 | boost::bind(&tcp_echo_server::handle_accept, this, session, boost::asio::placeholders::error) 95 | ); 96 | } 97 | 98 | void handle_accept(std::shared_ptr session, const boost::system::error_code &error) 99 | { 100 | if (!error) 101 | session->start(); 102 | 103 | start_accept(); 104 | } 105 | }; 106 | 107 | std::shared_ptr create_tcp_server(boost::asio::io_context& io, unsigned short port) 108 | { 109 | return std::make_shared(io, port); 110 | } 111 | 112 | const uint16_t TCP_SERVER_PORT = 8765; 113 | 114 | const boost::asio::ip::tcp::endpoint TCP_SERVER(boost::asio::ip::make_address("127.0.0.1"), TCP_SERVER_PORT); 115 | const boost::asio::ip::tcp::endpoint TCP_CLIENT(boost::asio::ip::tcp::v4(), 0); 116 | const boost::asio::ip::tcp::endpoint TCP_FAKE_SERVER(boost::asio::ip::make_address("1.1.1.1"), 1); 117 | 118 | } 119 | 120 | BOOST_AUTO_TEST_CASE(tcp_echo_exchange) 121 | { 122 | boost::asio::io_context io; 123 | auto server = create_tcp_server(io, TCP_SERVER_PORT); 124 | 125 | boost::asio::spawn(io, [&](boost::asio::yield_context yield) 126 | { 127 | auto shorty = plexus::network::create_tcp_client(io, TCP_FAKE_SERVER, TCP_CLIENT); 128 | BOOST_REQUIRE_THROW(shorty->connect(yield, 2000), boost::system::system_error); 129 | BOOST_REQUIRE_NO_THROW(shorty->shutdown()); 130 | 131 | auto client = plexus::network::create_tcp_client(io, TCP_SERVER, TCP_CLIENT); 132 | BOOST_REQUIRE_NO_THROW(client->connect(yield)); 133 | 134 | std::string wb = "Hello, Plexus!"; 135 | std::string rb; 136 | 137 | rb.resize(wb.size()); 138 | 139 | BOOST_REQUIRE_NO_THROW(BOOST_REQUIRE_EQUAL(client->write(boost::asio::buffer(wb), yield), wb.size())); 140 | BOOST_REQUIRE_NO_THROW(BOOST_REQUIRE_EQUAL(client->read(boost::asio::buffer(rb), yield), rb.size())); 141 | BOOST_REQUIRE_EQUAL(wb, rb); 142 | 143 | BOOST_REQUIRE_NO_THROW(BOOST_REQUIRE_EQUAL(client->write(boost::asio::buffer(wb), yield), wb.size())); 144 | BOOST_REQUIRE_NO_THROW(BOOST_REQUIRE_EQUAL(client->read(boost::asio::buffer(rb), yield), rb.size())); 145 | BOOST_REQUIRE_EQUAL(wb, rb); 146 | 147 | BOOST_REQUIRE_NO_THROW(client->shutdown()); 148 | 149 | io.stop(); 150 | }, boost::asio::detached); 151 | 152 | io.run(); 153 | } 154 | -------------------------------------------------------------------------------- /src/plexus/utils.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Novemus Band. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | */ 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | namespace plexus { namespace utils { 23 | 24 | template 25 | std::string to_base64_impl(const void* data, size_t length) 26 | { 27 | BIO* b64 = BIO_new(BIO_f_base64()); 28 | BIO* out = BIO_new(BIO_s_mem()); 29 | 30 | if(no_nl) 31 | { 32 | BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); 33 | } 34 | out = BIO_push(b64, out); 35 | BIO_write(out, data, (int) length); 36 | (void) BIO_flush(out); 37 | 38 | char* data_ptr(0); 39 | size_t data_size = BIO_get_mem_data(out, &data_ptr); 40 | if(url_cvt) 41 | { 42 | for(char* p=data_ptr, *end=(data_ptr+data_size); p!=end; ++p) 43 | { 44 | switch(*p) 45 | { 46 | case '/': *p = '_'; 47 | break; 48 | case '+': *p = '-'; 49 | break; 50 | } 51 | } 52 | } 53 | 54 | std::string res(data_ptr, data_size); 55 | 56 | BIO_free_all(out); 57 | 58 | return res; 59 | } 60 | 61 | std::string to_base64(const void* data, size_t length) 62 | { 63 | return to_base64_impl(data, length); 64 | } 65 | 66 | std::string to_base64_no_nl(const void* data, size_t length) 67 | { 68 | return to_base64_impl(data, length); 69 | } 70 | 71 | std::string to_base64_url(const void* data, size_t length) 72 | { 73 | return to_base64_impl(data,length); 74 | } 75 | 76 | template 77 | std::string from_base64_impl(const char* data, size_t length) 78 | { 79 | BIO* b64 = BIO_new(BIO_f_base64()); 80 | BIO* out = BIO_new(BIO_s_mem()); 81 | BIO* in = BIO_new_mem_buf((void*) data, (int) length); 82 | 83 | if(url_cvt) 84 | { 85 | char* data_ptr(0); 86 | size_t data_size = BIO_get_mem_data(in, &data_ptr); 87 | for(char* p=data_ptr, *end=(data_ptr+data_size); p!=end; ++p) 88 | { 89 | switch(*p) 90 | { 91 | case '_': *p = '/'; 92 | break; 93 | case '-': *p = '+'; 94 | break; 95 | } 96 | } 97 | } 98 | 99 | BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); 100 | if(!no_nl) 101 | { 102 | for (const char* ptr = data; ptr < data + length; ++ptr) 103 | if (*ptr == '\r' || *ptr == '\n') 104 | { 105 | BIO_clear_flags(b64, BIO_FLAGS_BASE64_NO_NL); 106 | break; 107 | } 108 | } 109 | 110 | b64 = BIO_push(b64, in); 111 | 112 | char inbuf[512]; 113 | int ret = 0; 114 | while(true) 115 | { 116 | ret = BIO_read(b64, inbuf, 512); 117 | if (ret <= 0) 118 | break; 119 | 120 | BIO_write(out, inbuf, ret); 121 | } 122 | 123 | char* data_ptr(0); 124 | size_t data_size = BIO_get_mem_data(out, &data_ptr); 125 | std::string res(data_ptr, data_size); 126 | 127 | BIO_free(in); 128 | BIO_free(out); 129 | BIO_free(b64); 130 | 131 | return res; 132 | } 133 | std::string from_base64(const char* data, size_t length) 134 | { 135 | return from_base64_impl(data,length); 136 | } 137 | std::string from_base64_url(const char* data, size_t length) 138 | { 139 | return from_base64_impl(data,length); 140 | } 141 | 142 | std::string format(const char* fmt, ...) 143 | { 144 | va_list args1; 145 | va_start(args1, fmt); 146 | va_list args2; 147 | va_copy(args2, args1); 148 | std::vector buf(1 + std::vsnprintf(nullptr, 0, fmt, args1)); 149 | va_end(args1); 150 | std::vsnprintf(buf.data(), buf.size(), fmt, args2); 151 | va_end(args2); 152 | return buf.data(); 153 | } 154 | 155 | std::string format(const char* fmt, const boost::posix_time::ptime& time) 156 | { 157 | std::stringstream out; 158 | out.imbue(std::locale(std::cout.getloc(), new boost::posix_time::time_facet(fmt))); 159 | out << time; 160 | return out.str(); 161 | } 162 | 163 | std::string format(const char* fmt, const std::chrono::system_clock::time_point& time) 164 | { 165 | std::time_t tt = std::chrono::system_clock::to_time_t(time); 166 | std::tm tm = *std::gmtime(&tt); 167 | std::stringstream ss; 168 | ss << std::put_time(&tm, fmt); 169 | return ss.str(); 170 | } 171 | 172 | std::string to_hexadecimal(const void* data, size_t len) 173 | { 174 | std::stringstream out; 175 | for (size_t i = 0; i < len; ++i) 176 | { 177 | out << std::setw(2) << std::setfill('0') << std::hex << (int)((uint8_t*)data)[i]; 178 | } 179 | return out.str(); 180 | } 181 | 182 | std::string get_email_address(const std::string& email) 183 | { 184 | std::smatch match; 185 | if (std::regex_search(email, match, std::regex("[\\w\\s]*\\<([^\\<]+@[^\\>]+)\\>\\s*"))) 186 | { 187 | return match[1].str(); 188 | } 189 | return email; 190 | } 191 | 192 | }} 193 | -------------------------------------------------------------------------------- /src/plexus/binder.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Novemus Band. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | */ 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | namespace plexus { namespace stun { 20 | 21 | class binder_impl : public plexus::stun_binder 22 | { 23 | boost::asio::io_context& m_io; 24 | boost::asio::ip::udp::endpoint m_stun; 25 | boost::asio::ip::udp::endpoint m_bind; 26 | uint16_t m_punch; 27 | 28 | class handshake : public tubus::mutable_buffer 29 | { 30 | uint64_t m_mask; 31 | 32 | uint8_t get_mask_byte(size_t pos) const 33 | { 34 | return uint8_t(m_mask >> (pos * 8)); 35 | } 36 | 37 | public: 38 | 39 | handshake(uint8_t flag, uint64_t mask) : mutable_buffer(8), m_mask(mask) 40 | { 41 | uint8_t sum = 0; 42 | for (size_t i = 0; i < 7; ++i) 43 | { 44 | uint8_t byte = utils::random(); 45 | 46 | if (i == 0) 47 | byte = flag ? (byte | 0x01) : (byte & 0xfe); 48 | 49 | sum ^= byte; 50 | set(i, byte ^ get_mask_byte(i)); 51 | } 52 | 53 | set(7, sum ^ get_mask_byte(7)); 54 | } 55 | 56 | handshake(uint64_t mask) : mutable_buffer(8), m_mask(mask) 57 | { 58 | } 59 | 60 | uint8_t flag() const 61 | { 62 | uint8_t sum = get(7) ^ get_mask_byte(7); 63 | 64 | for (size_t i = 0; i < 7; ++i) 65 | sum ^= get(i) ^ get_mask_byte(i); 66 | 67 | if (sum != 0) 68 | throw plexus::context_error(__FUNCTION__, "bad message checksum"); 69 | 70 | return (get(0) ^ get_mask_byte(0)) & 0x01; 71 | } 72 | }; 73 | 74 | public: 75 | 76 | binder_impl(boost::asio::io_context& io, const boost::asio::ip::udp::endpoint& stun, const boost::asio::ip::udp::endpoint& bind, uint16_t punch) 77 | : m_io(io) 78 | , m_stun(stun) 79 | , m_bind(bind) 80 | , m_punch(punch) 81 | { 82 | _dbg_ << "stun server: " << stun; 83 | _dbg_ << "stun client: " << bind; 84 | } 85 | 86 | std::shared_ptr reach_peer(boost::asio::yield_context yield, const boost::asio::ip::udp::endpoint& peer, uint64_t mask) noexcept(false) override 87 | { 88 | _dbg_ << "reaching peer..."; 89 | 90 | auto timer = [start = boost::posix_time::microsec_clock::universal_time()]() 91 | { 92 | return boost::posix_time::microsec_clock::universal_time() - start; 93 | }; 94 | 95 | int64_t deadline = plexus::utils::getenv("PLEXUS_HANDSHAKE_TIMEOUT", 60000); 96 | 97 | auto pin = plexus::network::create_udp_transport(m_io, m_bind); 98 | handshake out(0, mask); 99 | handshake in(mask); 100 | 101 | while (timer().total_milliseconds() < deadline) 102 | { 103 | try 104 | { 105 | pin->send_to(out, peer, yield); 106 | 107 | if (out.flag() == 1) 108 | { 109 | _dbg_ << "handshake peer: " << peer; 110 | return pin; 111 | } 112 | 113 | in.truncate(pin->receive_from(in, peer, yield)); 114 | 115 | if (in.flag() == 1) 116 | { 117 | out = handshake(1, mask); 118 | } 119 | } 120 | catch(const boost::system::system_error& ex) 121 | { 122 | if (ex.code() != boost::asio::error::operation_aborted) 123 | throw plexus::context_error(__FUNCTION__, ex.code()); 124 | 125 | _trc_ << ex.what(); 126 | } 127 | } 128 | 129 | throw plexus::timeout_error(__FUNCTION__); 130 | } 131 | 132 | std::shared_ptr await_peer(boost::asio::yield_context yield, const boost::asio::ip::udp::endpoint& peer, uint64_t mask) noexcept(false) override 133 | { 134 | _dbg_ << "awaiting peer..."; 135 | 136 | auto timer = [start = boost::posix_time::microsec_clock::universal_time()]() 137 | { 138 | return boost::posix_time::microsec_clock::universal_time() - start; 139 | }; 140 | 141 | int64_t deadline = plexus::utils::getenv("PLEXUS_HANDSHAKE_TIMEOUT", 60000); 142 | 143 | auto pin = plexus::network::create_udp_transport(m_io, m_bind); 144 | 145 | boost::asio::ip::unicast::hops old; 146 | pin->get_option(old); 147 | pin->set_option(boost::asio::ip::unicast::hops(m_punch)); 148 | pin->send_to(handshake(0, mask), peer, yield, 2000); 149 | pin->set_option(old); 150 | 151 | handshake out(1, mask); 152 | handshake in(mask); 153 | 154 | while (timer().total_milliseconds() < deadline) 155 | { 156 | try 157 | { 158 | in.truncate(pin->receive_from(in, peer, yield)); 159 | 160 | if (in.flag() == 0) 161 | { 162 | pin->send_to(out, peer, yield); 163 | } 164 | else 165 | { 166 | _dbg_ << "handshake peer: " << peer; 167 | return pin; 168 | } 169 | } 170 | catch(const boost::system::system_error& ex) 171 | { 172 | if (ex.code() != boost::asio::error::operation_aborted) 173 | throw plexus::context_error(__FUNCTION__, ex.code()); 174 | 175 | _trc_ << ex.what(); 176 | } 177 | } 178 | 179 | throw plexus::timeout_error(__FUNCTION__); 180 | } 181 | 182 | traverse explore_network(boost::asio::yield_context yield) noexcept(false) override 183 | { 184 | auto stun = plexus::create_stun_client(m_io, m_stun, m_bind); 185 | return stun->explore_network(yield); 186 | } 187 | }; 188 | 189 | } 190 | 191 | std::shared_ptr create_stun_binder(boost::asio::io_context& io, const boost::asio::ip::udp::endpoint& stun, const boost::asio::ip::udp::endpoint& bind, uint16_t punch) noexcept(false) 192 | { 193 | boost::asio::ip::udp::socket socket(io, stun.protocol()); 194 | socket.set_option(boost::asio::socket_base::reuse_address(true)); 195 | socket.bind(bind); 196 | 197 | return std::make_shared(io, stun, socket.local_endpoint(), punch); 198 | } 199 | 200 | } 201 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | The [plexus](https://github.com/novemus/plexus) tool is designed to make the possibility of establishing a direct network connection between applications running on machines located behind *NAT*s. To do this `plexus` implements the well-known *UDP hole punching* technique using the *STUN*. You can use a `DHT` network or `email` service as a rendezvous to exchange public addresses between local and remote app instances. For success, it is necessary that *NAT*s on both sides implement independent mapping policy of internal addresses to public ones. That is, the public address and port assigned by the *NAT* should not be changed when the destination address/port of the outgoing packet is changed, while the source address/port of the outgoing packet remains unchanged. In addition, you will need an accessible *STUN* server and email accounts for each side if you want to use `email` as the rendezvous. It is possible to use one email account for both sides. 4 | 5 | For those who prefer GUI, there is a [webpier](https://github.com/novemus/webpier) application based on the `plexus` library. 6 | 7 | ## Build 8 | 9 | You can download [prebuild packages](https://github.com/novemus/plexus/releases) for Debian and Windows platforms. 10 | 11 | The project depends on [boost](https://github.com/boostorg/boost), [openssl](https://github.com/openssl/openssl), [opendht](https://github.com/savoirfairelinux/opendht), [wormhole](https://github.com/novemus/wormhole) and [tubus](https://github.com/novemus/tubus) libraries. Clone the repository, then configure and build project. 12 | 13 | ```console 14 | $ cd ~ 15 | $ git clone https://github.com/novemus/plexus.git 16 | $ cd ~/plexus 17 | $ [PKG_CONFIG_PATH=...] [CMAKE_PREFIX_PATH=...] cmake -B ./build -DCMAKE_BUILD_TYPE=Release [-DBUILD_SHARED_LIBS=ON] [-DBOOST_ROOT=...] [-DOPENSSL_ROOT_DIR=...] 18 | $ cmake --build ./build --target all 19 | $ cmake --build ./build --target install 20 | ``` 21 | 22 | ## Using 23 | 24 | Create an application repository and specify the path to the host directory according to the scheme `apprepo/owner@mailer.com/pin`. You must place there the X509 *cert.crt* certificate and the *private.key* key of the host. Likewise, make the directory for the peer and place there its *cert.crt* certificate. Make the same on the peer machine. The intermediate directory `owner@mailer.com` can be named as you want if you use the `DHT` rendezvous, but for the `email` case it must have the name of the appropriate email address. So for compatibility, it is recommended to always use the email address. 25 | 26 | To run the example below you need to install the *openvpn*. The *exec.sh* script will try to establish point-to-point *VPN* connection. 27 | 28 | Command for the accepting side: 29 | ```console 30 | $ plexus --app-id=appname --app-repo=/path/to/apprepo --accept --dht-bootstrap=bootstrap.jami.net:4222 --host-info=host@mailer.com/hostid --peer-info=peer@mailer.com/peerid --stun-server=stun.someserver.com[:xxxx] --exec-command=~/plexus/tests/exec.sh 31 | ``` 32 | 33 | Command for the inviting side: 34 | ```console 35 | $ plexus --app-id=appname --app-repo=/path/to/apprepo --dht-bootstrap=bootstrap.jami.net:4222 --host-info=host@mailer.com/hostid --peer-info=peer@mailer.com/peerid --stun-server=stun.someserver.com[:xxxx] --exec-command=~/plexus/tests/exec.sh 36 | ``` 37 | 38 | The `--app-id` key determines the target application. The `--host-info` argument points to the local application identity and the `--peer-info` points to the remote one. The `--app-repo` key is used to specify the application repository. The `--accept` key tells the app to cyclically accept invitations from the remotes. It must only be set for one side. If you want to accept many peers you should just omit the `--peer-info` argument. In this case, every peer is contained in the repository will be accepted. 39 | 40 | Some *NAT*s may drop mapping when receiving an incoming packet that does not meet the filtering policy. This package may be a punching package sent by `plexus` towards the peer. In this case, it is impossible to punch the *passage* between the machines. To avoid such situations, `plexus` sets a small *ttl* to the punching packet, by default 7. In general, this is enough for the packet to go beyond the host *NAT* to punch it, but not to reach the peer *NAT* and not to drop its peer mapping. If necessary, you can set a more appropriate *ttl* using the `--punch-hops` argument, determining the suitable value by some routing utility. This only makes sense for the *accepting* side. 41 | 42 | As soon as both `plexus` instanses make the *passage* to each other the command specified by `--exec-command` will be started. You can pass your arguments to the executable by `--exec-args` argument with the following wildcards: 43 | 44 | `%innerip%` - local address specified by `--stun-client` argument 45 | 46 | `%innerport%` - local port specified by the `--stun-client` argument 47 | 48 | `%outerip%` - public address issued by the NAT 49 | 50 | `%outerport%` - port issued by the NAT 51 | 52 | `%peerip%` - public address of the peer received by the rendezvous 53 | 54 | `%peerport%` - port of the peer received by the rendezvous 55 | 56 | `%hostmail%` - owner of the host, first part of the `--host-info` argument 57 | 58 | `%peermail%` - owner of the peer, first part of the `--peer-info` argument 59 | 60 | `%hostpin%` - id of the host, second part of the `--host-info` argument 61 | 62 | `%peerpin%` - id of the peer, second part of the `--peer-info` argument 63 | 64 | `%secret%` - shared 64-bit key used for the handshake procedure 65 | 66 | To learn about additional parameters run the tool with the `--help` key. 67 | 68 | ## Extensions and Library 69 | 70 | If you need to link *TCP* applications and cannot use *VPN* for some reason, then consider the [wormhole](https://github.com/novemus/wormhole) tunneling extension as execution payload. For example, you can forward the remote *ssh* service with the following payload arguments. 71 | 72 | Remote machine: 73 | ```console 74 | --exec-command=wormhole --exec-args="--purpose=export --service=127.0.0.1:22 --gateway=%innerip%:%innerport% --faraway=%peerip%:%peerport%" --exec-log=export.ssh.log 75 | ``` 76 | 77 | Local machine: 78 | ```console 79 | --exec-command=wormhole --exec-args="--purpose=import --service=127.0.0.1:2222 --gateway=%innerip%:%innerport% --faraway=%peerip%:%peerport%" --exec-log=import.ssh.log 80 | ``` 81 | 82 | Then connect to the remote *ssh* via the local mapping: 83 | ```console 84 | $ ssh -p 2222 127.0.0.1 85 | ``` 86 | 87 | The `plexus` library API is described in the [plexus.h](https://github.com/novemus/plexus/blob/master/plexus.h) header and provides the same functionality with the additional UDP streaming capability, so you will need the [tubus](https://github.com/novemus/tubus) UDP library. 88 | 89 | ## Bugs and improvements 90 | 91 | Feel free to [report](https://github.com/novemus/plexus/issues) bugs and [suggest](https://github.com/novemus/plexus/issues) improvements. 92 | 93 | ## License 94 | 95 | Plexus is licensed under the Apache License 2.0, which means that you are free to get and use it for commercial and non-commercial purposes as long as you fulfill its conditions. See the LICENSE.txt file for more details. 96 | 97 | ## Copyright 98 | 99 | Copyright © 2022 Novemus Band. All Rights Reserved. 100 | -------------------------------------------------------------------------------- /src/plexus/socket.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Novemus Band. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | */ 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | namespace plexus { namespace network { 23 | 24 | template struct asio_socket : public socket_impl 25 | { 26 | typedef typename socket_impl::lowest_layer_type::endpoint_type endpoint_type; 27 | 28 | template 29 | asio_socket(const protocol_type& protocol, boost::asio::io_context& io, arguments&... args) 30 | : socket_impl(io, args...), m_io(io) 31 | { 32 | socket_impl::lowest_layer().open(protocol); 33 | 34 | static const size_t SOCKET_BUFFER_SIZE = 1048576; 35 | 36 | socket_impl::lowest_layer().non_blocking(true); 37 | socket_impl::lowest_layer().set_option(boost::asio::socket_base::reuse_address(true)); 38 | socket_impl::lowest_layer().set_option(boost::asio::socket_base::send_buffer_size(SOCKET_BUFFER_SIZE)); 39 | socket_impl::lowest_layer().set_option(boost::asio::socket_base::receive_buffer_size(SOCKET_BUFFER_SIZE)); 40 | } 41 | 42 | template 43 | asio_socket(const endpoint_type& remote, boost::asio::io_context& io, arguments&... args) 44 | : asio_socket(remote.protocol(), io, args...) 45 | { 46 | m_remote = remote; 47 | } 48 | 49 | void wait(boost::asio::socket_base::wait_type type, boost::asio::yield_context yield, int64_t timeout = timeout_ms) noexcept(false) 50 | { 51 | execute([&]() { 52 | socket_impl::lowest_layer().async_wait(type, yield); 53 | return 0; 54 | }, timeout); 55 | } 56 | 57 | void connect(boost::asio::yield_context yield, int64_t timeout = timeout_ms) noexcept(false) 58 | { 59 | execute([&]() { 60 | socket_impl::lowest_layer().async_connect(m_remote, yield); 61 | return 0; 62 | }, timeout); 63 | } 64 | 65 | void shutdown() noexcept(true) 66 | { 67 | if (socket_impl::lowest_layer().is_open()) 68 | { 69 | boost::system::error_code ec; 70 | socket_impl::lowest_layer().shutdown(boost::asio::socket_base::shutdown_both, ec); 71 | socket_impl::lowest_layer().close(ec); 72 | } 73 | } 74 | 75 | template 76 | void handshake(handshake_type type, boost::asio::yield_context yield, int64_t timeout = timeout_ms) noexcept(false) 77 | { 78 | execute([&]() { 79 | socket_impl::async_handshake(type, yield); 80 | return 0; 81 | }, timeout); 82 | } 83 | 84 | template 85 | size_t write_some(const const_buffer_type& buffer, boost::asio::yield_context yield, int64_t timeout = timeout_ms) noexcept(false) 86 | { 87 | return execute([&]() { 88 | return socket_impl::async_write_some(buffer, yield); 89 | }, timeout); 90 | } 91 | 92 | template 93 | size_t read_some(const mutable_buffer_type& buffer, boost::asio::yield_context yield, int64_t timeout = timeout_ms) noexcept(false) 94 | { 95 | return execute([&]() { 96 | return socket_impl::async_read_some(buffer, yield); 97 | }, timeout); 98 | } 99 | 100 | template 101 | size_t write(const const_buffer_type& buffer, boost::asio::yield_context yield, int64_t timeout = timeout_ms) noexcept(false) 102 | { 103 | return execute([&]() { 104 | return boost::asio::async_write(static_cast(*this), buffer, yield); 105 | }, timeout); 106 | } 107 | 108 | template 109 | size_t read(const mutable_buffer_type& buffer, boost::asio::yield_context yield, int64_t timeout = timeout_ms) noexcept(false) 110 | { 111 | return execute([&]() { 112 | return boost::asio::async_read(static_cast(*this), buffer, yield); 113 | }, timeout); 114 | } 115 | 116 | template 117 | size_t send_to(const const_buffer_type& buffer, const endpoint_type& endpoint, boost::asio::yield_context yield, int64_t timeout = timeout_ms) noexcept(false) 118 | { 119 | return execute([&]() { 120 | return socket_impl::async_send_to(buffer, endpoint, yield); 121 | }, timeout); 122 | } 123 | 124 | template 125 | size_t receive_from(const mutable_buffer_type& buffer, const endpoint_type& endpoint, boost::asio::yield_context yield, int64_t timeout = timeout_ms) noexcept(false) 126 | { 127 | auto timer = [start = boost::posix_time::microsec_clock::universal_time()]() 128 | { 129 | return boost::posix_time::microsec_clock::universal_time() - start; 130 | }; 131 | 132 | while (timer().total_milliseconds() < timeout) 133 | { 134 | endpoint_type source; 135 | 136 | size_t size = execute([&]() { 137 | return socket_impl::async_receive_from(buffer, source, yield); 138 | }, timeout - timer().total_milliseconds()); 139 | 140 | if (is_matched(source, endpoint)) 141 | return size; 142 | } 143 | 144 | throw boost::system::error_code(boost::asio::error::operation_aborted); 145 | } 146 | 147 | private: 148 | 149 | static bool is_matched(const endpoint_type& source, const endpoint_type& match) 150 | { 151 | return (match.address().is_unspecified() || match.address() == source.address()) && (match.port() == 0 || match.port() == source.port()); 152 | } 153 | 154 | size_t execute(const std::function& function, int64_t timeout = timeout_ms) noexcept(false) 155 | { 156 | boost::asio::deadline_timer timer(m_io); 157 | if (timeout > 0) 158 | { 159 | timer.expires_from_now(boost::posix_time::milliseconds(timeout)); 160 | timer.async_wait([&](const boost::system::error_code& error) 161 | { 162 | if(error) 163 | { 164 | if (error == boost::asio::error::operation_aborted) 165 | return; 166 | 167 | _err_ << error.message(); 168 | } 169 | 170 | try 171 | { 172 | socket_impl::lowest_layer().cancel(); 173 | } 174 | catch (const std::exception &ex) 175 | { 176 | _err_ << ex.what(); 177 | } 178 | }); 179 | } 180 | 181 | size_t res = function(); 182 | 183 | timer.cancel(); 184 | 185 | return res; 186 | } 187 | 188 | boost::asio::io_context& m_io; 189 | endpoint_type m_remote; 190 | }; 191 | 192 | }} 193 | -------------------------------------------------------------------------------- /src/plexus/features.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Novemus Band. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | */ 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | namespace plexus { 24 | 25 | struct context_error : public std::runtime_error 26 | { 27 | context_error(const std::string& loc, const std::string& msg) 28 | : std::runtime_error(utils::format("%s: %s", loc.c_str(), msg.c_str())) 29 | {} 30 | 31 | context_error(const std::string& loc, const boost::system::error_code& code) 32 | : std::runtime_error(utils::format("%s: %s", loc.c_str(), code.message().c_str())) 33 | {} 34 | }; 35 | 36 | struct timeout_error : public context_error 37 | { 38 | timeout_error(const std::string& loc) 39 | : context_error(loc, "timeout error") 40 | {} 41 | }; 42 | 43 | static constexpr const char* cert_file_name = "cert.crt"; 44 | static constexpr const char* key_file_name = "private.key"; 45 | static constexpr const char* ca_file_name = "ca.crt"; 46 | 47 | void exec(const std::string& prog, const std::string& args = "", const std::string& dir = "", const std::string& log = "", bool wait = false) noexcept(false); 48 | 49 | std::ostream& operator<<(std::ostream& stream, const reference& value); 50 | std::ostream& operator<<(std::ostream& stream, const identity& value); 51 | std::istream& operator>>(std::istream& in, reference& level); 52 | std::istream& operator>>(std::istream& in, identity& level); 53 | 54 | struct stun_client 55 | { 56 | virtual ~stun_client() {} 57 | virtual traverse explore_network(boost::asio::yield_context yield) noexcept(false) = 0; 58 | }; 59 | 60 | std::shared_ptr create_stun_client(boost::asio::io_context& io, const boost::asio::ip::udp::endpoint& stun, const boost::asio::ip::udp::endpoint& bind) noexcept(true); 61 | 62 | struct stun_binder : public stun_client 63 | { 64 | virtual std::shared_ptr reach_peer(boost::asio::yield_context yield, const boost::asio::ip::udp::endpoint& peer, uint64_t mask) noexcept(false) = 0; 65 | virtual std::shared_ptr await_peer(boost::asio::yield_context yield, const boost::asio::ip::udp::endpoint& peer, uint64_t mask) noexcept(false) = 0; 66 | }; 67 | 68 | std::shared_ptr create_stun_binder(boost::asio::io_context& io, const boost::asio::ip::udp::endpoint& stun, const boost::asio::ip::udp::endpoint& bind, uint16_t punch) noexcept(false); 69 | 70 | struct pipe 71 | { 72 | virtual ~pipe() {} 73 | virtual reference pull_request(boost::asio::yield_context yield) noexcept(false) = 0; 74 | virtual reference pull_response(boost::asio::yield_context yield) noexcept(false) = 0; 75 | virtual void push_response(boost::asio::yield_context yield, const reference& gateway) noexcept(false) = 0; 76 | virtual void push_request(boost::asio::yield_context yield, const reference& gateway) noexcept(false) = 0; 77 | virtual const identity& host() const noexcept(true) = 0; 78 | virtual const identity& peer() const noexcept(true) = 0; 79 | }; 80 | 81 | using coroutine = std::function pipe)>; 82 | 83 | template struct context : public mediator 84 | { 85 | std::string app; 86 | std::string repo; 87 | 88 | context(const std::string& app, const std::string& repo, const mediator& conf) noexcept(true) 89 | : mediator(conf) 90 | , app(app) 91 | , repo(repo) 92 | { 93 | } 94 | 95 | context(const context& conf) noexcept(true) 96 | : mediator(conf) 97 | , app(conf.app) 98 | , repo(conf.repo) 99 | { 100 | } 101 | 102 | bool are_defined(const identity& host, const identity& peer) const noexcept(true) 103 | { 104 | return !host.owner.empty() && !host.pin.empty() && !peer.owner.empty() && !peer.pin.empty(); 105 | } 106 | 107 | bool are_allowed(const identity& host, const identity& peer) const noexcept(false) 108 | { 109 | return are_defined(host, peer) 110 | && std::filesystem::exists(std::filesystem::path(std::filesystem::path(repo) / host.owner / host.pin)) 111 | && std::filesystem::exists(std::filesystem::path(std::filesystem::path(repo) / peer.owner / peer.pin)); 112 | } 113 | 114 | bool are_encryptable(const identity& host, const identity& peer) const noexcept(false) 115 | { 116 | return std::filesystem::exists(std::filesystem::path(std::filesystem::path(repo) / host.owner / host.pin / cert_file_name)) 117 | && std::filesystem::exists(std::filesystem::path(std::filesystem::path(repo) / host.owner / host.pin / key_file_name)) 118 | && std::filesystem::exists(std::filesystem::path(std::filesystem::path(repo) / peer.owner / peer.pin / cert_file_name)); 119 | } 120 | 121 | bool has_cert(const identity& info) const noexcept(false) 122 | { 123 | return std::filesystem::exists(std::filesystem::path(std::filesystem::path(repo) / info.owner / info.pin / cert_file_name)); 124 | } 125 | 126 | bool has_key(const identity& info) const noexcept(false) 127 | { 128 | return std::filesystem::exists(std::filesystem::path(std::filesystem::path(repo) / info.owner / info.pin / key_file_name)); 129 | } 130 | 131 | bool has_ca(const identity& info) const noexcept(false) 132 | { 133 | return std::filesystem::exists(std::filesystem::path(std::filesystem::path(repo) / info.owner / info.pin / ca_file_name)); 134 | } 135 | 136 | std::string get_cert(const identity& info) const noexcept(false) 137 | { 138 | std::filesystem::path cert(std::filesystem::path(repo) / info.owner / info.pin / cert_file_name); 139 | return std::filesystem::exists(cert) ? cert.generic_u8string() : ""; 140 | } 141 | 142 | std::string get_key(const identity& info) const noexcept(false) 143 | { 144 | std::filesystem::path key(std::filesystem::path(repo) / info.owner / info.pin / key_file_name); 145 | return std::filesystem::exists(key) ? key.generic_u8string() : ""; 146 | } 147 | 148 | std::string get_ca(const identity& info) const noexcept(false) 149 | { 150 | std::filesystem::path ca(std::filesystem::path(repo) / info.owner / info.pin / ca_file_name); 151 | return std::filesystem::exists(ca) ? ca.generic_u8string() : ""; 152 | } 153 | }; 154 | 155 | template 156 | void spawn_accept(boost::asio::io_context& io, const context& conf, const identity& host, const identity& peer, const coroutine& handler) noexcept(true); 157 | template 158 | void spawn_invite(boost::asio::io_context& io, const context& conf, const identity& host, const identity& peer, const coroutine& handler) noexcept(true); 159 | 160 | template 161 | void forward_advent(boost::asio::io_context& io, const context& conf, const identity& host, const identity& peer, const observer& reveal, const fallback& failure = nullptr) noexcept(true); 162 | template 163 | void receive_advent(boost::asio::io_context& io, const context& conf, const identity& host, const identity& peer, const observer& reveal, const fallback& failure = nullptr) noexcept(true); 164 | 165 | } 166 | -------------------------------------------------------------------------------- /tests/ssl_tests.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Novemus Band. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | */ 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | namespace { 22 | 23 | class ssl_echo_session : public std::enable_shared_from_this 24 | { 25 | enum { max_length = 1024 }; 26 | 27 | char m_data[max_length]; 28 | boost::asio::ssl::stream m_socket; 29 | 30 | public: 31 | 32 | ssl_echo_session(boost::asio::io_context& io, boost::asio::ssl::context &ssl) 33 | : m_socket(io, ssl) 34 | { 35 | } 36 | 37 | boost::asio::ssl::stream::lowest_layer_type &socket() 38 | { 39 | return m_socket.lowest_layer(); 40 | } 41 | 42 | void start() 43 | { 44 | m_socket.async_handshake( 45 | boost::asio::ssl::stream_base::server, 46 | boost::bind(&ssl_echo_session::handle_handshake, shared_from_this(), boost::asio::placeholders::error) 47 | ); 48 | } 49 | 50 | protected: 51 | 52 | void handle_handshake(const boost::system::error_code &error) 53 | { 54 | if (!error) 55 | { 56 | m_socket.async_read_some( 57 | boost::asio::buffer(m_data, max_length), 58 | boost::bind(&ssl_echo_session::handle_read, shared_from_this(), boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred) 59 | ); 60 | } 61 | } 62 | 63 | void handle_read(const boost::system::error_code &error, size_t transferred) 64 | { 65 | if (!error) 66 | { 67 | m_socket.async_write_some( 68 | boost::asio::buffer(m_data, transferred), 69 | boost::bind(&ssl_echo_session::handle_write, shared_from_this(), boost::asio::placeholders::error) 70 | ); 71 | } 72 | } 73 | 74 | void handle_write(const boost::system::error_code &error) 75 | { 76 | if (!error) 77 | { 78 | m_socket.async_read_some( 79 | boost::asio::buffer(m_data, max_length), 80 | boost::bind(&ssl_echo_session::handle_read, shared_from_this(), boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred) 81 | ); 82 | } 83 | } 84 | }; 85 | 86 | class ssl_echo_server 87 | { 88 | boost::asio::io_context& m_io; 89 | boost::asio::ip::tcp::acceptor m_acceptor; 90 | boost::asio::ssl::context m_ssl; 91 | 92 | public: 93 | 94 | ssl_echo_server(boost::asio::io_context& io, unsigned short port, const std::string& cert, const std::string& key, const std::string& ca = "") 95 | : m_io(io) 96 | , m_acceptor(m_io, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port)) 97 | , m_ssl(boost::asio::ssl::context::sslv23) 98 | { 99 | m_ssl.set_options(boost::asio::ssl::context::default_workarounds| boost::asio::ssl::context::sslv23_server); 100 | m_ssl.use_certificate_file(cert, boost::asio::ssl::context::pem); 101 | m_ssl.use_private_key_file(key, boost::asio::ssl::context::pem); 102 | if (!ca.empty()) 103 | { 104 | m_ssl.set_verify_mode(boost::asio::ssl::verify_peer | boost::asio::ssl::verify_fail_if_no_peer_cert | boost::asio::ssl::verify_client_once); 105 | m_ssl.load_verify_file(ca); 106 | } 107 | 108 | boost::asio::post(m_io, boost::bind(&ssl_echo_server::start_accept, this)); 109 | } 110 | 111 | protected: 112 | 113 | void start_accept() 114 | { 115 | auto session = std::make_shared(m_io, m_ssl); 116 | m_acceptor.async_accept( 117 | session->socket(), 118 | boost::bind(&ssl_echo_server::handle_accept, this, session, boost::asio::placeholders::error) 119 | ); 120 | } 121 | 122 | void handle_accept(std::shared_ptr session, const boost::system::error_code &error) 123 | { 124 | if (!error) 125 | session->start(); 126 | 127 | start_accept(); 128 | } 129 | }; 130 | 131 | std::shared_ptr create_ssl_server(boost::asio::io_context& io, unsigned short port, const std::string& cert = "", const std::string& key = "", const std::string& ca = "") 132 | { 133 | return std::make_shared(io, port, cert, key, ca); 134 | } 135 | 136 | const uint16_t SSL_PORT = 4433; 137 | const boost::asio::ip::tcp::endpoint SSL_SERVER(boost::asio::ip::make_address("127.0.0.1"), SSL_PORT); 138 | const boost::asio::ip::tcp::endpoint WRONG_SSL_SERVER(boost::asio::ip::make_address("1.2.3.4"), SSL_PORT); 139 | 140 | } 141 | 142 | BOOST_AUTO_TEST_CASE(no_check_certs) 143 | { 144 | boost::asio::io_context io; 145 | auto server = create_ssl_server(io, SSL_PORT, "./certs/server.crt", "./certs/server.key"); 146 | 147 | boost::asio::spawn(io, [&](boost::asio::yield_context yield) 148 | { 149 | auto client = plexus::network::create_ssl_client(io, SSL_SERVER); 150 | BOOST_REQUIRE_NO_THROW(client->connect(yield)); 151 | BOOST_REQUIRE_NO_THROW(client->handshake(boost::asio::ssl::stream_base::client, yield)); 152 | 153 | std::string wb = "hello"; 154 | std::string rb; 155 | 156 | rb.resize(wb.size()); 157 | 158 | BOOST_CHECK_EQUAL(client->write(boost::asio::buffer(wb), yield), wb.size()); 159 | BOOST_CHECK_EQUAL(client->read(boost::asio::buffer(rb), yield), rb.size()); 160 | BOOST_CHECK_EQUAL(wb, rb); 161 | 162 | wb = "bye bye"; 163 | rb.resize(wb.size()); 164 | 165 | BOOST_CHECK_EQUAL(client->write(boost::asio::buffer(wb), yield), wb.size()); 166 | BOOST_CHECK_EQUAL(client->read(boost::asio::buffer(rb), yield), rb.size()); 167 | BOOST_CHECK_EQUAL(wb, rb); 168 | 169 | BOOST_REQUIRE_NO_THROW(client->shutdown()); 170 | 171 | io.stop(); 172 | }, boost::asio::detached); 173 | 174 | io.run(); 175 | } 176 | 177 | BOOST_AUTO_TEST_CASE(check_certs) 178 | { 179 | boost::asio::io_context io; 180 | auto server = create_ssl_server(io, SSL_PORT, "./certs/server.crt", "./certs/server.key", "./certs/ca.crt"); 181 | 182 | boost::asio::spawn(io, [&](boost::asio::yield_context yield) 183 | { 184 | auto client = plexus::network::create_ssl_client(io, SSL_SERVER, "./certs/client.crt", "./certs/client.key", "./certs/ca.crt"); 185 | BOOST_REQUIRE_NO_THROW(client->connect(yield)); 186 | BOOST_REQUIRE_NO_THROW(client->handshake(boost::asio::ssl::stream_base::client, yield)); 187 | 188 | std::string wb = "hello"; 189 | std::string rb; 190 | 191 | rb.resize(wb.size()); 192 | 193 | BOOST_CHECK_EQUAL(client->write(boost::asio::buffer(wb), yield), wb.size()); 194 | BOOST_CHECK_EQUAL(client->read(boost::asio::buffer(rb), yield), rb.size()); 195 | BOOST_CHECK_EQUAL(wb, rb); 196 | 197 | wb = "bye bye"; 198 | rb.resize(wb.size()); 199 | 200 | BOOST_CHECK_EQUAL(client->write(boost::asio::buffer(wb), yield), wb.size()); 201 | BOOST_CHECK_EQUAL(client->read(boost::asio::buffer(rb), yield), rb.size()); 202 | BOOST_CHECK_EQUAL(wb, rb); 203 | 204 | BOOST_REQUIRE_NO_THROW(client->shutdown()); 205 | 206 | io.stop(); 207 | }, boost::asio::detached); 208 | 209 | io.run(); 210 | } 211 | 212 | BOOST_AUTO_TEST_CASE(wrong_certs) 213 | { 214 | boost::asio::io_context io; 215 | auto server = create_ssl_server(io, SSL_PORT, "./certs/server.crt", "./certs/server.key", "./certs/ca.crt"); 216 | 217 | boost::asio::spawn(io, [&](boost::asio::yield_context yield) 218 | { 219 | auto failed = plexus::network::create_ssl_client(io, WRONG_SSL_SERVER); 220 | BOOST_REQUIRE_THROW(failed->connect(yield, 2000), boost::system::system_error); 221 | 222 | auto client = plexus::network::create_ssl_client(io, SSL_SERVER, "./certs/client.crt", "./certs/client.key", "./certs/ca.crt"); 223 | BOOST_REQUIRE_NO_THROW(client->connect(yield)); 224 | BOOST_REQUIRE_NO_THROW(client->handshake(boost::asio::ssl::stream_base::client, yield)); 225 | 226 | char rb; 227 | BOOST_REQUIRE_THROW(client->read(boost::asio::buffer(&rb, 1), yield, 2000), boost::system::system_error); 228 | 229 | BOOST_REQUIRE_NO_THROW(client->shutdown()); 230 | 231 | client = plexus::network::create_ssl_client(io, SSL_SERVER, "./certs/alien/client.crt", "./certs/alien/client.key", "./certs/alien/ca.crt"); 232 | BOOST_REQUIRE_NO_THROW(client->connect(yield)); 233 | BOOST_REQUIRE_THROW(client->handshake(boost::asio::ssl::stream_base::client, yield), boost::system::system_error); 234 | 235 | BOOST_REQUIRE_NO_THROW(client->shutdown()); 236 | 237 | io.stop(); 238 | }, boost::asio::detached); 239 | 240 | io.run(); 241 | } 242 | -------------------------------------------------------------------------------- /src/plexus/smime.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Novemus Band. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | */ 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | namespace plexus { namespace utils { 18 | 19 | std::string get_last_error() 20 | { 21 | std::string ssl = ERR_error_string(ERR_get_error(), NULL); 22 | std::string sys = strerror(errno); 23 | if (ssl.empty()) 24 | return sys; 25 | if (sys.empty()) 26 | return ssl; 27 | return ssl + "\n" + sys; 28 | } 29 | 30 | std::string smime_sign(const std::string& msg, const std::string& cert, const std::string& key) 31 | { 32 | if (cert.empty() || key.empty()) 33 | return msg; 34 | 35 | int flags = PKCS7_DETACHED | PKCS7_STREAM | PKCS7_NOCERTS; 36 | 37 | std::shared_ptr cert_bio(BIO_new_file(cert.c_str(), "r"), BIO_free); 38 | if (!cert_bio) 39 | throw plexus::context_error(__FUNCTION__, get_last_error()); 40 | 41 | std::shared_ptr pcert(PEM_read_bio_X509(cert_bio.get(), NULL, 0, NULL), X509_free); 42 | 43 | std::shared_ptr key_bio(BIO_new_file(key.c_str(), "r"), BIO_free); 44 | if (!key_bio) 45 | throw plexus::context_error(__FUNCTION__, get_last_error()); 46 | 47 | std::shared_ptr pkey(PEM_read_bio_PrivateKey(key_bio.get(), NULL, 0, NULL), EVP_PKEY_free); 48 | 49 | if (!pcert || !pkey) 50 | throw plexus::context_error(__FUNCTION__, get_last_error()); 51 | 52 | std::shared_ptr in(BIO_new_mem_buf(msg.c_str(), (int)msg.size()), BIO_free); 53 | 54 | if (!in) 55 | throw plexus::context_error(__FUNCTION__, get_last_error()); 56 | 57 | std::shared_ptr p7(PKCS7_sign(pcert.get(), pkey.get(), NULL, in.get(), flags), PKCS7_free); 58 | 59 | if (!p7) 60 | throw plexus::context_error(__FUNCTION__, get_last_error()); 61 | 62 | std::shared_ptr out(BIO_new(BIO_s_mem()), BIO_free); 63 | if (!out) 64 | throw plexus::context_error(__FUNCTION__, get_last_error()); 65 | 66 | if (!SMIME_write_PKCS7(out.get(), p7.get(), in.get(), flags)) 67 | throw plexus::context_error(__FUNCTION__, get_last_error()); 68 | 69 | char *ptr; 70 | long len = BIO_get_mem_data(out.get(), &ptr); 71 | std::string data(ptr, len); 72 | 73 | return std::regex_replace(data, std::regex("\\n"), "\r\n"); 74 | } 75 | 76 | std::string smime_encrypt(const std::string& msg, const std::string& cert) 77 | { 78 | if (cert.empty()) 79 | throw plexus::context_error(__FUNCTION__, "no certificate"); 80 | 81 | int flags = PKCS7_STREAM; 82 | 83 | std::shared_ptr tbio(BIO_new_file(cert.c_str(), "r"), BIO_free); 84 | 85 | if (!tbio) 86 | throw plexus::context_error(__FUNCTION__, get_last_error()); 87 | 88 | std::shared_ptr pcert(PEM_read_bio_X509(tbio.get(), NULL, 0, NULL), X509_free); 89 | 90 | if (!pcert) 91 | throw plexus::context_error(__FUNCTION__, get_last_error()); 92 | 93 | STACK_OF(X509) *recips = sk_X509_new_null(); 94 | 95 | if (!recips || !sk_X509_push(recips, pcert.get())) 96 | throw plexus::context_error(__FUNCTION__, get_last_error()); 97 | 98 | std::shared_ptr in(BIO_new_mem_buf(msg.c_str(), (int)msg.size()), BIO_free); 99 | 100 | if (!in) 101 | throw plexus::context_error(__FUNCTION__, get_last_error()); 102 | 103 | std::shared_ptr p7(PKCS7_encrypt(recips, in.get(), EVP_des_ede3_cbc(), flags), PKCS7_free); 104 | 105 | if (!p7) 106 | throw plexus::context_error(__FUNCTION__, get_last_error()); 107 | 108 | std::shared_ptr out(BIO_new(BIO_s_mem()), BIO_free); 109 | if (!out) 110 | throw plexus::context_error(__FUNCTION__, get_last_error()); 111 | 112 | if (!SMIME_write_PKCS7(out.get(), p7.get(), in.get(), flags)) 113 | throw plexus::context_error(__FUNCTION__, get_last_error()); 114 | 115 | char *ptr; 116 | long len = BIO_get_mem_data(out.get(), &ptr); 117 | std::string data(ptr, len); 118 | 119 | return std::regex_replace(data, std::regex("\\n"), "\r\n"); 120 | } 121 | 122 | std::string smime_decrypt(const std::string& msg, const std::string& cert, const std::string& key) 123 | { 124 | if (cert.empty()) 125 | throw plexus::context_error(__FUNCTION__, "no certificate"); 126 | if (key.empty()) 127 | throw plexus::context_error(__FUNCTION__, "no private key"); 128 | 129 | std::shared_ptr cert_bio(BIO_new_file(cert.c_str(), "r"), BIO_free); 130 | if (!cert_bio) 131 | throw plexus::context_error(__FUNCTION__, get_last_error()); 132 | 133 | std::shared_ptr pcert(PEM_read_bio_X509(cert_bio.get(), NULL, 0, NULL), X509_free); 134 | 135 | std::shared_ptr key_bio(BIO_new_file(key.c_str(), "r"), BIO_free); 136 | if (!key_bio) 137 | throw plexus::context_error(__FUNCTION__, get_last_error()); 138 | 139 | std::shared_ptr pkey(PEM_read_bio_PrivateKey(key_bio.get(), NULL, 0, NULL), EVP_PKEY_free); 140 | 141 | if (!pcert || !pkey) 142 | throw plexus::context_error(__FUNCTION__, get_last_error()); 143 | 144 | std::string m = std::regex_replace(msg, std::regex("\\r\\n"), "\n"); 145 | std::shared_ptr in(BIO_new_mem_buf(m.c_str(), (int)m.size()), BIO_free); 146 | 147 | if (!in) 148 | throw plexus::context_error(__FUNCTION__, get_last_error()); 149 | 150 | std::shared_ptr p7(SMIME_read_PKCS7(in.get(), NULL), PKCS7_free); 151 | 152 | if (!p7) 153 | throw plexus::context_error(__FUNCTION__, get_last_error()); 154 | 155 | std::shared_ptr out(BIO_new(BIO_s_mem()), BIO_free); 156 | if (!out) 157 | throw plexus::context_error(__FUNCTION__, get_last_error()); 158 | 159 | if (!PKCS7_decrypt(p7.get(), pkey.get(), pcert.get(), out.get(), 0)) 160 | throw plexus::context_error(__FUNCTION__, get_last_error()); 161 | 162 | char *ptr; 163 | long len = BIO_get_mem_data(out.get(), &ptr); 164 | std::string data(ptr, len); 165 | 166 | return data; 167 | } 168 | 169 | std::string smime_verify(const std::string& msg, const std::string& cert, const std::string& ca) 170 | { 171 | if (cert.empty()) 172 | return msg; 173 | 174 | STACK_OF(X509) *certs = sk_X509_new_null(); 175 | if (certs == NULL) 176 | throw plexus::context_error(__FUNCTION__, get_last_error()); 177 | 178 | std::shared_ptr cert_bio(BIO_new_file(cert.c_str(), "r"), BIO_free); 179 | if (!cert_bio) 180 | throw plexus::context_error(__FUNCTION__, get_last_error()); 181 | 182 | std::shared_ptr sign_cert(PEM_read_bio_X509(cert_bio.get(), NULL, 0, NULL), X509_free); 183 | 184 | if (!sign_cert) 185 | throw plexus::context_error(__FUNCTION__, get_last_error()); 186 | 187 | sk_X509_push(certs, sign_cert.get()); 188 | 189 | std::shared_ptr ca_bio; 190 | std::shared_ptr ca_cert; 191 | std::shared_ptr st; 192 | 193 | if (!ca.empty()) 194 | { 195 | ca_bio.reset(BIO_new_file(ca.c_str(), "r"), BIO_free); 196 | if (!ca_bio) 197 | throw plexus::context_error(__FUNCTION__, get_last_error()); 198 | 199 | ca_cert.reset(PEM_read_bio_X509(ca_bio.get(), NULL, 0, NULL), X509_free); 200 | if (!ca_cert) 201 | throw plexus::context_error(__FUNCTION__, get_last_error()); 202 | 203 | st.reset(X509_STORE_new(), X509_STORE_free); 204 | if (st == NULL) 205 | throw plexus::context_error(__FUNCTION__, get_last_error()); 206 | 207 | X509_STORE_set_purpose(st.get(), X509_PURPOSE_ANY); 208 | 209 | if (!X509_STORE_add_cert(st.get(), ca_cert.get())) 210 | throw plexus::context_error(__FUNCTION__, get_last_error()); 211 | } 212 | 213 | std::string m = std::regex_replace(msg, std::regex("\\r\\n"), "\n"); 214 | std::shared_ptr in(BIO_new_mem_buf(m.c_str(), (int)m.size()), BIO_free); 215 | 216 | if (!in) 217 | throw plexus::context_error(__FUNCTION__, get_last_error()); 218 | 219 | BIO* cont = nullptr; 220 | std::shared_ptr p7(SMIME_read_PKCS7(in.get(), &cont), PKCS7_free); 221 | 222 | if (p7 == NULL) 223 | throw plexus::context_error(__FUNCTION__, get_last_error()); 224 | 225 | std::shared_ptr out(BIO_new(BIO_s_mem()), BIO_free); 226 | if (!out) 227 | throw plexus::context_error(__FUNCTION__, get_last_error()); 228 | 229 | if (!PKCS7_verify(p7.get(), certs, st.get(), cont, out.get(), st ? 0 : PKCS7_NOVERIFY)) 230 | throw plexus::context_error(__FUNCTION__, get_last_error()); 231 | 232 | char *ptr; 233 | long len = BIO_get_mem_data(out.get(), &ptr); 234 | std::string data(ptr, len); 235 | 236 | return data; 237 | } 238 | }} 239 | -------------------------------------------------------------------------------- /src/plexus/exec.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Novemus Band. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | */ 10 | 11 | #include 12 | #include 13 | 14 | #ifdef _WIN32 15 | 16 | #include 17 | #include 18 | #include 19 | 20 | namespace plexus { 21 | 22 | struct waiter 23 | { 24 | HANDLE handle; 25 | HANDLE process_handle; 26 | DWORD process_id; 27 | 28 | static void notify(PVOID data, BOOLEAN timeout) 29 | { 30 | waiter* ptr = static_cast(data); 31 | 32 | DWORD code; 33 | if (!GetExitCodeProcess(ptr->process_handle, &code)) 34 | _wrn_ << "can't get child process " << ptr->process_id << " exit code: error=" << GetLastError(); 35 | else 36 | _inf_ << "got child process " << ptr->process_id << " exit code: " << code; 37 | 38 | CloseHandle(ptr->process_handle); 39 | UnregisterWait(ptr->handle); 40 | 41 | delete ptr; 42 | } 43 | }; 44 | 45 | void exec(const std::string& prog, const std::string& args, const std::string& dir, const std::string& log, bool wait) noexcept(false) 46 | { 47 | _dbg_ << "execute: cmd=\"" << prog << "\" args=\"" << args << "\" pwd=\"" << dir << "\" log=\"" << log << "\""; 48 | 49 | std::string cmd = "\"" + prog + "\" " + args; 50 | 51 | STARTUPINFO si; 52 | PROCESS_INFORMATION pi; 53 | 54 | std::memset(&si, 0, sizeof(si)); 55 | si.cb = sizeof(si); 56 | si.dwFlags |= STARTF_USESTDHANDLES; 57 | std::memset(&pi, 0, sizeof(pi)); 58 | 59 | if (!log.empty()) 60 | { 61 | SECURITY_ATTRIBUTES sa; 62 | sa.nLength = sizeof(sa); 63 | sa.lpSecurityDescriptor = NULL; 64 | sa.bInheritHandle = TRUE; 65 | 66 | auto path = std::filesystem::path(log); 67 | if (!dir.empty() && path.is_relative()) 68 | path = std::filesystem::path(dir) / path; 69 | 70 | HANDLE h = CreateFile(path.string().c_str(), 71 | FILE_APPEND_DATA, 72 | FILE_SHARE_WRITE | FILE_SHARE_READ, 73 | &sa, 74 | OPEN_ALWAYS, 75 | FILE_ATTRIBUTE_NORMAL, 76 | NULL); 77 | 78 | if (h == INVALID_HANDLE_VALUE) 79 | throw std::runtime_error(utils::format("CreateFile: error=%d", GetLastError())); 80 | 81 | if(wait) 82 | si.hStdInput = GetStdHandle(STD_INPUT_HANDLE); 83 | 84 | si.hStdOutput = h; 85 | si.hStdError = h; 86 | } 87 | else if (wait) 88 | { 89 | si.hStdInput = GetStdHandle(STD_INPUT_HANDLE); 90 | si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); 91 | si.hStdError = GetStdHandle(STD_ERROR_HANDLE); 92 | } 93 | 94 | if (CreateProcess(prog.c_str(), (char*)cmd.c_str(), 0, 0, true, wait ? 0 : CREATE_NO_WINDOW, 0, dir.empty() ? 0 : dir.c_str(), &si, &pi)) 95 | { 96 | _inf_ << "execute: pid=" << pi.dwProcessId; 97 | 98 | if (!log.empty()) 99 | CloseHandle(si.hStdOutput); 100 | 101 | CloseHandle(pi.hThread); 102 | 103 | if (wait) 104 | { 105 | WaitForSingleObject(pi.hProcess, INFINITE); 106 | 107 | DWORD code = 0; 108 | if (!GetExitCodeProcess(pi.hProcess, &code) || code != 0) 109 | { 110 | CloseHandle(pi.hProcess); 111 | throw std::runtime_error(utils::format("GetExitCodeProcess: error=%d, code=%d", GetLastError(), code)); 112 | } 113 | CloseHandle(pi.hProcess); 114 | } 115 | else 116 | { 117 | waiter* ptr = new waiter { 0, pi.hProcess, pi.dwProcessId }; 118 | if (!RegisterWaitForSingleObject(&ptr->handle, pi.hProcess, &waiter::notify, ptr, INFINITE, WT_EXECUTEONLYONCE)) 119 | { 120 | CloseHandle(pi.hProcess); 121 | throw std::runtime_error(utils::format("RegisterWaitForSingleObject: error=%d", GetLastError())); 122 | } 123 | } 124 | } 125 | else 126 | { 127 | throw std::runtime_error(utils::format("CreateProcess: error=%d", GetLastError())); 128 | } 129 | } 130 | 131 | } 132 | 133 | #else 134 | 135 | #include 136 | #include 137 | #include 138 | #include 139 | #include 140 | #include 141 | 142 | extern char **environ; 143 | 144 | namespace plexus { 145 | 146 | boost::shared_array make_arguments(const std::string& exe, const std::string& args) 147 | { 148 | auto holder = std::make_shared>(); 149 | holder->push_back(boost::replace_all_copy(exe, " ", "\\ ")); 150 | 151 | boost::tokenizer> tokenizer(args.begin(), args.end(), boost::escaped_list_separator("\\", " \t", "'\"")); 152 | auto iter = tokenizer.begin(); 153 | while (iter != tokenizer.end()) 154 | { 155 | if (!iter->empty()) 156 | holder->push_back(iter->c_str()); 157 | ++iter; 158 | } 159 | 160 | boost::shared_array array(new char*[holder->size() + 1], [holder](char** ptr) { delete[] ptr; }); 161 | for (size_t i = 0; i < holder->size(); ++i) 162 | array[i] = holder->at(i).data(); 163 | array[holder->size()] = 0; 164 | 165 | return array; 166 | } 167 | 168 | void exec(const std::string& prog, const std::string& args, const std::string& dir, const std::string& log, bool wait) noexcept(false) 169 | { 170 | _dbg_ << "execute: cmd=\"" << prog << "\" args=\"" << args << "\" pwd=\"" << dir << "\" log=\"" << log << "\""; 171 | 172 | static std::once_flag s_flag; 173 | std::call_once(s_flag, []() 174 | { 175 | signal(SIGCHLD, [](int num) 176 | { 177 | pid_t pid = waitpid(-1, &num, WNOHANG); 178 | while (pid > 0) 179 | { 180 | _inf_ << "got child process " << pid << " exit code: " << num; 181 | pid = waitpid(-1, &num, WNOHANG); 182 | } 183 | }); 184 | }); 185 | 186 | auto argv = make_arguments(prog, args); 187 | 188 | posix_spawn_file_actions_t action; 189 | int status = posix_spawn_file_actions_init(&action); 190 | if (status) 191 | throw std::runtime_error(utils::format("posix_spawn_file_actions_init: error=%d", status)); 192 | 193 | if (!dir.empty()) 194 | { 195 | std::string pwd = boost::replace_all_copy(dir, " ", "\\ "); 196 | status = posix_spawn_file_actions_addchdir_np(&action, pwd.c_str()); 197 | if (status) 198 | throw std::runtime_error(utils::format("posix_spawn_file_actions_addchdir_np: error=%d", status)); 199 | } 200 | 201 | if (!wait) 202 | { 203 | status = posix_spawn_file_actions_addclose(&action, 0); 204 | if (status) 205 | throw std::runtime_error(utils::format("posix_spawn_file_actions_addclose stdin: error=%d", status)); 206 | 207 | status = posix_spawn_file_actions_addclose(&action, 1); 208 | if (status) 209 | throw std::runtime_error(utils::format("posix_spawn_file_actions_addclose stdout: error=%d", status)); 210 | 211 | status = posix_spawn_file_actions_addclose(&action, 2); 212 | if (status) 213 | throw std::runtime_error(utils::format("posix_spawn_file_actions_addclose stderr: error=%d", status)); 214 | } 215 | 216 | if (!log.empty()) 217 | { 218 | std::string out = boost::replace_all_copy(log, " ", "\\ "); 219 | status = posix_spawn_file_actions_addopen(&action, 1, out.c_str(), O_CREAT | O_APPEND | O_WRONLY, 0644); 220 | if (status) 221 | throw std::runtime_error(utils::format("posix_spawn_file_actions_addopen: error=%d", status)); 222 | 223 | status = posix_spawn_file_actions_adddup2(&action, 1, 2); 224 | if (status) 225 | throw std::runtime_error(utils::format("posix_spawn_file_actions_adddup2: error=%d", status)); 226 | } 227 | 228 | posix_spawnattr_t attr; 229 | status = posix_spawnattr_init(&attr); 230 | if (status) 231 | throw std::runtime_error(utils::format("posix_spawnattr_init: error=%d", status)); 232 | 233 | status = posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSID); 234 | if (status) 235 | throw std::runtime_error(utils::format("posix_spawnattr_setflags: error=%d", status)); 236 | 237 | pid_t pid; 238 | status = posix_spawnp(&pid, argv[0], &action, &attr, argv.get(), environ); 239 | if (status) 240 | throw std::runtime_error(utils::format("posix_spawn: error=%d", status)); 241 | 242 | _inf_ << "execute: pid=" << pid; 243 | 244 | status = posix_spawn_file_actions_destroy(&action); 245 | if (status) 246 | throw std::runtime_error(utils::format("posix_spawn_file_actions_destroy: error=%d", status)); 247 | 248 | status = posix_spawnattr_destroy(&attr); 249 | if (status) 250 | throw std::runtime_error(utils::format("posix_spawnattr_destroy: error=%d", status)); 251 | 252 | if (wait) 253 | { 254 | int code = 0; 255 | if (waitpid(pid, &code, 0) == -1) 256 | { 257 | if (errno != ECHILD) 258 | throw std::runtime_error(utils::format("waitpid: error=%d", errno)); 259 | } 260 | else if (code) 261 | throw std::runtime_error(utils::format("execute: error=%d", code)); 262 | } 263 | } 264 | 265 | } 266 | 267 | #endif 268 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the 13 | copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other 16 | entities that control, are controlled by, or are under common control 17 | with that entity. For the purposes of this definition, "control" means 18 | (i) the power, direct or indirect, to cause the direction or management 19 | of such entity, whether by contract or otherwise, or (ii) ownership of 20 | fifty percent (50%) or more of the outstanding shares, or (iii) 21 | beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity exercising 24 | permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation source, 28 | and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical transformation 31 | or translation of a Source form, including but not limited to compiled 32 | object code, generated documentation, and conversions to other media types. 33 | 34 | "Work" shall mean the work of authorship, whether in Source or Object form, 35 | made available under the License, as indicated by a copyright notice that 36 | is included in or attached to the work (an example is provided in the 37 | Appendix below). 38 | 39 | "Derivative Works" shall mean any work, whether in Source or Object form, 40 | that is based on (or derived from) the Work and for which the editorial 41 | revisions, annotations, elaborations, or other modifications represent, 42 | as a whole, an original work of authorship. For the purposes of this License, 43 | Derivative Works shall not include works that remain separable from, or 44 | merely link (or bind by name) to the interfaces of, the Work and Derivative 45 | Works thereof. 46 | 47 | "Contribution" shall mean any work of authorship, including the original 48 | version of the Work and any modifications or additions to that Work or 49 | Derivative Works thereof, that is intentionally submitted to Licensor 50 | for inclusion in the Work by the copyright owner or by an individual 51 | or Legal Entity authorized to submit on behalf of the copyright owner. 52 | For the purposes of this definition, "submitted" means any form of 53 | electronic, verbal, or written communication sent to the Licensor or 54 | its representatives, including but not limited to communication on 55 | electronic mailing lists, source code control systems, and issue tracking 56 | systems that are managed by, or on behalf of, the Licensor for the purpose 57 | of discussing and improving the Work, but excluding communication that is 58 | conspicuously marked or otherwise designated in writing by the copyright 59 | owner as "Not a Contribution." 60 | 61 | "Contributor" shall mean Licensor and any individual or Legal Entity on 62 | behalf of whom a Contribution has been received by Licensor and subsequently 63 | incorporated within the Work. 64 | 65 | 2. Grant of Copyright License. Subject to the terms and conditions of this 66 | License, each Contributor hereby grants to You a perpetual, worldwide, 67 | non-exclusive, no-charge, royalty-free, irrevocable copyright license 68 | to reproduce, prepare Derivative Works of, publicly display, publicly 69 | perform, sublicense, and distribute the Work and such Derivative Works 70 | in Source or Object form. 71 | 72 | 3. Grant of Patent License. Subject to the terms and conditions of this 73 | License, each Contributor hereby grants to You a perpetual, worldwide, 74 | non-exclusive, no-charge, royalty-free, irrevocable (except as stated in 75 | this section) patent license to make, have made, use, offer to sell, sell, 76 | import, and otherwise transfer the Work, where such license applies only 77 | to those patent claims licensable by such Contributor that are necessarily 78 | infringed by their Contribution(s) alone or by combination of their 79 | Contribution(s) with the Work to which such Contribution(s) was submitted. 80 | If You institute patent litigation against any entity (including a 81 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 82 | Contribution incorporated within the Work constitutes direct or 83 | contributory patent infringement, then any patent licenses granted to 84 | You under this License for that Work shall terminate as of the date such 85 | litigation is filed. 86 | 87 | 4. Redistribution. You may reproduce and distribute copies of the Work or 88 | Derivative Works thereof in any medium, with or without modifications, and 89 | in Source or Object form, provided that You meet the following conditions: 90 | 91 | (a) You must give any other recipients of the Work or Derivative Works a 92 | copy of this License; and 93 | (b) You must cause any modified files to carry prominent notices stating 94 | that You changed the files; and 95 | (c) You must retain, in the Source form of any Derivative Works that You 96 | distribute, all copyright, patent, trademark, and attribution notices 97 | from the Source form of the Work, excluding those notices that do not 98 | pertain to any part of the Derivative Works; and 99 | (d) If the Work includes a "NOTICE" text file as part of its distribution, 100 | then any Derivative Works that You distribute must include a readable 101 | copy of the attribution notices contained within such NOTICE file, 102 | excluding those notices that do not pertain to any part of the Derivative 103 | Works, in at least one of the following places: within a NOTICE text file 104 | distributed as part of the Derivative Works; within the Source form or 105 | documentation, if provided along with the Derivative Works; or, within 106 | a display generated by the Derivative Works, if and wherever such 107 | third-party notices normally appear. The contents of the NOTICE file are 108 | for informational purposes only and do not modify the License. You may 109 | add Your own attribution notices within Derivative Works that You 110 | distribute, alongside or as an addendum to the NOTICE text from the Work, 111 | provided that such additional attribution notices cannot be construed 112 | as modifying the License. 113 | 114 | You may add Your own copyright statement to Your modifications and may 115 | provide additional or different license terms and conditions for use, 116 | reproduction, or distribution of Your modifications, or for any such 117 | Derivative Works as a whole, provided Your use, reproduction, and 118 | distribution of the Work otherwise complies with the conditions stated 119 | in this License. 120 | 121 | 5. Submission of Contributions. Unless You explicitly state otherwise, any 122 | Contribution intentionally submitted for inclusion in the Work by You to 123 | the Licensor shall be under the terms and conditions of this License, 124 | without any additional terms or conditions. Notwithstanding the above, 125 | nothing herein shall supersede or modify the terms of any separate license 126 | agreement you may have executed with Licensor regarding such Contributions. 127 | 128 | 6. Trademarks. This License does not grant permission to use the trade names, 129 | trademarks, service marks, or product names of the Licensor, except as required 130 | for reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in 134 | writing, Licensor provides the Work (and each Contributor provides its 135 | Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 136 | ANY KIND, either express or implied, including, without limitation, any 137 | warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or 138 | FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining 139 | the appropriateness of using or redistributing the Work and assume any risks 140 | associated with Your exercise of permissions under this License. 141 | 142 | 8. Limitation of Liability. In no event and under no legal theory, whether in 143 | tort (including negligence), contract, or otherwise, unless required by 144 | applicable law (such as deliberate and grossly negligent acts) or agreed to 145 | in writing, shall any Contributor be liable to You for damages, including 146 | any direct, indirect, special, incidental, or consequential damages of any 147 | character arising as a result of this License or out of the use or inability 148 | to use the Work (including but not limited to damages for loss of goodwill, 149 | work stoppage, computer failure or malfunction, or any and all other commercial 150 | damages or losses), even if such Contributor has been advised of the possibility 151 | of such damages. 152 | 153 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or 154 | Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance 155 | of support, warranty, indemnity, or other liability obligations and/or rights 156 | consistent with this License. However, in accepting such obligations, You may act 157 | only on Your own behalf and on Your sole responsibility, not on behalf of any 158 | other Contributor, and only if You agree to indemnify, defend, and hold each 159 | Contributor harmless for any liability incurred by, or claims asserted against, 160 | such Contributor by reason of your accepting any such warranty or additional 161 | liability. 162 | 163 | END OF TERMS AND CONDITIONS 164 | -------------------------------------------------------------------------------- /src/plexus/plexus.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Novemus Band. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | */ 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace plexus { 17 | 18 | std::ostream& operator<<(std::ostream& stream, const reference& value) 19 | { 20 | if (stream.rdbuf()) 21 | return stream << value.endpoint << "/" << value.puzzle; 22 | return stream; 23 | } 24 | 25 | std::ostream& operator<<(std::ostream& stream, const identity& value) 26 | { 27 | if (stream.rdbuf()) 28 | return stream << value.owner << "/" << value.pin; 29 | return stream; 30 | } 31 | 32 | std::istream& operator>>(std::istream& in, reference& value) 33 | { 34 | std::string str; 35 | in >> str; 36 | 37 | std::smatch match; 38 | if (std::regex_match(str, match, std::regex("^([^/]+)/([^/]+)$"))) 39 | { 40 | value.endpoint = plexus::utils::parse_endpoint(match[1].str(), ""); 41 | value.puzzle = boost::lexical_cast(match[2].str()); 42 | return in; 43 | } 44 | 45 | throw boost::bad_lexical_cast(); 46 | } 47 | 48 | std::istream& operator>>(std::istream& in, identity& value) 49 | { 50 | std::string str; 51 | in >> str; 52 | 53 | std::smatch match; 54 | if (std::regex_match(str, match, std::regex("^([^/]*)/([^/]*)$"))) 55 | { 56 | value.owner = match[1].str(); 57 | value.pin = match[2].str(); 58 | return in; 59 | } 60 | 61 | throw boost::bad_lexical_cast(); 62 | } 63 | 64 | void explore_network(boost::asio::io_context& io, const udp::endpoint& bind, const udp::endpoint& stun, const std::function& handler, const std::function& failure) noexcept(true) 65 | { 66 | boost::asio::spawn(io, [&io, bind, stun, handler, failure](boost::asio::yield_context yield) 67 | { 68 | _inf_ << "exploring network by stun " << stun << " from " << bind; 69 | 70 | try 71 | { 72 | auto client = plexus::create_stun_client(io, stun, bind); 73 | auto hole = client->explore_network(yield); 74 | 75 | handler(hole); 76 | } 77 | catch (const std::exception& e) 78 | { 79 | _err_ << "exploring network by stun " << stun << " from " << bind << " error: " << e.what(); 80 | 81 | if (failure) 82 | failure(e.what()); 83 | } 84 | }, boost::asio::detached); 85 | } 86 | 87 | void spawn_accept(boost::asio::io_context& io, const options& config, const identity& host, const identity& peer, const connector& connect, const fallback& failure) noexcept(true) 88 | { 89 | auto handler = [&io, config, connect, failure](boost::asio::yield_context yield, std::shared_ptr pipe) 90 | { 91 | auto peer = pipe->peer(); 92 | auto host = pipe->host(); 93 | 94 | _inf_ << "accepting " << peer << " -> " << host << ":" << config.app; 95 | 96 | try 97 | { 98 | auto binder = plexus::create_stun_binder(io, config.stun, config.bind, config.hops); 99 | 100 | auto hole = binder->explore_network(yield); 101 | if (hole.traits.mapping != traverse::independent) 102 | throw plexus::context_error(__FUNCTION__, "bad network"); 103 | 104 | reference faraway = pipe->pull_request(yield); 105 | reference gateway = {hole.outer_endpoint, plexus::utils::random()}; 106 | pipe->push_response(yield, gateway); 107 | 108 | auto pin = binder->await_peer(yield, faraway.endpoint, faraway.puzzle ^ gateway.puzzle); 109 | 110 | connect(host, peer, hole.inner_endpoint, gateway, faraway); 111 | } 112 | catch (const std::exception& e) 113 | { 114 | _err_ << "accepting " << peer << " -> " << host << ":" << config.app << " error: " << e.what(); 115 | 116 | if (failure) 117 | failure(host, peer, e.what()); 118 | } 119 | }; 120 | 121 | config.mediator.index() == 0 122 | ? spawn_accept(io, context(config.app, config.repo, std::get(config.mediator)), host, peer, handler) 123 | : spawn_accept(io, context(config.app, config.repo, std::get(config.mediator)), host, peer, handler); 124 | } 125 | 126 | void spawn_invite(boost::asio::io_context& io, const options& config, const identity& host, const identity& peer, const connector& connect, const fallback& failure) noexcept(true) 127 | { 128 | auto handler = [&io, config, connect, failure](boost::asio::yield_context yield, std::shared_ptr pipe) 129 | { 130 | auto peer = pipe->peer(); 131 | auto host = pipe->host(); 132 | 133 | _inf_ << "inviting " << host << " -> " << peer << ":" << config.app; 134 | 135 | try 136 | { 137 | auto binder = plexus::create_stun_binder(io, config.stun, config.bind, config.hops); 138 | 139 | auto hole = binder->explore_network(yield); 140 | if (hole.traits.mapping != traverse::independent) 141 | throw plexus::context_error(__FUNCTION__, "bad network"); 142 | 143 | plexus::reference gateway = { hole.outer_endpoint, plexus::utils::random() }; 144 | pipe->push_request(yield, gateway); 145 | plexus::reference faraway = pipe->pull_response(yield); 146 | 147 | auto pin = binder->reach_peer(yield, faraway.endpoint, faraway.puzzle ^ gateway.puzzle); 148 | 149 | connect(host, peer, hole.inner_endpoint, gateway, faraway); 150 | } 151 | catch (const std::exception& e) 152 | { 153 | _err_ << "inviting " << host << " -> " << peer << ":" << config.app << " error: " << e.what(); 154 | 155 | if (failure) 156 | failure(pipe->host(), pipe->peer(), e.what()); 157 | } 158 | }; 159 | 160 | config.mediator.index() == 0 161 | ? spawn_invite(io, context(config.app, config.repo, std::get(config.mediator)), host, peer, handler) 162 | : spawn_invite(io, context(config.app, config.repo, std::get(config.mediator)), host, peer, handler); 163 | } 164 | 165 | void spawn_accept(boost::asio::io_context& io, const options& config, const identity& host, const identity& peer, const collector& collect, const fallback& failure) noexcept(true) 166 | { 167 | spawn_accept(io, config, host, peer, [&io, collect, failure](const identity& host, const identity& peer, const udp::endpoint& bind, const reference& gateway, const reference& faraway) 168 | { 169 | _inf_ << "accepting " << faraway.endpoint << " -> " << bind; 170 | 171 | auto socket = std::make_shared(io, faraway.puzzle ^ gateway.puzzle); 172 | boost::system::error_code ec; 173 | socket->open(bind, ec); 174 | 175 | if (ec) 176 | { 177 | _err_ << "can't open " << bind << " socket: " << ec.message(); 178 | 179 | if (failure) 180 | failure(host, peer, ec.message()); 181 | return; 182 | } 183 | 184 | socket->async_accept(faraway.endpoint, [socket, faraway, bind, host, peer, collect, failure](const boost::system::error_code& ec) 185 | { 186 | if (ec) 187 | { 188 | _err_ << "accepting " << faraway.endpoint << " -> " << bind << " error: " << ec.message(); 189 | 190 | if (failure) 191 | failure(host, peer, ec.message()); 192 | return; 193 | } 194 | 195 | collect(host, peer, std::move(*socket)); 196 | }); 197 | }, failure); 198 | } 199 | 200 | void spawn_invite(boost::asio::io_context& io, const options& config, const identity& host, const identity& peer, const collector& collect, const fallback& failure) noexcept(true) 201 | { 202 | spawn_invite(io, config, host, peer, [&io, collect, failure](const identity& host, const identity& peer, const udp::endpoint& bind, const reference& gateway, const reference& faraway) 203 | { 204 | _inf_ << "connecting " << bind << " -> " << faraway.endpoint; 205 | 206 | auto socket = std::make_shared(io, faraway.puzzle ^ gateway.puzzle); 207 | boost::system::error_code ec; 208 | socket->open(bind, ec); 209 | 210 | if (ec) 211 | { 212 | _err_ << "can't open " << bind << " socket: " << ec.message(); 213 | 214 | if (failure) 215 | failure(host, peer, ec.message()); 216 | return; 217 | } 218 | 219 | socket->async_connect(faraway.endpoint, [socket, faraway, bind, host, peer, collect, failure](const boost::system::error_code& ec) 220 | { 221 | if (ec) 222 | { 223 | _err_ << "connecting " << bind << " -> " << faraway.endpoint << " error: " << ec.message(); 224 | 225 | if (failure) 226 | failure(host, peer, ec.message()); 227 | return; 228 | } 229 | 230 | collect(host, peer, std::move(*socket)); 231 | }); 232 | }, failure); 233 | } 234 | 235 | void forward_advent(boost::asio::io_context& io, const rendezvous& mediator, const std::string& app, const std::string& repo, const identity& host, const identity& peer, const observer& handler, const fallback& failure) noexcept(true) 236 | { 237 | _inf_ << "forward advent " << peer << " -> " << host << ":" << app; 238 | 239 | mediator.index() == 0 240 | ? forward_advent(io, context(app, repo, std::get(mediator)), host, peer, handler, failure) 241 | : forward_advent(io, context(app, repo, std::get(mediator)), host, peer, handler, failure); 242 | } 243 | 244 | void receive_advent(boost::asio::io_context& io, const rendezvous& mediator, const std::string& app, const std::string& repo, const identity& host, const identity& peer, const observer& handler, const fallback& failure) noexcept(true) 245 | { 246 | _inf_ << "receive advent " << host << " -> " << peer << ":" << app; 247 | 248 | mediator.index() == 0 249 | ? receive_advent(io, context(app, repo, std::get(mediator)), host, peer, handler, failure) 250 | : receive_advent(io, context(app, repo, std::get(mediator)), host, peer, handler, failure); 251 | } 252 | 253 | } 254 | -------------------------------------------------------------------------------- /src/plexus/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Novemus Band. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | */ 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | template 19 | struct endpoint : public boost::asio::ip::basic_endpoint 20 | { 21 | endpoint() {} 22 | endpoint(const boost::asio::ip::basic_endpoint& ep) : boost::asio::ip::basic_endpoint(ep) {} 23 | }; 24 | 25 | constexpr char stun_server_default_port[] = "3478"; 26 | constexpr char stun_client_default_port[] = "0"; 27 | constexpr char smtp_server_default_port[] = "smtps"; 28 | constexpr char imap_client_default_port[] = "imaps"; 29 | 30 | using stun_server_endpoint = endpoint; 31 | using stun_client_endpoint = endpoint; 32 | using smtp_server_endpoint = endpoint; 33 | using imap_server_endpoint = endpoint; 34 | 35 | template 36 | void validate(boost::any& result, const std::vector& values, endpoint*, int) 37 | { 38 | boost::program_options::validators::check_first_occurrence(result); 39 | const std::string& url = boost::program_options::validators::get_single_string(values); 40 | 41 | try 42 | { 43 | result = endpoint( 44 | plexus::utils::parse_endpoint>(url, service) 45 | ); 46 | } 47 | catch(const boost::system::system_error&) 48 | { 49 | boost::throw_exception(boost::program_options::error("can't resolve " + url)); 50 | } 51 | } 52 | 53 | int main(int argc, char** argv) 54 | { 55 | boost::program_options::options_description desc("plexus options"); 56 | desc.add_options() 57 | ("help", "produce help message") 58 | ("accept", boost::program_options::bool_switch(), "accept or invite peer to initiate connection") 59 | ("app-id", boost::program_options::value()->required(), "identifier of the application") 60 | ("app-repo", boost::program_options::value()->default_value(""), "path to the application repository") 61 | ("host-info", boost::program_options::value()->default_value(plexus::identity()), "identifier of the host: ") 62 | ("peer-info", boost::program_options::value()->default_value(plexus::identity()), "identifier of the peer: ") 63 | ("stun-server", boost::program_options::value()->required(), "endpoint of the public stun server") 64 | ("stun-client", boost::program_options::value()->default_value(stun_client_endpoint()), "endpoint to bind the stun client and the application being launched") 65 | ("dht-bootstrap", boost::program_options::value()->default_value("bootstrap.jami.net:4222"), "url of the bootstrap DHT service") 66 | ("dht-port", boost::program_options::value()->default_value(4222), "local port to bind the DHT node") 67 | ("dht-network", boost::program_options::value()->default_value(0), "DHT network id") 68 | ("email-smtps", boost::program_options::value(), "smtps server used for the email rendezvous") 69 | ("email-imaps", boost::program_options::value(), "imaps server used for the email rendezvous") 70 | ("email-login", boost::program_options::value(), "login of the email account") 71 | ("email-password", boost::program_options::value(), "password of the email account") 72 | ("email-cert", boost::program_options::value()->default_value(""), "path to the X509 certificate of the email client") 73 | ("email-key", boost::program_options::value()->default_value(""), "path to the Private Key of the email client") 74 | ("email-ca", boost::program_options::value()->default_value(""), "path to the email Certification Authority") 75 | ("punch-hops", boost::program_options::value()->default_value(7), "time-to-live parameter for the punch packet") 76 | ("exec-command", boost::program_options::value()->required(), "command executed after punching the NAT") 77 | ("exec-args", boost::program_options::value()->default_value("%innerip% %innerport% %outerip% %outerport% %peerip% %peerport%"), "arguments for the command executed after punching the NAT, allowed wildcards: %innerip%, %innerport%, %outerip%, %outerport%, %peerip%, %peerport%, %secret%, %hostpin%, %peerpin%, %hostemail%, %peeremail%") 78 | ("exec-pwd", boost::program_options::value()->default_value(""), "working directory for executable, the above wildcards are allowed") 79 | ("exec-log", boost::program_options::value()->default_value(""), "exec log file, the above wildcards are allowed") 80 | ("log-level", boost::program_options::value()->default_value(wormhole::log::info), "log level: ") 81 | ("log-file", boost::program_options::value()->default_value(""), "plexus log file, allowed %p (process id) wildcard") 82 | ("config", boost::program_options::value(), "path to the INI-like configuration file"); 83 | 84 | boost::program_options::variables_map vm; 85 | try 86 | { 87 | boost::program_options::store(boost::program_options::parse_command_line(argc, argv, desc), vm); 88 | if(vm.count("help")) 89 | { 90 | std::cout << desc << std::endl; 91 | return -1; 92 | } 93 | 94 | auto count = vm.count("email-smtps") + vm.count("email-imaps") + vm.count("email-login") + vm.count("email-password"); 95 | if(count > 0 && count != 4) 96 | { 97 | std::cout << "to use email service for a rendezvous, specify at least the 'email-smtps', 'email-imaps', 'email-login' and 'email-password' arguments" << std::endl; 98 | return -1; 99 | } 100 | 101 | if(vm.count("config")) 102 | boost::program_options::store(boost::program_options::parse_config_file(vm["config"].as().c_str(), desc), vm); 103 | 104 | boost::program_options::notify(vm); 105 | } 106 | catch (const std::exception& e) 107 | { 108 | std::cerr << e.what() << std::endl; 109 | std::cout << desc; 110 | return -1; 111 | } 112 | 113 | try 114 | { 115 | wormhole::log::set(vm["log-level"].as(), vm["log-file"].as()); 116 | 117 | auto launch = [&](const plexus::identity& host, const plexus::identity& peer, const boost::asio::ip::udp::endpoint& bind, const plexus::reference& gateway, const plexus::reference& faraway) 118 | { 119 | auto format = [&](const std::string& line) 120 | { 121 | std::string res = line; 122 | std::vector> replaces = { 123 | { std::regex("%innerip%"), bind.address().to_string() }, 124 | { std::regex("%innerport%"), std::to_string(bind.port()) }, 125 | { std::regex("%outerip%"), gateway.endpoint.address().to_string()}, 126 | { std::regex("%outerport%"), std::to_string(gateway.endpoint.port()) }, 127 | { std::regex("%peerip%"), faraway.endpoint.address().to_string() }, 128 | { std::regex("%peerport%"), std::to_string(faraway.endpoint.port()) }, 129 | { std::regex("%secret%"), std::to_string(gateway.puzzle ^ faraway.puzzle) }, 130 | { std::regex("%hostpin%"), host.pin }, 131 | { std::regex("%peerpin%"), peer.pin }, 132 | { std::regex("%hostemail%"), host.owner }, 133 | { std::regex("%peeremail%"), peer.owner } 134 | }; 135 | 136 | for (const auto& item : replaces) 137 | { 138 | res = std::regex_replace(res, item.first, item.second); 139 | } 140 | 141 | return res; 142 | }; 143 | 144 | plexus::exec( 145 | vm["exec-command"].as(), 146 | format(vm["exec-args"].as()), 147 | format(vm["exec-pwd"].as()), 148 | format(vm["exec-log"].as()) 149 | ); 150 | }; 151 | 152 | plexus::options config = { 153 | vm["app-id"].as(), 154 | vm["app-repo"].as(), 155 | vm["stun-server"].as(), 156 | vm["stun-client"].as(), 157 | vm["punch-hops"].as(), 158 | vm.count("email-smtps") 159 | ? plexus::rendezvous { 160 | plexus::emailer { 161 | vm["email-smtps"].as(), 162 | vm["email-imaps"].as(), 163 | vm["email-login"].as(), 164 | vm["email-password"].as(), 165 | vm["email-cert"].as(), 166 | vm["email-key"].as(), 167 | vm["email-ca"].as() 168 | }} 169 | : plexus::rendezvous { 170 | plexus::dhtnode { 171 | vm["dht-bootstrap"].as(), 172 | vm["dht-port"].as(), 173 | vm["dht-network"].as() 174 | }} 175 | }; 176 | 177 | boost::asio::io_context io; 178 | vm["accept"].as() 179 | ? plexus::spawn_accept(io, config, vm["host-info"].as(), vm["peer-info"].as(), launch) 180 | : plexus::spawn_invite(io, config, vm["host-info"].as(), vm["peer-info"].as(), launch); 181 | io.run(); 182 | } 183 | catch(const std::exception& e) 184 | { 185 | _ftl_ << e.what(); 186 | return -1; 187 | } 188 | 189 | return 0; 190 | } 191 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | message("***** plexus ***** ") 2 | 3 | ########## setup ########## 4 | 5 | cmake_minimum_required(VERSION 3.21) 6 | 7 | project(plexus VERSION 3.2.0 DESCRIPTION "The tool for connecting UDP applications via NAT" LANGUAGES CXX) 8 | 9 | option(PLEXUS_USE_SHARED_LIB "Build shared library" ${BUILD_SHARED_LIBS}) 10 | option(PLEXUS_SKIP_TEST_RULES "Don't generate test rules" ON) 11 | option(PLEXUS_SKIP_INSTALL_RULES "Don't generate install rules" OFF) 12 | option(PLEXUS_SKIP_PACKAGE_RULES "Don't generate package rules" ON) 13 | 14 | if (PLEXUS_USE_SHARED_LIB) 15 | set(BUILD_SHARED_LIBS ON) 16 | endif() 17 | 18 | ########## dependencies ########## 19 | 20 | if (NOT MSVC AND NOT BUILD_SHARED_LIBS) 21 | set(Boost_USE_STATIC_LIBS ON) 22 | endif() 23 | 24 | if(NOT PLEXUS_SKIP_TEST_RULES) 25 | find_package(Boost 1.80 CONFIG REQUIRED COMPONENTS system program_options coroutine unit_test_framework) 26 | else() 27 | find_package(Boost 1.80 CONFIG REQUIRED COMPONENTS system program_options coroutine) 28 | endif() 29 | 30 | message("* Boost Version: ${Boost_VERSION}") 31 | message("* Boost Include Dirs: ${Boost_INCLUDE_DIRS}") 32 | message("* Boost Library Dirs: ${Boost_LIBRARY_DIRS}") 33 | message("* Boost Libraries: ${Boost_LIBRARIES}") 34 | 35 | find_package(OpenSSL REQUIRED) 36 | 37 | message("* OpenSSL Version: ${OPENSSL_VERSION}") 38 | message("* OpenSSL Include Dir: ${OPENSSL_INCLUDE_DIR}") 39 | message("* OpenSSL Libraries: ${OPENSSL_LIBRARIES}") 40 | 41 | find_package(tubus 1.2.3 REQUIRED COMPONENTS libtubus) 42 | 43 | message("* tubus Version: ${tubus_VERSION}") 44 | message("* tubus Include Dirs: ${tubus_INCLUDE_DIRS}") 45 | message("* tubus Library: ${tubus_LIBRARY}") 46 | 47 | find_package(wormhole 1.2.4 REQUIRED COMPONENTS libwormhole) 48 | 49 | message("* wormhole Version: ${wormhole_VERSION}") 50 | message("* wormhole Include Dirs: ${wormhole_INCLUDE_DIRS}") 51 | message("* wormhole Library: ${wormhole_LIBRARY}") 52 | 53 | find_package(opendht 3.2 REQUIRED) 54 | find_package(fmt REQUIRED) 55 | 56 | find_package(msgpackc-cxx QUIET CONFIG NAMES msgpackc-cxx msgpack) 57 | if(msgpackc-cxx_FOUND) 58 | add_library(msgpack-cxx ALIAS msgpackc-cxx) 59 | else() 60 | find_package(msgpack-cxx CONFIG REQUIRED) 61 | endif() 62 | 63 | find_package(PkgConfig REQUIRED) 64 | 65 | pkg_search_module(GnuTLS REQUIRED IMPORTED_TARGET gnutls) 66 | 67 | message("* GnuTLS Include Dir: ${GnuTLS_INCLUDEDIR}") 68 | message("* GnuTLS Lib Dir: ${GnuTLS_LIBDIR}") 69 | 70 | pkg_search_module(Nettle REQUIRED IMPORTED_TARGET nettle) 71 | 72 | message("* Nettle Include Dir: ${Nettle_INCLUDEDIR}") 73 | message("* Nettle Lib Dir: ${Nettle_LIBDIR}") 74 | 75 | pkg_search_module(argon2 REQUIRED IMPORTED_TARGET libargon2) 76 | 77 | message("* argon2 Include Dir: ${argon2_INCLUDEDIR}") 78 | message("* argon2 Lib Dir: ${argon2_LIBDIR}") 79 | 80 | pkg_search_module(Jsoncpp REQUIRED IMPORTED_TARGET jsoncpp) 81 | 82 | message("* Jsoncpp Include Dir: ${Jsoncpp_INCLUDEDIR}") 83 | message("* Jsoncpp Lib Dir: ${Jsoncpp_LIBDIR}") 84 | 85 | ########## build ########## 86 | 87 | include(GenerateExportHeader) 88 | 89 | set(BINPLEXUS ${PROJECT_NAME}) 90 | set(LIBPLEXUS lib${PROJECT_NAME}) 91 | 92 | add_executable(${BINPLEXUS}) 93 | add_executable(${PROJECT_NAME}::${BINPLEXUS} ALIAS ${BINPLEXUS}) 94 | 95 | add_library(${LIBPLEXUS}) 96 | add_library(${PROJECT_NAME}::${LIBPLEXUS} ALIAS ${LIBPLEXUS}) 97 | 98 | generate_export_header(${LIBPLEXUS} EXPORT_FILE_NAME "export_header/plexus/export.h") 99 | 100 | set(SOURCES "${CMAKE_CURRENT_BINARY_DIR}/export_header/plexus/export.h" 101 | src/plexus/utils.h 102 | src/plexus/socket.h 103 | src/plexus/network.h 104 | src/plexus/features.h 105 | src/plexus/plexus.h 106 | src/plexus/utils.cpp 107 | src/plexus/exec.cpp 108 | src/plexus/smime.cpp 109 | src/plexus/udp.cpp 110 | src/plexus/tcp.cpp 111 | src/plexus/email.cpp 112 | src/plexus/dht.cpp 113 | src/plexus/stun.cpp 114 | src/plexus/binder.cpp 115 | src/plexus/plexus.cpp 116 | ) 117 | 118 | set(HEADERS "${CMAKE_CURRENT_BINARY_DIR}/export_header/plexus/export.h" src/plexus/plexus.h) 119 | 120 | if(MSVC) 121 | set(CMAKE_STATIC_LIBRARY_PREFIX "lib") 122 | set(CMAKE_SHARED_LIBRARY_PREFIX "lib") 123 | set(CMAKE_STATIC_LIBRARY_SUFFIX "-static.lib") 124 | add_definitions(-D_WIN32_WINNT=0x0601) 125 | endif() 126 | 127 | if(NOT DEFINED CMAKE_BUILD_TYPE AND NOT DEFINED CMAKE_CONFIGURATION_TYPES) 128 | set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) 129 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") 130 | endif() 131 | 132 | if(NOT DEFINED CMAKE_CXX_VISIBILITY_PRESET) 133 | set(CMAKE_CXX_VISIBILITY_PRESET hidden) 134 | endif() 135 | if(NOT DEFINED CMAKE_VISIBILITY_INLINES_HIDDEN) 136 | set(CMAKE_VISIBILITY_INLINES_HIDDEN ON) 137 | endif() 138 | 139 | target_sources(${LIBPLEXUS} PRIVATE ${SOURCES}) 140 | target_sources(${BINPLEXUS} PRIVATE $ src/plexus/main.cpp) 141 | 142 | target_link_libraries(${LIBPLEXUS} PUBLIC tubus::libtubus PRIVATE wormhole::libwormhole Boost::coroutine OpenSSL::SSL OpenSSL::Crypto opendht fmt::fmt msgpack-cxx PkgConfig::GnuTLS PkgConfig::argon2 PkgConfig::Nettle PkgConfig::Jsoncpp "$<$:Iphlpapi>") 143 | target_link_libraries(${BINPLEXUS} PRIVATE tubus::libtubus wormhole::libwormhole Boost::coroutine Boost::program_options OpenSSL::SSL OpenSSL::Crypto opendht fmt::fmt msgpack-cxx PkgConfig::GnuTLS PkgConfig::argon2 PkgConfig::Nettle PkgConfig::Jsoncpp "$<$:Iphlpapi>") 144 | 145 | target_compile_features(${LIBPLEXUS} PRIVATE cxx_std_17) 146 | target_compile_features(${BINPLEXUS} PRIVATE cxx_std_17) 147 | 148 | target_include_directories(${BINPLEXUS} PRIVATE 149 | "$" 150 | "$" 151 | "${opendht_INCLUDEDIR}") 152 | 153 | target_include_directories(${LIBPLEXUS} PUBLIC 154 | "$" 155 | "$" 156 | PRIVATE "${opendht_INCLUDEDIR}") 157 | 158 | set_target_properties(${BINPLEXUS} PROPERTIES DEBUG_POSTFIX "d" COMPILE_FLAGS -DLIBPLEXUS_STATIC_DEFINE) 159 | set_target_properties(${BINPLEXUS} PROPERTIES CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN ON) 160 | set_target_properties(${LIBPLEXUS} PROPERTIES DEBUG_POSTFIX "d" OUTPUT_NAME ${PROJECT_NAME} IMPORT_PREFIX "lib") 161 | set_target_properties(${LIBPLEXUS} PROPERTIES SOVERSION ${PROJECT_VERSION_MAJOR} VERSION ${PROJECT_VERSION}) 162 | set_target_properties(${LIBPLEXUS} PROPERTIES CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN ON) 163 | 164 | ########## install ########## 165 | 166 | if(NOT PLEXUS_SKIP_INSTALL_RULES AND NOT CMAKE_SKIP_INSTALL_RULES) 167 | 168 | include(GNUInstallDirs) 169 | include(CMakePackageConfigHelpers) 170 | 171 | set(PLEXUS_INSTALL_CMAKEDIR "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}") 172 | 173 | configure_package_config_file(cmake/plexus-config.cmake.in plexus-config.cmake INSTALL_DESTINATION "${PLEXUS_INSTALL_CMAKEDIR}") 174 | 175 | write_basic_package_version_file(plexus-config-version.cmake COMPATIBILITY SameMajorVersion) 176 | 177 | install(TARGETS ${BINPLEXUS} RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" COMPONENT binary_files) 178 | 179 | install(TARGETS ${LIBPLEXUS} EXPORT plexus_export 180 | RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" COMPONENT runtime_files 181 | LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" COMPONENT library_files 182 | ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" COMPONENT archive_files 183 | INCLUDES DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") 184 | 185 | set(TARGET_FILE plexus-shared-targets.cmake) 186 | 187 | if(NOT BUILD_SHARED_LIBS) 188 | set(TARGET_FILE plexus-static-targets.cmake) 189 | endif() 190 | 191 | install(EXPORT plexus_export COMPONENT cmake_files 192 | FILE ${TARGET_FILE} 193 | DESTINATION "${PLEXUS_INSTALL_CMAKEDIR}" 194 | NAMESPACE plexus::) 195 | 196 | install(FILES 197 | "${CMAKE_CURRENT_BINARY_DIR}/plexus-config.cmake" 198 | "${CMAKE_CURRENT_BINARY_DIR}/plexus-config-version.cmake" 199 | COMPONENT cmake_files 200 | DESTINATION "${PLEXUS_INSTALL_CMAKEDIR}") 201 | 202 | install(FILES ${HEADERS} DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}" COMPONENT header_files) 203 | 204 | if(MSVC) 205 | install(FILES "$" COMPONENT pdb_files 206 | CONFIGURATIONS Debug RelWithDebInfo 207 | DESTINATION "${CMAKE_INSTALL_LIBDIR}" 208 | OPTIONAL) 209 | if(BUILD_SHARED_LIBS) 210 | install(FILES "$" COMPONENT pdb_files 211 | CONFIGURATIONS Debug RelWithDebInfo 212 | DESTINATION "${CMAKE_INSTALL_LIBDIR}" 213 | OPTIONAL) 214 | else() 215 | install(FILES "$/$$-static.pdb" COMPONENT pdb_files 216 | CONFIGURATIONS Debug RelWithDebInfo 217 | DESTINATION "${CMAKE_INSTALL_LIBDIR}" 218 | OPTIONAL) 219 | endif() 220 | endif() 221 | endif() 222 | 223 | ########## tests ########## 224 | 225 | if(NOT PLEXUS_SKIP_TEST_RULES) 226 | set(PLEXUS_TEST plexus_ut) 227 | add_executable(${PLEXUS_TEST} tests/utils_tests.cpp 228 | tests/ssl_tests.cpp 229 | tests/udp_tests.cpp 230 | tests/tcp_tests.cpp 231 | tests/smime_tests.cpp 232 | tests/exec_tests.cpp 233 | tests/plexus_tests.cpp) 234 | target_link_libraries(${PLEXUS_TEST} PRIVATE $ tubus::libtubus wormhole::libwormhole Boost::coroutine Boost::program_options Boost::unit_test_framework OpenSSL::SSL OpenSSL::Crypto opendht fmt::fmt msgpack-cxx PkgConfig::GnuTLS PkgConfig::argon2 PkgConfig::Nettle PkgConfig::Jsoncpp "$<$:Iphlpapi>") 235 | 236 | target_include_directories(${PLEXUS_TEST} PRIVATE 237 | "$" 238 | "$" 239 | "${opendht_INCLUDEDIR}") 240 | 241 | target_compile_features(${PLEXUS_TEST} PRIVATE cxx_std_17) 242 | set_target_properties(${PLEXUS_TEST} PROPERTIES DEBUG_POSTFIX "d" COMPILE_FLAGS -DLIBPLEXUS_STATIC_DEFINE) 243 | 244 | enable_testing() 245 | add_test(NAME ${PLEXUS_TEST} COMMAND ${PLEXUS_TEST} WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests") 246 | endif() 247 | 248 | ########## package ########## 249 | 250 | if(NOT PLEXUS_SKIP_PACKAGE_RULES AND NOT PLEXUS_SKIP_INSTALL_RULES AND NOT CMAKE_SKIP_INSTALL_RULES) 251 | if(CMAKE_HOST_SYSTEM_NAME MATCHES "Linux") 252 | cmake_host_system_information(RESULT DISTR QUERY DISTRIB_ID) 253 | if(DISTR MATCHES "(debian)|(ubuntu)|(kali)|(astra)") 254 | add_custom_command(DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/debian/changelog" 255 | COMMAND gzip -cn9 "${CMAKE_CURRENT_SOURCE_DIR}/debian/changelog" > "${CMAKE_CURRENT_BINARY_DIR}/changelog.gz" 256 | OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/changelog.gz") 257 | add_custom_target(changelog ALL DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/changelog.gz") 258 | 259 | set(DEBIAN_DOC_FILES 260 | "${CMAKE_CURRENT_BINARY_DIR}/changelog.gz" 261 | "${CMAKE_CURRENT_SOURCE_DIR}/debian/copyright" 262 | "${CMAKE_CURRENT_SOURCE_DIR}/debian/README.Debian" 263 | ) 264 | install(FILES ${DEBIAN_DOC_FILES} DESTINATION "${CMAKE_INSTALL_DATADIR}/doc/${BINPLEXUS}" COMPONENT bin_deb_files) 265 | if(BUILD_SHARED_LIBS) 266 | install(FILES ${DEBIAN_DOC_FILES} DESTINATION "${CMAKE_INSTALL_DATADIR}/doc/${LIBPLEXUS}" COMPONENT lib_deb_files) 267 | else() 268 | install(FILES ${DEBIAN_DOC_FILES} DESTINATION "${CMAKE_INSTALL_DATADIR}/doc/${LIBPLEXUS}-dev" COMPONENT dev_deb_files) 269 | endif() 270 | endif() 271 | endif() 272 | 273 | if(NOT BUILD_SHARED_LIBS) 274 | set(DEVEL_DOC_FILES 275 | "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE.txt" 276 | "${CMAKE_CURRENT_SOURCE_DIR}/README.md" 277 | "${CMAKE_CURRENT_SOURCE_DIR}/CHANGELOG.md" 278 | ) 279 | install(FILES ${DEVEL_DOC_FILES} DESTINATION "${CMAKE_INSTALL_DATADIR}/doc/${LIBPLEXUS}-dev" COMPONENT dev_doc_files) 280 | endif() 281 | 282 | include(CPack) 283 | endif() 284 | -------------------------------------------------------------------------------- /tests/plexus_tests.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Novemus Band. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | */ 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | namespace 19 | { 20 | struct context 21 | { 22 | plexus::options conf; 23 | plexus::identity host; 24 | plexus::identity peer; 25 | }; 26 | 27 | void init_test_repo(const plexus::options& conf, const plexus::identity& host, const plexus::identity& peer) 28 | { 29 | auto host_dir = conf.repo + "/" + host.owner + "/" + host.pin; 30 | auto peer_dir = conf.repo + "/" + peer.owner + "/" + peer.pin; 31 | 32 | std::filesystem::remove_all(host_dir); 33 | std::filesystem::create_directories(host_dir); 34 | 35 | std::filesystem::create_symlink(std::filesystem::canonical("certs/server.crt"), host_dir + "/cert.crt"); 36 | std::filesystem::create_symlink(std::filesystem::canonical("certs/server.key"), host_dir + "/private.key"); 37 | std::filesystem::create_symlink(std::filesystem::canonical("certs/ca.crt"), host_dir + "/ca.crt"); 38 | 39 | std::filesystem::remove_all(peer_dir); 40 | std::filesystem::create_directories(peer_dir); 41 | 42 | std::filesystem::create_symlink(std::filesystem::canonical("certs/client.crt"), peer_dir + "/cert.crt"); 43 | std::filesystem::create_symlink(std::filesystem::canonical("certs/client.key"), peer_dir + "/private.key"); 44 | std::filesystem::create_symlink(std::filesystem::canonical("certs/ca.crt"), peer_dir + "/ca.crt"); 45 | } 46 | 47 | context with_emailer = []() 48 | { 49 | plexus::options conf; 50 | plexus::identity host; 51 | plexus::identity peer; 52 | 53 | auto path = plexus::utils::getenv("PLEXUS_EMAILER_CONTEXT", ""); 54 | if (std::filesystem::exists(path)) 55 | { 56 | std::ifstream in(path); 57 | 58 | std::string smtp_addr, smtp_port, imap_addr, imap_port, login, password, stun_addr, stun_port, punch_hops; 59 | 60 | in >> smtp_addr >> smtp_port >> imap_addr >> imap_port >> login >> password >> stun_addr >> stun_port >> punch_hops; 61 | 62 | conf.app = "plexus_email_app"; 63 | conf.repo = std::filesystem::temp_directory_path().generic_u8string() + "/plexus_email_app"; 64 | conf.stun = plexus::utils::parse_endpoint(stun_addr, stun_port); 65 | conf.hops = boost::lexical_cast(punch_hops); 66 | conf.mediator = plexus::emailer { 67 | plexus::utils::parse_endpoint(smtp_addr, smtp_port), 68 | plexus::utils::parse_endpoint(imap_addr, imap_port), 69 | login, 70 | password 71 | }; 72 | 73 | host.owner = login; 74 | host.pin = "host"; 75 | peer.owner = login; 76 | peer.pin = "peer"; 77 | 78 | init_test_repo(conf, host, peer); 79 | } 80 | 81 | return context{ conf, host, peer }; 82 | }(); 83 | 84 | context with_dhtnode = []() 85 | { 86 | plexus::options conf; 87 | plexus::identity host; 88 | plexus::identity peer; 89 | 90 | auto path = plexus::utils::getenv("PLEXUS_DHTNODE_CONTEXT", ""); 91 | if (std::filesystem::exists(path)) 92 | { 93 | std::ifstream in(path); 94 | 95 | std::string bootstrap, node_port, network, stun_addr, stun_port, punch_hops; 96 | 97 | in >> bootstrap >> node_port >> network >> stun_addr >> stun_port >> punch_hops; 98 | 99 | conf.app = "plexus_dht_app"; 100 | conf.repo = std::filesystem::temp_directory_path().generic_u8string() + "/plexus_dht_app"; 101 | conf.stun = plexus::utils::parse_endpoint(stun_addr, stun_port); 102 | conf.hops = boost::lexical_cast(punch_hops); 103 | conf.mediator = plexus::dhtnode { 104 | bootstrap, 105 | boost::lexical_cast(node_port), 106 | boost::lexical_cast(network) 107 | }; 108 | 109 | host.owner = "test@plexus"; 110 | host.pin = "host"; 111 | peer.owner = "test@plexus"; 112 | peer.pin = "peer"; 113 | 114 | init_test_repo(conf, host, peer); 115 | } 116 | 117 | return context{ conf, host, peer }; 118 | }(); 119 | 120 | boost::test_tools::assertion_result is_emailer_context_defined(boost::unit_test::test_unit_id) 121 | { 122 | return std::getenv("PLEXUS_EMAILER_CONTEXT") != nullptr; 123 | } 124 | 125 | boost::test_tools::assertion_result is_dhtnode_context_defined(boost::unit_test::test_unit_id) 126 | { 127 | return std::getenv("PLEXUS_DHTNODE_CONTEXT") != nullptr; 128 | } 129 | } 130 | 131 | void make_advent_test(const plexus::rendezvous& receiver, const plexus::rendezvous& forwarder, const std::string& app, const std::string& repo, const plexus::identity& host, const plexus::identity& peer) 132 | { 133 | auto rcv = std::async(std::launch::async, [&]() 134 | { 135 | boost::asio::io_context io; 136 | 137 | plexus::receive_advent(io, receiver, app, repo, host, peer, 138 | [&](const plexus::identity& h, const plexus::identity& p) 139 | { 140 | BOOST_CHECK_EQUAL(host.owner, h.owner); 141 | BOOST_CHECK_EQUAL(host.pin, h.pin); 142 | BOOST_CHECK_EQUAL(peer.owner, p.owner); 143 | BOOST_CHECK_EQUAL(peer.pin, p.pin); 144 | io.stop(); 145 | }, 146 | [&](const plexus::identity& h, const plexus::identity& p, const std::string& error) 147 | { 148 | BOOST_CHECK_EQUAL(host.owner, h.owner); 149 | BOOST_CHECK_EQUAL(host.pin, h.pin); 150 | BOOST_CHECK_EQUAL(peer.owner, p.owner); 151 | BOOST_CHECK_EQUAL(peer.pin, p.pin); 152 | BOOST_VERIFY_MSG(false, error.c_str()); 153 | io.stop(); 154 | }); 155 | 156 | io.run(); 157 | }); 158 | 159 | auto fwd = std::async(std::launch::async, [&]() 160 | { 161 | boost::asio::io_context io; 162 | 163 | plexus::forward_advent(io, forwarder, app, repo, peer, host, 164 | [&](const plexus::identity& h, const plexus::identity& p) 165 | { 166 | BOOST_CHECK_EQUAL(host.owner, p.owner); 167 | BOOST_CHECK_EQUAL(host.pin, p.pin); 168 | BOOST_CHECK_EQUAL(peer.owner, h.owner); 169 | BOOST_CHECK_EQUAL(peer.pin, h.pin); 170 | io.stop(); 171 | }, 172 | [&](const plexus::identity& h, const plexus::identity& p, const std::string& error) 173 | { 174 | BOOST_CHECK_EQUAL(host.owner, p.owner); 175 | BOOST_CHECK_EQUAL(host.pin, p.pin); 176 | BOOST_CHECK_EQUAL(peer.owner, h.owner); 177 | BOOST_CHECK_EQUAL(peer.pin, h.pin); 178 | BOOST_VERIFY_MSG(false, error.c_str()); 179 | io.stop(); 180 | }); 181 | 182 | io.run(); 183 | }); 184 | 185 | rcv.wait(); 186 | fwd.wait(); 187 | } 188 | 189 | void make_rendezvous_test(const context& info) 190 | { 191 | auto acc = std::async(std::launch::async, [&]() 192 | { 193 | boost::asio::io_context io; 194 | 195 | plexus::spawn_accept(io, info.conf, info.host, info.peer, 196 | [&](const plexus::identity& host, const plexus::identity& peer, const plexus::udp::endpoint& bind, const plexus::reference& gateway, const plexus::reference& faraway) 197 | { 198 | BOOST_CHECK_EQUAL(info.host.owner, host.owner); 199 | BOOST_CHECK_EQUAL(info.host.pin, host.pin); 200 | BOOST_CHECK_EQUAL(info.peer.owner, peer.owner); 201 | BOOST_CHECK_EQUAL(info.peer.pin, peer.pin); 202 | BOOST_CHECK_NE(bind.port(), 0); 203 | BOOST_CHECK_NE(gateway.endpoint.address().is_unspecified(), true); 204 | BOOST_CHECK_NE(gateway.endpoint.port(), 0); 205 | BOOST_CHECK_NE(gateway.puzzle, 0); 206 | BOOST_CHECK_NE(faraway.endpoint.address().is_unspecified(), true); 207 | BOOST_CHECK_NE(faraway.endpoint.port(), 0); 208 | BOOST_CHECK_NE(faraway.puzzle, 0); 209 | io.stop(); 210 | }, 211 | [&](const plexus::identity& host, const plexus::identity& peer, const std::string& error) 212 | { 213 | BOOST_CHECK_EQUAL(info.host.owner, host.owner); 214 | BOOST_CHECK_EQUAL(info.host.pin, host.pin); 215 | BOOST_CHECK_EQUAL(info.peer.owner, peer.owner); 216 | BOOST_CHECK_EQUAL(info.peer.pin, peer.pin); 217 | BOOST_VERIFY_MSG(false, error.c_str()); 218 | io.stop(); 219 | }); 220 | 221 | io.run(); 222 | }); 223 | 224 | auto inv = std::async(std::launch::async, [&]() 225 | { 226 | boost::asio::io_context io; 227 | 228 | plexus::spawn_invite(io, info.conf, info.peer, info.host, 229 | [&](const plexus::identity& host, const plexus::identity& peer, const plexus::udp::endpoint& bind, const plexus::reference& gateway, const plexus::reference& faraway) 230 | { 231 | BOOST_CHECK_EQUAL(info.host.owner, peer.owner); 232 | BOOST_CHECK_EQUAL(info.host.pin, peer.pin); 233 | BOOST_CHECK_EQUAL(info.peer.owner, host.owner); 234 | BOOST_CHECK_EQUAL(info.peer.pin, host.pin); 235 | BOOST_CHECK_NE(bind.port(), 0); 236 | BOOST_CHECK_NE(gateway.endpoint.address().is_unspecified(), true); 237 | BOOST_CHECK_NE(gateway.endpoint.port(), 0); 238 | BOOST_CHECK_NE(gateway.puzzle, 0); 239 | BOOST_CHECK_NE(faraway.endpoint.address().is_unspecified(), true); 240 | BOOST_CHECK_NE(faraway.endpoint.port(), 0); 241 | BOOST_CHECK_NE(faraway.puzzle, 0); 242 | io.stop(); 243 | }, 244 | [&](const plexus::identity& host, const plexus::identity& peer, const std::string& error) 245 | { 246 | BOOST_CHECK_EQUAL(info.host.owner, peer.owner); 247 | BOOST_CHECK_EQUAL(info.host.pin, peer.pin); 248 | BOOST_CHECK_EQUAL(info.peer.owner, host.owner); 249 | BOOST_CHECK_EQUAL(info.peer.pin, host.pin); 250 | BOOST_VERIFY_MSG(false, error.c_str()); 251 | io.stop(); 252 | }); 253 | 254 | io.run(); 255 | }); 256 | 257 | acc.wait(); 258 | inv.wait(); 259 | } 260 | 261 | void make_streaming_test(const context& info) 262 | { 263 | static const boost::system::error_code NONE_ERROR; 264 | 265 | std::string wb("hello plexus"); 266 | std::string rb(wb.size(), '\0'); 267 | 268 | auto acc = std::async(std::launch::async, [&]() 269 | { 270 | boost::asio::io_context io; 271 | 272 | plexus::spawn_accept(io, info.conf, info.host, info.peer, 273 | [&](const plexus::identity& host, const plexus::identity& peer, tubus::socket&& socket) 274 | { 275 | auto stream = std::make_shared(std::move(socket)); 276 | 277 | BOOST_CHECK_EQUAL(info.host.owner, host.owner); 278 | BOOST_CHECK_EQUAL(info.host.pin, host.pin); 279 | BOOST_CHECK_EQUAL(info.peer.owner, peer.owner); 280 | BOOST_CHECK_EQUAL(info.peer.pin, peer.pin); 281 | 282 | boost::asio::async_read(*stream, boost::asio::buffer(rb), [&, stream](const boost::system::error_code& error, size_t size) 283 | { 284 | BOOST_CHECK_EQUAL(error, NONE_ERROR); 285 | BOOST_CHECK_EQUAL(wb, rb.substr(0, size)); 286 | stream->async_shutdown([&, stream](const boost::system::error_code& error) 287 | { 288 | BOOST_CHECK_EQUAL(error, NONE_ERROR); 289 | io.stop(); 290 | }); 291 | }); 292 | }, 293 | [&](const plexus::identity& host, const plexus::identity& peer, const std::string& error) 294 | { 295 | BOOST_CHECK_EQUAL(info.host.owner, host.owner); 296 | BOOST_CHECK_EQUAL(info.host.pin, host.pin); 297 | BOOST_CHECK_EQUAL(info.peer.owner, peer.owner); 298 | BOOST_CHECK_EQUAL(info.peer.pin, peer.pin); 299 | BOOST_VERIFY_MSG(false, error.c_str()); 300 | io.stop(); 301 | }); 302 | 303 | io.run(); 304 | }); 305 | 306 | auto inv = std::async(std::launch::async, [&]() 307 | { 308 | boost::asio::io_context io; 309 | 310 | plexus::spawn_invite(io, info.conf, info.peer, info.host, 311 | [&](const plexus::identity& host, const plexus::identity& peer, tubus::socket&& socket) 312 | { 313 | auto stream = std::make_shared(std::move(socket)); 314 | 315 | BOOST_CHECK_EQUAL(info.host.owner, peer.owner); 316 | BOOST_CHECK_EQUAL(info.host.pin, peer.pin); 317 | BOOST_CHECK_EQUAL(info.peer.owner, host.owner); 318 | BOOST_CHECK_EQUAL(info.peer.pin, host.pin); 319 | 320 | boost::asio::async_write(*stream, boost::asio::buffer(wb), [&, stream](const boost::system::error_code& error, size_t size) mutable 321 | { 322 | BOOST_CHECK_EQUAL(error, NONE_ERROR); 323 | BOOST_CHECK_EQUAL(wb.size(), size); 324 | stream->async_shutdown([&, stream](const boost::system::error_code& error) 325 | { 326 | BOOST_CHECK_EQUAL(error, NONE_ERROR); 327 | io.stop(); 328 | }); 329 | }); 330 | }, 331 | [&](const plexus::identity& host, const plexus::identity& peer, const std::string& error) 332 | { 333 | BOOST_CHECK_EQUAL(info.host.owner, peer.owner); 334 | BOOST_CHECK_EQUAL(info.host.pin, peer.pin); 335 | BOOST_CHECK_EQUAL(info.peer.owner, host.owner); 336 | BOOST_CHECK_EQUAL(info.peer.pin, host.pin); 337 | BOOST_VERIFY_MSG(false, error.c_str()); 338 | io.stop(); 339 | }); 340 | 341 | io.run(); 342 | }); 343 | 344 | acc.wait(); 345 | inv.wait(); 346 | } 347 | 348 | BOOST_AUTO_TEST_CASE(plexus_email_rendezvous, *boost::unit_test::precondition(is_emailer_context_defined)) 349 | { 350 | BOOST_TEST_MESSAGE("testing plexus email rendezvous..."); 351 | make_rendezvous_test(with_emailer); 352 | } 353 | 354 | BOOST_AUTO_TEST_CASE(plexus_dht_rendezvous, *boost::unit_test::precondition(is_dhtnode_context_defined)) 355 | { 356 | BOOST_TEST_MESSAGE("testing plexus dht rendezvous..."); 357 | make_rendezvous_test(with_dhtnode); 358 | } 359 | 360 | BOOST_AUTO_TEST_CASE(plexus_email_advent, *boost::unit_test::precondition(is_emailer_context_defined)) 361 | { 362 | BOOST_TEST_MESSAGE("testing plexus email advent..."); 363 | make_advent_test(with_emailer.conf.mediator, with_emailer.conf.mediator, with_emailer.conf.app, with_emailer.conf.repo, with_emailer.host, with_emailer.peer); 364 | } 365 | 366 | BOOST_AUTO_TEST_CASE(plexus_dht_advent, *boost::unit_test::precondition(is_dhtnode_context_defined)) 367 | { 368 | BOOST_TEST_MESSAGE("testing plexus dht advent..."); 369 | make_advent_test(with_dhtnode.conf.mediator, with_dhtnode.conf.mediator, with_dhtnode.conf.app, with_dhtnode.conf.repo, with_dhtnode.host, with_dhtnode.peer); 370 | } 371 | 372 | BOOST_AUTO_TEST_CASE(plexus_streaming_with_emailer, *boost::unit_test::precondition(is_emailer_context_defined)) 373 | { 374 | BOOST_TEST_MESSAGE("testing plexus streaming with email rendezvous..."); 375 | make_streaming_test(with_emailer); 376 | } 377 | 378 | BOOST_AUTO_TEST_CASE(plexus_streaming_with_dhtnode, *boost::unit_test::precondition(is_dhtnode_context_defined)) 379 | { 380 | BOOST_TEST_MESSAGE("testing plexus streaming with dht rendezvous..."); 381 | make_streaming_test(with_dhtnode); 382 | } 383 | -------------------------------------------------------------------------------- /src/plexus/stun.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Novemus Band. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | */ 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #ifndef _WIN32 20 | #include 21 | #include 22 | #include 23 | #else 24 | #include 25 | #include 26 | #include 27 | #endif 28 | 29 | /* 30 | * Procedure of polling the STUN server to test network traverse 31 | * 32 | * SA - source ip address of the stun server 33 | * CA - canged ip address of the stun server 34 | * SP - source port of the stun server 35 | * CP - canged port of the stun server 36 | * MA - mapped address 37 | * MP - mapped port 38 | * AF - state of the address change flag 39 | * PF - state of the port change flag 40 | * 41 | * ***** NAT ***** 42 | * 43 | * NAT_TEST: SA, SP, AF=0, PF=0 44 | * Acquire mapped endpoint from source address and source port of the stun server. 45 | * If mapped endpoint is equal to the local endpoint, then there is no NAT, 46 | * otherwise check if the mapping preserves port and go to the MAPPING_TEST_1 47 | * 48 | * ***** MAPPING ***** 49 | * 50 | * MAPPING_TEST_1: CA, CP, AF=0, PF=0 51 | * Acquire mapped endpoint from changed address and changed port of the stun server. 52 | * If mapped endpoint is equal to the NAT_TEST endpoint, then there is the independent mapping, 53 | * otherwise check if the port is variable and go to the MAPPING_TEST_2 test 54 | * MAPPING_TEST_2: CA, SP, AF=0, PF=0 55 | * Acquire mapped endpoint from changed address and source port of the stun server. 56 | * if mapped endpoint is equal to the NAT_TEST endpoint, then there is the port dependent mapping, 57 | * else if mapped endpoint is equal to the MAPPING_TEST_1 endpoint, then there is the address dependent mapping, 58 | * otherwise there is the address and port dependent mapping. Check if the address is variable. 59 | * 60 | * ***** FILERING ***** 61 | * 62 | * FILERING_TEST_1: SA, SP, AF=1, PF=1 63 | * Tell the stun server to reply from changed address and changed port. 64 | * If response will be received, then there is the endpoint independent filtering, 65 | * otherwise go to the FILERING_TEST_2 test 66 | * FILERING_TEST_2: SA, SP, AF=1, PF=0 67 | * Tell the stun server to reply from changed address and source port. 68 | * If response will be received, then there is the port dependent filtering, 69 | * otherwise go to the FILERING_TEST_3 test 70 | * FILERING_TEST_3: SA, SP, AF=0, PF=1 71 | * Tell the stun server to reply from source address and changed port. 72 | * If response will be received, then there is the address dependent filtering, 73 | * otherwise there is the address and port dependent filtering 74 | * 75 | * ***** HAIRPIN ***** 76 | * 77 | * HAIRPIN_TEST: MA, MP, AF=0, PF=0 78 | * Send request to the mapped endpoint. 79 | * If response will be received, then there is a hairpin. 80 | * 81 | */ 82 | 83 | namespace plexus { namespace stun { 84 | 85 | std::string to_string(const traverse::binding& value) 86 | { 87 | switch (value) 88 | { 89 | case traverse::port_dependent: 90 | return "port dependent"; 91 | case traverse::address_dependent: 92 | return "address dependent"; 93 | case traverse::address_and_port_dependent: 94 | return "address and port dependent"; 95 | default: 96 | return "independent"; 97 | } 98 | return ""; 99 | } 100 | 101 | typedef std::array transaction_id; 102 | 103 | namespace msg 104 | { 105 | const size_t min_size = 20; 106 | const size_t max_size = 548; 107 | const uint16_t binding_request = 0x0001; 108 | const uint16_t binding_response = 0x0101; 109 | const uint16_t binding_error_response = 0x0111; 110 | } 111 | 112 | namespace attr 113 | { 114 | const uint16_t mapped_address = 0x0001; 115 | const uint16_t change_request = 0x0003; 116 | const uint16_t source_address = 0x0004; 117 | const uint16_t changed_address = 0x0005; 118 | const uint16_t error_code = 0x0009; 119 | const uint16_t unknown_attributes = 0x000a; 120 | const uint16_t reflected_from = 0x000b; 121 | } 122 | 123 | namespace flag 124 | { 125 | const uint8_t ip_v4 = 0x01; 126 | const uint8_t ip_v6 = 0x02; 127 | const uint8_t change_address = 0x04; 128 | const uint8_t change_port = 0x02; 129 | } 130 | 131 | inline uint16_t read_short(const uint8_t* array, size_t offset = 0) 132 | { 133 | return ntohs(*(uint16_t*)(array + offset)); 134 | } 135 | 136 | inline uint8_t high_byte(uint16_t value) 137 | { 138 | return uint8_t(value >> 8) & 0xff; 139 | } 140 | 141 | inline uint8_t low_byte(uint16_t value) 142 | { 143 | return uint8_t(value); 144 | } 145 | 146 | class message : public tubus::mutable_buffer 147 | { 148 | const uint8_t* fetch_attribute_place(uint16_t type) const 149 | { 150 | static const size_t TYPE_LENGTH_PART_SIZE = 4; 151 | 152 | const uint8_t* ptr = (uint8_t*)data() + 20; 153 | const uint8_t* end = (uint8_t*)data() + size(); 154 | 155 | while (ptr + TYPE_LENGTH_PART_SIZE < end) 156 | { 157 | uint16_t attribute = read_short(ptr, 0); 158 | uint16_t length = read_short(ptr, 2); 159 | 160 | if (attribute == type) 161 | { 162 | if (ptr + length + TYPE_LENGTH_PART_SIZE > end) 163 | throw plexus::context_error(__FUNCTION__, "wrong attribute data"); 164 | 165 | return ptr; 166 | } 167 | 168 | ptr += length + TYPE_LENGTH_PART_SIZE; 169 | } 170 | 171 | return 0; 172 | } 173 | 174 | boost::asio::ip::udp::endpoint fetch_endpoint(uint16_t kind) const 175 | { 176 | const uint8_t* ptr = fetch_attribute_place(kind); 177 | if (ptr) 178 | { 179 | uint16_t length = read_short(ptr, 2); 180 | 181 | if (ptr[5] == flag::ip_v4) 182 | { 183 | if (length != 8u) 184 | throw plexus::context_error(__FUNCTION__, "wrong endpoint data"); 185 | 186 | return boost::asio::ip::udp::endpoint( 187 | boost::asio::ip::make_address(utils::format("%d.%d.%d.%d", ptr[8], ptr[9], ptr[10], ptr[11])), 188 | read_short(ptr, 6) 189 | ); 190 | } 191 | else if (ptr[5] == flag::ip_v6) 192 | { 193 | if (length != 20u) 194 | throw plexus::context_error(__FUNCTION__, "wrong endpoint data"); 195 | 196 | return boost::asio::ip::udp::endpoint( 197 | boost::asio::ip::make_address(utils::format("%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x", ptr[8], ptr[9], ptr[10], ptr[11], ptr[12], ptr[13], ptr[14], ptr[15], ptr[16], ptr[17], ptr[18], ptr[19], ptr[20], ptr[21], ptr[22], ptr[23])), 198 | read_short(ptr, 6) 199 | ); 200 | } 201 | } 202 | 203 | if (type() == msg::binding_response) 204 | throw plexus::context_error(__FUNCTION__, utils::format("attribute %d not found", kind)); 205 | 206 | return boost::asio::ip::udp::endpoint(); 207 | } 208 | 209 | public: 210 | 211 | message() : mutable_buffer(msg::max_size) 212 | { 213 | } 214 | 215 | message(uint8_t flags) : mutable_buffer(std::vector{ 216 | 0x00, 0x01, 0x00, 0x08, 217 | utils::random(), utils::random(), utils::random(), utils::random(), 218 | utils::random(), utils::random(), utils::random(), utils::random(), 219 | utils::random(), utils::random(), utils::random(), utils::random(), 220 | utils::random(), utils::random(), utils::random(), utils::random(), 221 | 0x00, 0x03, 0x00, 0x04, 0x00, 0x00, 0x00, flags 222 | }) 223 | { 224 | } 225 | 226 | transaction_id transaction() const 227 | { 228 | return { 229 | get(4), get(5), get(6), get(7), 230 | get(8), get(9), get(10), get(11), 231 | get(12), get(13), get(14), get(15), 232 | get(16), get(17), get(18), get(19) 233 | }; 234 | } 235 | 236 | uint16_t type() const 237 | { 238 | return read_short((uint8_t*)data()); 239 | } 240 | 241 | uint16_t size() const 242 | { 243 | return 20u + read_short((uint8_t*)data(), 2); 244 | } 245 | 246 | std::string error() const 247 | { 248 | if (type() == msg::binding_error_response) 249 | { 250 | const uint8_t* ptr = fetch_attribute_place(attr::error_code); 251 | if (ptr) 252 | { 253 | uint16_t length = read_short(ptr, 2); 254 | return std::string((const char*)ptr + 8, length - 8); 255 | } 256 | throw plexus::context_error(__FUNCTION__, "error code attribute not found"); 257 | } 258 | return std::string(); 259 | } 260 | 261 | boost::asio::ip::udp::endpoint source_endpoint() const 262 | { 263 | return fetch_endpoint(attr::source_address); 264 | } 265 | 266 | boost::asio::ip::udp::endpoint changed_endpoint() const 267 | { 268 | return fetch_endpoint(attr::changed_address); 269 | } 270 | 271 | boost::asio::ip::udp::endpoint mapped_endpoint() const 272 | { 273 | return fetch_endpoint(attr::mapped_address); 274 | } 275 | }; 276 | 277 | class client_impl : public stun_client 278 | { 279 | boost::asio::io_context& m_io; 280 | boost::asio::ip::udp::endpoint m_stun; 281 | boost::asio::ip::udp::endpoint m_bind; 282 | 283 | public: 284 | 285 | client_impl(boost::asio::io_context& io, const boost::asio::ip::udp::endpoint& stun, const boost::asio::ip::udp::endpoint& bind) 286 | : m_io(io) 287 | , m_stun(stun) 288 | , m_bind(bind) 289 | {} 290 | 291 | static message exec_binding(std::shared_ptr udp, boost::asio::yield_context yield, const boost::asio::ip::udp::endpoint& to, const boost::asio::ip::udp::endpoint& from, const message& recv = message(0), int64_t deadline = 4600) 292 | { 293 | auto timer = [start = boost::posix_time::microsec_clock::universal_time()]() 294 | { 295 | return boost::posix_time::microsec_clock::universal_time() - start; 296 | }; 297 | 298 | message resp; 299 | 300 | int64_t timeout = 200; 301 | while (timer().total_milliseconds() < deadline) 302 | { 303 | udp->send_to(recv, to, yield, timeout); 304 | 305 | try 306 | { 307 | resp.truncate(udp->receive_from(resp, from, yield, timeout)); 308 | 309 | if (timer().total_milliseconds() >= deadline) 310 | throw plexus::timeout_error(__FUNCTION__); 311 | else if (recv.transaction() != resp.transaction()) 312 | continue; 313 | 314 | switch (resp.type()) 315 | { 316 | case msg::binding_response: 317 | { 318 | auto me = resp.mapped_endpoint(); 319 | auto se = resp.source_endpoint(); 320 | auto ce = resp.changed_endpoint(); 321 | 322 | _trc_ << "mapped_endpoint=" << me 323 | << " source_endpoint=" << se 324 | << " changed_endpoint=" << ce; 325 | break; 326 | } 327 | case msg::binding_request: 328 | break; 329 | case msg::binding_error_response: 330 | throw plexus::context_error(__FUNCTION__, resp.error()); 331 | default: 332 | throw plexus::context_error(__FUNCTION__, "server responded with unexpected message type"); 333 | } 334 | 335 | return resp; 336 | } 337 | catch(const boost::system::system_error& ex) 338 | { 339 | if (ex.code() != boost::asio::error::operation_aborted) 340 | throw plexus::context_error(__FUNCTION__, ex.code()); 341 | 342 | _trc_ << ex.what(); 343 | 344 | timeout = std::min(1600, timeout * 2); 345 | } 346 | } 347 | 348 | throw plexus::timeout_error(__FUNCTION__); 349 | } 350 | 351 | static bool identical(const boost::asio::ip::udp::endpoint& lhs, const boost::asio::ip::udp::endpoint& rhs) 352 | { 353 | if(lhs.port() != rhs.port()) 354 | return false; 355 | 356 | if (lhs.address() != boost::asio::ip::address() && rhs.address() != boost::asio::ip::address()) 357 | return lhs == rhs; 358 | 359 | if (lhs.address() == boost::asio::ip::address() && rhs.address() == boost::asio::ip::address()) 360 | return true; 361 | 362 | auto addr = lhs.address() != boost::asio::ip::address() ? lhs.address() : rhs.address(); 363 | bool local = false; 364 | 365 | #ifndef _WIN32 366 | struct ifaddrs *ifaddr, *ifa; 367 | int family, s; 368 | char host[NI_MAXHOST]; 369 | 370 | if (getifaddrs(&ifaddr) == -1) 371 | { 372 | _err_ << "getifaddrs() failed: " << errno; 373 | return false; 374 | } 375 | 376 | for (ifa = ifaddr; ifa != NULL && !local; ifa = ifa->ifa_next) 377 | { 378 | if (ifa->ifa_addr == NULL) 379 | continue; 380 | 381 | family = ifa->ifa_addr->sa_family; 382 | 383 | if (family == AF_INET || family == AF_INET6) 384 | { 385 | s = getnameinfo(ifa->ifa_addr, 386 | (family == AF_INET) ? sizeof(struct sockaddr_in) : 387 | sizeof(struct sockaddr_in6), 388 | host, NI_MAXHOST, NULL, 0, NI_NUMERICHOST); 389 | if (s != 0) 390 | { 391 | _err_ << "getnameinfo() failed: " << gai_strerror(s); 392 | continue; 393 | } 394 | 395 | local = addr == boost::asio::ip::make_address(host); 396 | } 397 | } 398 | 399 | freeifaddrs(ifaddr); 400 | #else 401 | ULONG bufferSize = 15000; 402 | std::vector buffer(bufferSize); 403 | PIP_ADAPTER_ADDRESSES pAdapterAddresses = reinterpret_cast(buffer.data()); 404 | 405 | DWORD dwRetVal = GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_PREFIX, nullptr, pAdapterAddresses, &bufferSize); 406 | 407 | if (dwRetVal == ERROR_BUFFER_OVERFLOW) 408 | { 409 | buffer.resize(bufferSize); 410 | pAdapterAddresses = reinterpret_cast(buffer.data()); 411 | dwRetVal = GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_PREFIX, nullptr, pAdapterAddresses, &bufferSize); 412 | } 413 | 414 | if (dwRetVal == NO_ERROR) 415 | { 416 | for (PIP_ADAPTER_ADDRESSES pAdapter = pAdapterAddresses; pAdapter != nullptr && !local; pAdapter = pAdapter->Next) 417 | { 418 | for (PIP_ADAPTER_UNICAST_ADDRESS pUnicast = pAdapter->FirstUnicastAddress; pUnicast != nullptr && !local; pUnicast = pUnicast->Next) 419 | { 420 | char ipString[INET6_ADDRSTRLEN]; 421 | if (pUnicast->Address.lpSockaddr->sa_family == AF_INET) 422 | { 423 | sockaddr_in* pSockAddrV4 = reinterpret_cast(pUnicast->Address.lpSockaddr); 424 | inet_ntop(AF_INET, &(pSockAddrV4->sin_addr), ipString, sizeof(ipString)); 425 | } 426 | else if (pUnicast->Address.lpSockaddr->sa_family == AF_INET6) 427 | { 428 | sockaddr_in6* pSockAddrV6 = reinterpret_cast(pUnicast->Address.lpSockaddr); 429 | inet_ntop(AF_INET6, &(pSockAddrV6->sin6_addr), ipString, sizeof(ipString)); 430 | } 431 | 432 | local = addr == boost::asio::ip::make_address(ipString); 433 | } 434 | } 435 | } 436 | else 437 | { 438 | _err_ << "GetAdaptersAddresses() failed: " << dwRetVal << std::endl; 439 | } 440 | #endif 441 | return local; 442 | } 443 | 444 | public: 445 | 446 | traverse explore_network(boost::asio::yield_context yield) noexcept(false) override 447 | { 448 | _trc_ << "exploring network..."; 449 | 450 | traverse hole = { { 0 } }; 451 | 452 | auto mapper = plexus::network::create_udp_transport(m_io, m_bind); 453 | auto response = exec_binding(mapper, yield, m_stun, m_stun); 454 | 455 | hole.inner_endpoint = mapper->local_endpoint(); 456 | hole.outer_endpoint = response.mapped_endpoint(); 457 | 458 | auto changed_stun = response.changed_endpoint(); 459 | 460 | if (identical(hole.inner_endpoint, hole.outer_endpoint)) 461 | { 462 | hole.traits.mapping = traverse::independent; 463 | hole.traits.filtering = traverse::independent; 464 | hole.traits.hairpin = true; 465 | } 466 | else 467 | { 468 | hole.traits.nat = true; 469 | 470 | if (hole.inner_endpoint.port() != hole.outer_endpoint.port()) 471 | { 472 | hole.traits.random_port = true; 473 | } 474 | 475 | _trc_ << "first mapping test..."; 476 | 477 | auto first_endpoint = exec_binding(mapper, yield, changed_stun, changed_stun).mapped_endpoint(); 478 | if (first_endpoint == hole.outer_endpoint) 479 | { 480 | hole.traits.mapping = traverse::independent; 481 | } 482 | else 483 | { 484 | _trc_ << "second mapping test..."; 485 | 486 | boost::asio::ip::udp::endpoint stun(changed_stun.address(), m_stun.port()); 487 | auto second_endpoint = exec_binding(mapper, yield, stun, stun).mapped_endpoint(); 488 | 489 | if (second_endpoint == hole.outer_endpoint) 490 | { 491 | hole.traits.mapping = traverse::port_dependent; 492 | } 493 | else if (second_endpoint == first_endpoint) 494 | { 495 | hole.traits.mapping = traverse::address_dependent; 496 | } 497 | else 498 | { 499 | hole.traits.mapping = traverse::address_and_port_dependent; 500 | } 501 | 502 | if (second_endpoint.address() != hole.outer_endpoint.address() || second_endpoint.address() != first_endpoint.address()) 503 | { 504 | hole.traits.variable_address = true; 505 | } 506 | } 507 | 508 | auto filter = plexus::network::create_udp_transport(m_io); 509 | try 510 | { 511 | _trc_ << "first filtering test..."; 512 | 513 | exec_binding(filter, yield, m_stun, changed_stun, message(flag::change_address | flag::change_port), 1400); 514 | hole.traits.filtering = traverse::independent; 515 | } 516 | catch(const plexus::timeout_error&) 517 | { 518 | try 519 | { 520 | _trc_ << "second filtering test..."; 521 | 522 | exec_binding(filter, yield, m_stun, boost::asio::ip::udp::endpoint(changed_stun.address(), m_stun.port()), message(flag::change_address), 1400); 523 | hole.traits.filtering = traverse::port_dependent; 524 | } 525 | catch(const plexus::timeout_error&) 526 | { 527 | try 528 | { 529 | _trc_ << "third filtering test..."; 530 | 531 | exec_binding(filter, yield, m_stun, boost::asio::ip::udp::endpoint(m_stun.address(), changed_stun.port()), message(flag::change_port), 1400); 532 | hole.traits.filtering = traverse::address_dependent; 533 | } 534 | catch(const plexus::timeout_error&) 535 | { 536 | hole.traits.filtering = traverse::address_and_port_dependent; 537 | } 538 | } 539 | } 540 | 541 | try 542 | { 543 | _trc_ << "hairpin test..."; 544 | 545 | exec_binding(mapper, yield, hole.outer_endpoint , hole.outer_endpoint , message(0), 1400); 546 | hole.traits.hairpin = true; 547 | } 548 | catch(const plexus::timeout_error&) { } 549 | } 550 | 551 | _inf_ << "\ntraverse:" 552 | << "\n\tnat: " << hole.traits.nat 553 | << "\n\tmapping: " << to_string(hole.traits.mapping) 554 | << "\n\tfiltering: " << to_string(hole.traits.filtering) 555 | << "\n\trandom port: " << hole.traits.random_port 556 | << "\n\tvariable address: " << hole.traits.variable_address 557 | << "\n\thairpin: " << hole.traits.hairpin 558 | << "\n\tinner endpoint: " << hole.inner_endpoint 559 | << "\n\touter endpoint: " << hole.outer_endpoint; 560 | 561 | return hole; 562 | } 563 | }; 564 | 565 | } 566 | 567 | std::shared_ptr create_stun_client(boost::asio::io_context& io, const boost::asio::ip::udp::endpoint& server, const boost::asio::ip::udp::endpoint& local) noexcept(true) 568 | { 569 | return std::make_shared(io, server, local); 570 | } 571 | 572 | } 573 | --------------------------------------------------------------------------------