tools: add upnpctrl
Change-Id: I4eba598ae849d982d077fce000d0d83f5a4f7762
diff --git a/CMakeLists.txt b/CMakeLists.txt
index f4ce3b7..d787958 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -145,6 +145,12 @@
target_link_libraries(dnc PRIVATE dhtnet fmt::fmt)
target_include_directories(dnc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tools)
install(TARGETS dnc RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
+
+ add_executable(upnpctrl
+ tools/upnp/upnpctrl.cpp)
+ target_link_libraries(upnpctrl PRIVATE dhtnet fmt::fmt readline)
+ target_include_directories(upnpctrl PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tools)
+ install(TARGETS upnpctrl RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
endif()
if (BUILD_TESTING AND NOT MSVC)
diff --git a/tools/upnp/upnpctrl.cpp b/tools/upnp/upnpctrl.cpp
new file mode 100644
index 0000000..586898b
--- /dev/null
+++ b/tools/upnp/upnpctrl.cpp
@@ -0,0 +1,96 @@
+#include "upnp/upnp_control.h"
+#include "upnp/upnp_context.h"
+#include "string_utils.h"
+#include <asio/executor_work_guard.hpp>
+#include <opendht/log.h>
+
+#include <readline/readline.h>
+#include <readline/history.h>
+
+void
+print_help()
+{
+ fmt::print("Commands:\n"
+ " help, h, ?\n"
+ " quit, exit, q, x\n"
+ " ip\n"
+ " open <port> <protocol>\n");
+}
+
+std::string to_lower(std::string_view str_v) {
+ std::string str(str_v);
+ std::transform(str.begin(), str.end(), str.begin(),
+ [](unsigned char c){ return std::tolower(c); }
+ );
+ return str;
+}
+
+int
+main(int argc, char** argv)
+{
+ auto ioContext = std::make_shared<asio::io_context>();
+ std::shared_ptr<dht::log::Logger> logger = dht::log::getStdLogger();
+ auto upnpContext = std::make_shared<dhtnet::upnp::UPnPContext>(ioContext, logger);
+
+ auto ioContextRunner = std::make_shared<std::thread>([context = ioContext]() {
+ try {
+ auto work = asio::make_work_guard(*context);
+ context->run();
+ } catch (const std::exception& ex) {
+ // print the error;
+ }
+ });
+
+ auto controller = std::make_shared<dhtnet::upnp::Controller>(upnpContext);
+ std::set<std::shared_ptr<dhtnet::upnp::Mapping>> mappings;
+
+ while (true) {
+ char* l = readline("> ");
+ if (not l)
+ break;
+ std::string_view line{l};
+ if (line.empty())
+ continue;
+ add_history(l);
+ auto args = dhtnet::split_string(line, ' ');
+ auto command = args[0];
+ if (command == "quit" || command == "exit" || command == "q" || command == "x")
+ break;
+ else if (command == "help" || command == "h" || command == "?") {
+ print_help();
+ }
+ else if (command == "ip") {
+ fmt::print("{}\n", controller->getExternalIP().toString());
+ } else if (command == "open") {
+ if (args.size() < 3) {
+ fmt::print("Usage: open <port> <protocol>\n");
+ continue;
+ }
+ auto protocol = to_lower(args[2]) == "udp" ? dhtnet::upnp::PortType::UDP : dhtnet::upnp::PortType::TCP;
+ mappings.emplace(controller->reserveMapping(dhtnet::to_int<in_port_t>(args[1]), protocol));
+ } else if (command == "close") {
+ if (args.size() < 2) {
+ fmt::print("Usage: close <port>\n");
+ continue;
+ }
+ auto port = dhtnet::to_int<in_port_t>(args[1]);
+ for (auto it = mappings.begin(); it != mappings.end(); ) {
+ if ((*it)->getExternalPort() == port) {
+ controller->releaseMapping(**it);
+ it = mappings.erase(it);
+ } else {
+ ++it;
+ }
+ }
+ } else {
+ fmt::print("Unknown command: {}\n", command);
+ }
+ }
+ fmt::print("Stopping...");
+ for (auto c: mappings)
+ controller->releaseMapping(*c);
+ mappings.clear();
+
+ ioContext->stop();
+ ioContextRunner->join();
+}