From dedeb9cd788ed7af2e1ed96f968d921e6540345d Mon Sep 17 00:00:00 2001 From: talksik Date: Thu, 27 Mar 2025 11:58:49 -0700 Subject: [PATCH] feat: initial commit (migrate from old flowy monorepo) --- .clang-format | 3 + .gitignore | 84 ++++++++++++++++++++ CMakeLists.txt | 35 +++++++++ README.md | 1 + src/broadcaster.cpp | 167 +++++++++++++++++++++++++++++++++++++++ src/broadcaster.h | 82 +++++++++++++++++++ src/scanner.cpp | 186 ++++++++++++++++++++++++++++++++++++++++++++ src/scanner.h | 56 +++++++++++++ 8 files changed, 614 insertions(+) create mode 100644 .clang-format create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 src/broadcaster.cpp create mode 100644 src/broadcaster.h create mode 100644 src/scanner.cpp create mode 100644 src/scanner.h diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..8f094f7 --- /dev/null +++ b/.clang-format @@ -0,0 +1,3 @@ +BasedOnStyle: Microsoft +TabWidth: 2 +IndentWidth: 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..caf4aa3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,84 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* +*.qbs.user* +CMakeLists.txt.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + +# Directories with generated files +.moc/ +.obj/ +.pch/ +.rcc/ +.uic/ +/build*/ + +.cache/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..228f010 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 3.16) + +project(mdns VERSION 0.1 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(CMAKE_EXPORT_COMPILATION_DATABASE ON) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") + +# https://doc.qt.io/qt-6/qt-cmake-policy-qtp0003.html +option(BUILD_SHARED_LIBS "Build shared instead of static library" ON) + +find_package(Qt6 6.5 REQUIRED COMPONENTS Core) +qt_standard_project_setup(REQUIRES 6.5) +qt_policy(SET QTP0003 NEW) + +set(SOURCES + src/broadcaster.h + src/broadcaster.cpp + src/scanner.h + src/scanner.cpp +) +qt_add_library(mdns ${SOURCES}) + +target_include_directories(mdns PUBLIC src/) + +target_link_libraries(mdns PRIVATE Qt6::Core) + +include(GNUInstallDirs) +install(TARGETS mdns + BUNDLE DESTINATION . + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) + diff --git a/README.md b/README.md new file mode 100644 index 0000000..ad1a452 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +This package is a Qt wrapper over [zeroconf](https://github.com/yvz/zeroconf). diff --git a/src/broadcaster.cpp b/src/broadcaster.cpp new file mode 100644 index 0000000..a00d1a1 --- /dev/null +++ b/src/broadcaster.cpp @@ -0,0 +1,167 @@ +#include "broadcaster.h" +#include +#include +#include +#include + +ServiceInfo::ServiceInfo(QString sn, QString st, uint16_t p) : service_name(sn), service_type(st), service_port(p) +{ +} + +void BroadcastWorker::static_callback(DNSServiceRef sdRef, DNSServiceFlags flags, DNSServiceErrorType errorCode, + const char *name, const char *regtype, const char *domain, void *context) +{ + // Cast the context back to our class instance + BroadcastWorker *self = static_cast(context); + if (errorCode == kDNSServiceErr_NoError) + { + std::cout << "Service registered successfully!" << std::endl; + std::cout << "Service Name: " << name << std::endl; + std::cout << "Service Type: " << regtype << std::endl; + std::cout << "Domain: " << domain << std::endl; + emit self->service_registered(); + } + else + { + std::cerr << "Error registering service: " << errorCode << std::endl; + emit self->error_occurred("Error registering service."); + } +} + +BroadcastWorker::BroadcastWorker(QObject *parent, ServiceInfo *service) : QThread(parent), m_service_info(service) +{ +} + +void BroadcastWorker::run() +{ + DNSServiceRef serviceRef; + DNSServiceErrorType error; + + // Service parameters + std::string service_name = m_service_info->service_name.toStdString(); + std::string service_type = m_service_info->service_type.toStdString(); + const char *serviceName = service_name.c_str(); // Can be NULL for automatic naming + const char *serviceType = service_type.c_str(); // Service type and protocol + const char *serviceDomain = NULL; // NULL for default domain + const char *serviceHost = NULL; // NULL for default host + uint16_t servicePort = m_service_info->service_port; // Service port in network byte order + uint16_t txtLen = 0; // Length of TXT record + const void *txtRecord = NULL; // TXT record (can be NULL) + + // Convert port to network byte order + servicePort = htons(servicePort); + + // Register the service + error = DNSServiceRegister(&serviceRef, + 0, // Default flags + 0, // Default interface + serviceName, serviceType, serviceDomain, serviceHost, servicePort, txtLen, txtRecord, + BroadcastWorker::static_callback, + this); // Context pointer + + if (error != kDNSServiceErr_NoError) + { + std::cerr << "Error registering service: " << error << std::endl; + emit error_occurred("Error registering service."); + return; + } + + // Process the service registration + int dns_sd_fd = DNSServiceRefSockFD(serviceRef); + fd_set readfds; + struct timeval tv; + int result; + + // Keep the service registered until the user terminates the program + std::cout << "Service is being broadcasted." << std::endl; + + while (1) + { + if (isInterruptionRequested()) + { + // Clean up + DNSServiceRefDeallocate(serviceRef); + return; + } + + FD_ZERO(&readfds); + FD_SET(dns_sd_fd, &readfds); + + tv.tv_sec = 1; + tv.tv_usec = 0; + + result = select(dns_sd_fd + 1, &readfds, NULL, NULL, &tv); + if (result > 0) + { + DNSServiceProcessResult(serviceRef); + } + } +} + +Broadcaster::Broadcaster(QObject *parent) : QObject(parent) +{ +} + +Broadcaster::~Broadcaster() +{ + if (m_worker) + { + m_worker->requestInterruption(); + } +} + +void Broadcaster::start_broadcasting(ServiceInfo *service) +{ + if (m_worker) + { + qWarning() << "Already have a worker for broadcasting"; + emit error_occurred("Already have a worker for broadcasting. Please stop first."); + return; + } + + m_worker = new BroadcastWorker(this, service); + connect(m_worker, &BroadcastWorker::error_occurred, this, [this](const QString &message) { + emit broadcasting_failure(); + emit error_occurred(message); + }); + connect(m_worker, &BroadcastWorker::started, this, [this]() { qDebug() << "Broadcast worker started"; }); + connect(m_worker, &BroadcastWorker::service_registered, this, [this] { + set_is_broadcasting(true); + qDebug() << "Service registered"; + }); + connect(m_worker, &BroadcastWorker::finished, [this] { + qDebug() << "Broadcast worker finished"; + m_worker->deleteLater(); + m_worker = nullptr; + + set_is_broadcasting(false); + }); + m_worker->start(); +} + +void Broadcaster::stop_broadcasting() +{ + if (!m_worker || !m_worker->isRunning()) + { + qWarning() << "Not broadcasting"; + emit error_occurred("Not broadcasting, can't stop"); + return; + } + + m_worker->requestInterruption(); + m_worker->wait(); +} + +bool Broadcaster::is_broadcasting() const +{ + return m_is_broadcasting; +} + +void Broadcaster::set_is_broadcasting(bool new_val) +{ + if (m_is_broadcasting == new_val) + return; + + m_is_broadcasting = new_val; + emit is_broadcasting_changed(); +} diff --git a/src/broadcaster.h b/src/broadcaster.h new file mode 100644 index 0000000..d877a91 --- /dev/null +++ b/src/broadcaster.h @@ -0,0 +1,82 @@ +#ifndef BROADCASTER_H +#define BROADCASTER_H + +#include +#include +#include + +struct ServiceInfo +{ + QString service_name; + /** + * @brief e.g. _http._tcp + */ + QString service_type; + uint16_t service_port; + + ServiceInfo(QString sn, QString st, uint16_t sp); +}; + +/** + * @class BroadcastAdvertiser + * @brief Runs in another thread to broadcast a given service + * + */ +class BroadcastWorker : public QThread +{ + Q_OBJECT + +public: + explicit BroadcastWorker(QObject *parent, ServiceInfo *service_info); + +private: + ServiceInfo *m_service_info; + void run() override; + + // Static callback wrapper that will be called by the C API + static void static_callback(DNSServiceRef sdRef, DNSServiceFlags flags, DNSServiceErrorType errorCode, + const char *name, const char *regtype, const char *domain, void *context); + +signals: + void error_occurred(const QString &message); + void service_registered(); +}; + +/** + * @class Broadcaster + * @brief Broadcasts a service with given port. It does so continuously in another thread. + */ +class Broadcaster : public QObject +{ + Q_OBJECT + +public: + explicit Broadcaster(QObject *parent = nullptr); + ~Broadcaster(); + + bool is_broadcasting() const; + +public slots: + /** + * @brief Starts broadcasting using the information from the given service. + */ + void start_broadcasting(ServiceInfo *); + /** + * @brief Stops broadcasting and does cleanup. Gracefully handles if not currently broadcasting. + */ + void stop_broadcasting(); + +signals: + void error_occurred(const QString &); + void broadcasting_failure(); + void is_broadcasting_changed(); + +private: + BroadcastWorker *m_worker; + + bool m_is_broadcasting; + + void set_is_broadcasting(bool); +}; + +#endif // BROADCASTER_H diff --git a/src/scanner.cpp b/src/scanner.cpp new file mode 100644 index 0000000..68e5254 --- /dev/null +++ b/src/scanner.cpp @@ -0,0 +1,186 @@ +#include "scanner.h" +#include // For inet_ntop +#include +#include + +Scanner::Scanner(QString query, QObject *parent) : m_query(query), QThread(parent) +{ +} + +void Scanner::run() +{ + DNSServiceRef serviceRef; + std::string query = m_query.toStdString(); + + DNSServiceErrorType error = + DNSServiceBrowse(&serviceRef, 0, 0, query.c_str(), NULL, Scanner::service_browse_callback, this); + + if (error != kDNSServiceErr_NoError) + { + fprintf(stderr, "Error browsing services: %d\n", error); + emit error_occurred("Error browing services"); + return; + } + + // Process the service registration + int dns_sd_fd = DNSServiceRefSockFD(serviceRef); + fd_set readfds; + struct timeval tv; + int result; + + // Keep the service registered until the user terminates the program + std::cout << "Service is being broadcasted." << std::endl; + + while (1) + { + if (isInterruptionRequested()) + { + // Clean up + DNSServiceRefDeallocate(serviceRef); + return; + } + + FD_ZERO(&readfds); + FD_SET(dns_sd_fd, &readfds); + + tv.tv_sec = 1; + tv.tv_usec = 0; + + result = select(dns_sd_fd + 1, &readfds, NULL, NULL, &tv); + if (result > 0) + { + DNSServiceProcessResult(serviceRef); + } + } +} + +void Scanner::service_resolve_callback(DNSServiceRef service, DNSServiceFlags flags, uint32_t interface_index, + DNSServiceErrorType error_code, const char *full_name, const char *host_target, + uint16_t port, uint16_t txt_len, const unsigned char *txt_record, void *context) +{ + Scanner *self = static_cast(context); + if (error_code == kDNSServiceErr_NoError) + { + printf("Resolved Service: %s\n", full_name); + printf("Host: %s\n", host_target); + printf("Port: %u\n", ntohs(port)); // Convert the port to host byte order + // + self->m_current_service_name = QString(full_name); + self->m_current_host_target = QString(host_target); + self->m_current_port = ntohs(port); + + DNSServiceRef addrRef; + DNSServiceErrorType addrError = + DNSServiceGetAddrInfo(&addrRef, 0, interface_index, kDNSServiceProtocol_IPv4 | kDNSServiceProtocol_IPv6, + host_target, Scanner::address_info_callback, self); + + if (addrError != kDNSServiceErr_NoError) + { + printf("Error getting address info: %d\n", addrError); + emit self->error_occurred("Error resolving IP address"); + } + else + { + // Process the result to get the IP + DNSServiceProcessResult(addrRef); + DNSServiceRefDeallocate(addrRef); + } + } + else + { + printf("Failed to resolve service: %d\n", error_code); + emit self->error_occurred("Error resolving service"); + } +} + +void Scanner::address_info_callback(DNSServiceRef sdRef, DNSServiceFlags flags, uint32_t interfaceIndex, + DNSServiceErrorType errorCode, const char *hostname, const struct sockaddr *address, + uint32_t ttl, void *context) +{ + Scanner *self = static_cast(context); + if (errorCode == kDNSServiceErr_NoError) + { + // Convert sockaddr to readable IP address + char ip_str[INET6_ADDRSTRLEN]; // Big enough for both IPv4 and IPv6 + bool is_good_address = false; + + if (address->sa_family == AF_INET) + { + // IPv4 + struct sockaddr_in *addr_in = (struct sockaddr_in *)address; + inet_ntop(AF_INET, &(addr_in->sin_addr), ip_str, INET_ADDRSTRLEN); + + // Skip link-local addresses (169.254.x.x) + uint8_t first_octet = addr_in->sin_addr.s_addr & 0xFF; + uint8_t second_octet = (addr_in->sin_addr.s_addr >> 8) & 0xFF; + + if (!(first_octet == 169 && second_octet == 254)) + { + is_good_address = true; + } + } + else if (address->sa_family == AF_INET6) + { + // IPv6 + struct sockaddr_in6 *addr_in6 = (struct sockaddr_in6 *)address; + inet_ntop(AF_INET6, &(addr_in6->sin6_addr), ip_str, INET6_ADDRSTRLEN); + + // Skip link-local addresses (fe80::) + if (!IN6_IS_ADDR_LINKLOCAL(&addr_in6->sin6_addr)) + { + // Only use IPv6 if we have no IPv4 (lower priority) + is_good_address = true; + } + } + else + { + strcpy(ip_str, "Unknown address type"); + } + + printf("IP Address: %s\n", ip_str); + + // Only emit the signal if this is a good address + if (is_good_address) + { + emit self->service_found(self->m_current_service_name, QString(ip_str), QString(hostname), self->m_current_port); + } + } + else + { + printf("Failed to resolve address: %d\n", errorCode); + emit self->error_occurred("Error resolving IP address"); + } +} + +void Scanner::service_browse_callback(DNSServiceRef service, DNSServiceFlags flags, uint32_t interface_index, + DNSServiceErrorType error_code, const char *service_name, const char *regtype, + const char *reply_domain, void *context) +{ + Scanner *self = static_cast(context); + + if (error_code == kDNSServiceErr_NoError) + { + printf("Service found: %s.%s%s\n", service_name, regtype, reply_domain ? reply_domain : ""); + + // Now resolve the service to get port and other details + DNSServiceRef resolveRef; + DNSServiceErrorType resolveError = DNSServiceResolve(&resolveRef, 0, interface_index, service_name, regtype, + reply_domain, Scanner::service_resolve_callback, self); + + if (resolveError != kDNSServiceErr_NoError) + { + printf("Error resolving service: %d\n", resolveError); + self->error_occurred("Error resolving service"); + } + else + { + // Process the resolved service + DNSServiceProcessResult(resolveRef); + DNSServiceRefDeallocate(resolveRef); + } + } + else + { + self->error_occurred("Error browsing dns"); + } +} diff --git a/src/scanner.h b/src/scanner.h new file mode 100644 index 0000000..ddabcaf --- /dev/null +++ b/src/scanner.h @@ -0,0 +1,56 @@ +#ifndef SCANNER_H +#define SCANNER_H + +#include +#include +#include + +struct Record +{ +}; + +/** + * @class Scanner + * @brief A thread that handles scanning with given mDNS query and returns. + * + */ +class Scanner : public QThread +{ + Q_OBJECT + +public: + /** + * @param query: e.g. "_http._tcp" + */ + explicit Scanner(QString query, QObject *parent = nullptr); + +private: + void run() override; + + QString m_query; + + // Caching values to use across callbacks + // TODO: pass a context struct around instead of this? or does every callback run sequentially? + QString m_current_service_name; + QString m_current_host_target; + uint16_t m_current_port; + + static void DNSSD_API service_resolve_callback(DNSServiceRef service, DNSServiceFlags flags, uint32_t interface_index, + DNSServiceErrorType error_code, const char *full_name, + const char *host_target, uint16_t port, uint16_t txt_len, + const unsigned char *txt_record, void *context); + static void DNSSD_API service_browse_callback(DNSServiceRef service, DNSServiceFlags flags, uint32_t interface_index, + DNSServiceErrorType error_code, const char *service_name, + const char *regtype, const char *reply_domain, void *context); + static void DNSSD_API address_info_callback(DNSServiceRef sdRef, DNSServiceFlags flags, uint32_t interfaceIndex, + DNSServiceErrorType errorCode, const char *hostname, + const struct sockaddr *address, uint32_t ttl, void *context); + +public slots: + +signals: + void service_found(const QString &service_name, const QString &ip, const QString &host, const uint16_t port); + void error_occurred(const QString &message); +}; + +#endif // SCANNER_H