diff --git a/deps_src/CMakeLists.txt b/deps_src/CMakeLists.txt index 5c553835ad..a4d71c6f27 100644 --- a/deps_src/CMakeLists.txt +++ b/deps_src/CMakeLists.txt @@ -27,6 +27,7 @@ add_subdirectory(libigl) add_subdirectory(libnest2d) add_subdirectory(mcut) add_subdirectory(md4c) +add_subdirectory(mdns) add_subdirectory(miniz) add_subdirectory(minilzo) add_subdirectory(qhull) diff --git a/deps_src/mdns/CMakeLists.txt b/deps_src/mdns/CMakeLists.txt new file mode 100644 index 0000000000..60960d44e1 --- /dev/null +++ b/deps_src/mdns/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.13) + +project(mdns) + +# Static library wrapping mjansson/mdns (public domain) plus the cxmdns C++ +# wrapper from CrealityPrint v7.1.1 (AGPL-3.0). See NOTICE.md for attribution. +add_library(mdns STATIC + mdns.h + mdns.c + cxmdns.h + cxmdns.cpp +) + +target_include_directories(mdns SYSTEM PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +if (MSVC) + # mjansson/mdns uses GetAdaptersAddresses (Iphlpapi) and Winsock2 (Ws2_32). + target_link_libraries(mdns PUBLIC Iphlpapi Ws2_32) +endif() diff --git a/deps_src/mdns/NOTICE.md b/deps_src/mdns/NOTICE.md new file mode 100644 index 0000000000..60ad15c29a --- /dev/null +++ b/deps_src/mdns/NOTICE.md @@ -0,0 +1,39 @@ +# mDNS / DNS-SD library + +The four files in this directory implement mDNS / DNS-SD lookup and are +vendored from third-party sources: + +## mdns.h, mdns.c + +mDNS / DNS-SD lookup library by Mattias Jansson. Originally released to +the public domain at https://github.com/mjansson/mdns. + +The exact files here were taken from CrealityOfficial/CrealityPrint +v7.1.1, which split the upstream header-only library into separate +declaration (mdns.h) and implementation (mdns.c) files. + +- Source: https://github.com/mjansson/mdns +- License: Public domain (no restrictions on use) + +## cxmdns.h, cxmdns.cpp + +Thin C++ wrapper over mdns.{h,c} that exposes a single function: + + std::vector syncDiscoveryService( + const std::vector& prefix); + +It sends a DNS-SD meta-discovery query (`_services._dns-sd._udp.local.`), +listens for ~5 seconds, and returns `{ip, service_name}` for every +service announcement whose name contains any of the given prefixes. + +OrcaSlicer uses this to find Creality K-series printers on the LAN +(service-name prefix "Creality"), since K-series firmware announces +each printer under a per-device-unique service type +`_Creality-._udp.local.` that no fixed-name query can +target. + +- Source: CrealityOfficial/CrealityPrint v7.1.1 + `src/slic3r/GUI/print_manage/utils/cxmdns.{h,cpp}` +- License: GNU AGPL-3.0 (compatible with OrcaSlicer's AGPL-3.0; see + top-level LICENSE.txt) +- Imported: 2026-05-19 diff --git a/deps_src/mdns/cxmdns.cpp b/deps_src/mdns/cxmdns.cpp new file mode 100644 index 0000000000..f505eaa0b2 --- /dev/null +++ b/deps_src/mdns/cxmdns.cpp @@ -0,0 +1,256 @@ +#include"cxmdns.h" +#include"mdns.h" +#ifdef _WIN32 +#define _CRT_SECURE_NO_WARNINGS 1 +#endif + +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#define sleep(x) Sleep(x * 1000) +#else +#include +#include +#include +#endif + +// Alias some things to simulate recieving data to fuzz library +#if defined(MDNS_FUZZING) +#define recvfrom(sock, buffer, capacity, flags, src_addr, addrlen) ((mdns_ssize_t)capacity) +#define printf +#endif + +#include "mdns.h" + +#if defined(MDNS_FUZZING) +#undef recvfrom +#endif + +namespace cxnet +{ + template + mdns_record_callback_fn lambda2function(F lambda) + { + static auto lambdabak = lambda; + return [](int sock, const struct sockaddr* from, size_t addrlen, + mdns_entry_type_t entry, uint16_t query_id, uint16_t rtype, + uint16_t rclass, uint32_t ttl, const void* data, size_t size, + size_t name_offset, size_t name_length, size_t record_offset, + size_t record_length, void* user_data)->int {lambdabak(sock, from, addrlen, entry, query_id, rtype, rclass, ttl, data, size, name_offset, name_length, record_offset, record_length, user_data); return 0; }; + } + + volatile sig_atomic_t running = 1; +#ifdef _WIN32 + BOOL console_handler(DWORD signal) { + if (signal == CTRL_C_EVENT) { + running = 0; + } + return TRUE; + } +#else + void signal_handler(int signal) { + running = 0; + } +#endif + + void recvMachineInfoFromSocket(int sock, void* buffer, size_t capacity, const std::vector& prefix, std::vector& retmachineInfos, int recIndex) + { + struct sockaddr_in6 addr; + struct sockaddr* saddr = (struct sockaddr*)&addr; + socklen_t addrlen = sizeof(addr); + memset(&addr, 0, sizeof(addr)); +#ifdef __APPLE__ + saddr->sa_len = sizeof(addr); +#endif + mdns_ssize_t ret = recvfrom(sock, (char*)buffer, (mdns_size_t)capacity, 0, saddr, &addrlen); + if (ret <= 0) + return; + + size_t data_size = (size_t)ret; + //size_t records = 0; + const uint16_t* data = (uint16_t*)buffer; + + uint16_t query_id = mdns_ntohs(data++); + uint16_t flags = mdns_ntohs(data++); + uint16_t questions = mdns_ntohs(data++); + uint16_t answer_rrs = mdns_ntohs(data++); + uint16_t authority_rrs = mdns_ntohs(data++); + uint16_t additional_rrs = mdns_ntohs(data++); + + // According to RFC 6762 the query ID MUST match the sent query ID (which is 0 in our case) + if (query_id || (flags != 0x8400)) + return; // Not a reply to our question + + // It seems some implementations do not fill the correct questions field, + // so ignore this check for now and only validate answer string + // if (questions != 1) + // return 0; + + int i; + for (i = 0; i < questions; ++i) { + size_t offset = MDNS_POINTER_DIFF(data, buffer); + size_t verify_offset = 12; + // Verify it's our question, _services._dns-sd._udp.local. + if (!mdns_string_equal(buffer, data_size, &offset, mdns_services_query, + sizeof(mdns_services_query), &verify_offset)) + return; + data = (const uint16_t*)MDNS_POINTER_OFFSET(buffer, offset); + + uint16_t rtype = mdns_ntohs(data++); + uint16_t rclass = mdns_ntohs(data++); + + // Make sure we get a reply based on our PTR question for class IN + if ((rtype != MDNS_RECORDTYPE_PTR) || ((rclass & 0x7FFF) != MDNS_CLASS_IN)) + return; + } + + for (i = 0; i < answer_rrs; ++i) { + size_t offset = MDNS_POINTER_DIFF(data, buffer); + size_t verify_offset = 12; + // Verify it's an answer to our question, _services._dns-sd._udp.local. + size_t name_offset = offset; + int is_answer = mdns_string_equal(buffer, data_size, &offset, mdns_services_query, + sizeof(mdns_services_query), &verify_offset); + if (!is_answer && !mdns_string_skip(buffer, data_size, &offset)) + break; + size_t name_length = offset - name_offset; + if ((offset + 10) > data_size) + return; + data = (const uint16_t*)MDNS_POINTER_OFFSET(buffer, offset); + + uint16_t rtype = mdns_ntohs(data++); + uint16_t rclass = mdns_ntohs(data++); + uint32_t ttl = mdns_ntohl(data); + data += 2; + uint16_t length = mdns_ntohs(data++); + if (length > (data_size - offset)) + return; + + static char addrbuf[64]; + static char entrybuf[256]; + static char namebuf[256]; + + if (is_answer) { + offset = MDNS_POINTER_DIFF(data, buffer); + (void)sizeof(sock); + (void)sizeof(query_id); + (void)sizeof(name_length); + //(void)sizeof(0); + mdns_string_t fromaddrstr = ip_address_to_string(addrbuf, sizeof(addrbuf), saddr, addrlen); + mdns_string_t entrystr = + mdns_string_extract(buffer, data_size, &name_offset, entrybuf, sizeof(entrybuf)); + if (rtype == MDNS_RECORDTYPE_PTR) { + mdns_string_t namestr = mdns_record_parse_ptr(buffer, data_size, offset, length, + namebuf, sizeof(namebuf)); + if (!namestr.str || namestr.length == 0) { + return; + } + const std::string answer_name(namestr.str, namestr.length); + bool bFound = false; + for (const auto& item : prefix) + { + if (answer_name.find(item) != std::string::npos) + bFound = true; + } + if (!bFound) + { + return; + } + + char ip[16] = { 0 }; + sscanf(fromaddrstr.str, "%[^:]", ip); + retmachineInfos.push_back({ ip, answer_name }); + } + } + } + } + + std::vector syncDiscoveryService(const std::vector& prefix) + { + std::vector retmachineInfos; + const char* hostname = "cxslice-host"; + // Initialize network environment +#ifdef _WIN32 + WORD versionWanted = MAKEWORD(1, 1); + WSADATA wsaData; + if (WSAStartup(versionWanted, &wsaData)) { + printf("Failed to initialize WinSock\n"); + return retmachineInfos; + } + char hostname_buffer[256]; + DWORD hostname_size = (DWORD)sizeof(hostname_buffer); + if (GetComputerNameA(hostname_buffer, &hostname_size)) + hostname = hostname_buffer; + SetConsoleCtrlHandler(console_handler, TRUE); +#else + char hostname_buffer[256]; + size_t hostname_size = sizeof(hostname_buffer); + if (gethostname(hostname_buffer, hostname_size) == 0) + hostname = hostname_buffer; + signal(SIGINT, signal_handler); +#endif + int sockets[32]; + int num_sockets = open_client_sockets(sockets, sizeof(sockets) / sizeof(sockets[0]), 0); + if (num_sockets <= 0) { + printf("Failed to open any client sockets\n"); +#ifdef _WIN32 + WSACleanup(); +#endif + return retmachineInfos; + } + printf("Opened %d socket%s for DNS-SD\n", num_sockets, num_sockets > 1 ? "s" : ""); + printf("Sending DNS-SD discovery\n"); + for (int isock = 0; isock < num_sockets; ++isock) { + if (mdns_discovery_send(sockets[isock])) + printf("Failed to send DNS-DS discovery: %s\n", strerror(errno)); + } + size_t capacity = 2048; + void* buffer = malloc(capacity); + size_t recordNum = 0; + void* user_data = 0; + + // This is a simple implementation that loops for 5 seconds or as long as we get replies + int res; + printf("Reading DNS-SD replies\n"); + do { + struct timeval timeout; + timeout.tv_sec = 5; + timeout.tv_usec = 0; + + int nfds = 0; + fd_set readfs; + FD_ZERO(&readfs); + for (int isock = 0; isock < num_sockets; ++isock) { + if (sockets[isock] >= nfds) + nfds = sockets[isock] + 1; + FD_SET(sockets[isock], &readfs); + } + res = select(nfds, &readfs, 0, 0, &timeout); + if (res > 0) { + for (int isock = 0; isock < num_sockets; ++isock) { + if (FD_ISSET(sockets[isock], &readfs)) { + // records += mdns_discovery_recv(sockets[isock], buffer, capacity, query_callback, + // 0); + recvMachineInfoFromSocket(sockets[isock], buffer, capacity, prefix, retmachineInfos, isock); + recordNum++; + } + } + } + } while (res > 0); + + free(buffer); + // + for (int isock = 0; isock < num_sockets; ++isock) + mdns_socket_close(sockets[isock]); + printf("Closed socket%s\n", num_sockets ? "s" : ""); +#ifdef _WIN32 + WSACleanup(); +#endif + return std::move(retmachineInfos); + } +} diff --git a/deps_src/mdns/cxmdns.h b/deps_src/mdns/cxmdns.h new file mode 100644 index 0000000000..e0f9c1c3dc --- /dev/null +++ b/deps_src/mdns/cxmdns.h @@ -0,0 +1,16 @@ +#ifndef _CX_MDNS_H +#define _CX_MDNS_H +#include +#include + +namespace cxnet +{ + struct machine_info + { + std::string machineIp; + std::string answer; + }; + + std::vector syncDiscoveryService(const std::vector& prefix); +} +#endif \ No newline at end of file diff --git a/deps_src/mdns/mdns.c b/deps_src/mdns/mdns.c new file mode 100644 index 0000000000..089b495176 --- /dev/null +++ b/deps_src/mdns/mdns.c @@ -0,0 +1,1263 @@ + +#ifdef _WIN32 +#define _CRT_SECURE_NO_WARNINGS 1 +#endif + +#include + +#include +#include + +#ifdef _WIN32 +#include +#include +#define sleep(x) Sleep(x * 1000) +#else +#include +#include +#include +#endif + +// Alias some things to simulate recieving data to fuzz library +#if defined(MDNS_FUZZING) +#define recvfrom(sock, buffer, capacity, flags, src_addr, addrlen) ((mdns_ssize_t)capacity) +#define printf +#endif + +#include "mdns.h" + +#if defined(MDNS_FUZZING) +#undef recvfrom +#endif + +static char addrbuffer[64]; +static char entrybuffer[256]; +static char namebuffer[256]; +static char sendbuffer[1024]; +static mdns_record_txt_t txtbuffer[128]; + +static struct sockaddr_in service_address_ipv4; +static struct sockaddr_in6 service_address_ipv6; + +static int has_ipv4; +static int has_ipv6; + +volatile sig_atomic_t running = 1; + +// Data for our service including the mDNS records +typedef struct { + mdns_string_t service; + mdns_string_t hostname; + mdns_string_t service_instance; + mdns_string_t hostname_qualified; + struct sockaddr_in address_ipv4; + struct sockaddr_in6 address_ipv6; + int port; + mdns_record_t record_ptr; + mdns_record_t record_srv; + mdns_record_t record_a; + mdns_record_t record_aaaa; + mdns_record_t txt_record[2]; +} service_t; + +mdns_string_t +ipv4_address_to_string(char* buffer, size_t capacity, const struct sockaddr_in* addr, + size_t addrlen) { + char host[NI_MAXHOST] = {0}; + char service[NI_MAXSERV] = {0}; + int ret = getnameinfo((const struct sockaddr*)addr, (socklen_t)addrlen, host, NI_MAXHOST, + service, NI_MAXSERV, NI_NUMERICSERV | NI_NUMERICHOST); + int len = 0; + if (ret == 0) { + if (addr->sin_port != 0) + len = snprintf(buffer, capacity, "%s:%s", host, service); + else + len = snprintf(buffer, capacity, "%s", host); + } + if (len >= (int)capacity) + len = (int)capacity - 1; + mdns_string_t str; + str.str = buffer; + str.length = len; + return str; +} + +static mdns_string_t +ipv6_address_to_string(char* buffer, size_t capacity, const struct sockaddr_in6* addr, + size_t addrlen) { + char host[NI_MAXHOST] = {0}; + char service[NI_MAXSERV] = {0}; + int ret = getnameinfo((const struct sockaddr*)addr, (socklen_t)addrlen, host, NI_MAXHOST, + service, NI_MAXSERV, NI_NUMERICSERV | NI_NUMERICHOST); + int len = 0; + if (ret == 0) { + if (addr->sin6_port != 0) + len = snprintf(buffer, capacity, "[%s]:%s", host, service); + else + len = snprintf(buffer, capacity, "%s", host); + } + if (len >= (int)capacity) + len = (int)capacity - 1; + mdns_string_t str; + str.str = buffer; + str.length = len; + return str; +} + +mdns_string_t +ip_address_to_string(char* buffer, size_t capacity, const struct sockaddr* addr, size_t addrlen) { + if (addr->sa_family == AF_INET6) + return ipv6_address_to_string(buffer, capacity, (const struct sockaddr_in6*)addr, addrlen); + return ipv4_address_to_string(buffer, capacity, (const struct sockaddr_in*)addr, addrlen); +} + +// Callback handling parsing answers to queries sent +int +query_callback(int sock, const struct sockaddr* from, size_t addrlen, mdns_entry_type_t entry, + uint16_t query_id, uint16_t rtype, uint16_t rclass, uint32_t ttl, const void* data, + size_t size, size_t name_offset, size_t name_length, size_t record_offset, + size_t record_length, void* user_data) { + (void)sizeof(sock); + (void)sizeof(query_id); + (void)sizeof(name_length); + (void)sizeof(user_data); + mdns_string_t fromaddrstr = ip_address_to_string(addrbuffer, sizeof(addrbuffer), from, addrlen); + const char* entrytype = (entry == MDNS_ENTRYTYPE_ANSWER) ? + "answer" : + ((entry == MDNS_ENTRYTYPE_AUTHORITY) ? "authority" : "additional"); + mdns_string_t entrystr = + mdns_string_extract(data, size, &name_offset, entrybuffer, sizeof(entrybuffer)); + if (rtype == MDNS_RECORDTYPE_PTR) { + mdns_string_t namestr = mdns_record_parse_ptr(data, size, record_offset, record_length, + namebuffer, sizeof(namebuffer)); + printf("%.*s : %s %.*s PTR %.*s rclass 0x%x ttl %u length %d\n", + MDNS_STRING_FORMAT(fromaddrstr), entrytype, MDNS_STRING_FORMAT(entrystr), + MDNS_STRING_FORMAT(namestr), rclass, ttl, (int)record_length); + } else if (rtype == MDNS_RECORDTYPE_SRV) { + mdns_record_srv_t srv = mdns_record_parse_srv(data, size, record_offset, record_length, + namebuffer, sizeof(namebuffer)); + printf("%.*s : %s %.*s SRV %.*s priority %d weight %d port %d\n", + MDNS_STRING_FORMAT(fromaddrstr), entrytype, MDNS_STRING_FORMAT(entrystr), + MDNS_STRING_FORMAT(srv.name), srv.priority, srv.weight, srv.port); + } else if (rtype == MDNS_RECORDTYPE_A) { + struct sockaddr_in addr; + mdns_record_parse_a(data, size, record_offset, record_length, &addr); + mdns_string_t addrstr = + ipv4_address_to_string(namebuffer, sizeof(namebuffer), &addr, sizeof(addr)); + printf("%.*s : %s %.*s A %.*s\n", MDNS_STRING_FORMAT(fromaddrstr), entrytype, + MDNS_STRING_FORMAT(entrystr), MDNS_STRING_FORMAT(addrstr)); + } else if (rtype == MDNS_RECORDTYPE_AAAA) { + struct sockaddr_in6 addr; + mdns_record_parse_aaaa(data, size, record_offset, record_length, &addr); + mdns_string_t addrstr = + ipv6_address_to_string(namebuffer, sizeof(namebuffer), &addr, sizeof(addr)); + printf("%.*s : %s %.*s AAAA %.*s\n", MDNS_STRING_FORMAT(fromaddrstr), entrytype, + MDNS_STRING_FORMAT(entrystr), MDNS_STRING_FORMAT(addrstr)); + } else if (rtype == MDNS_RECORDTYPE_TXT) { + size_t parsed = mdns_record_parse_txt(data, size, record_offset, record_length, txtbuffer, + sizeof(txtbuffer) / sizeof(mdns_record_txt_t)); + for (size_t itxt = 0; itxt < parsed; ++itxt) { + if (txtbuffer[itxt].value.length) { + printf("%.*s : %s %.*s TXT %.*s = %.*s\n", MDNS_STRING_FORMAT(fromaddrstr), + entrytype, MDNS_STRING_FORMAT(entrystr), + MDNS_STRING_FORMAT(txtbuffer[itxt].key), + MDNS_STRING_FORMAT(txtbuffer[itxt].value)); + } else { + printf("%.*s : %s %.*s TXT %.*s\n", MDNS_STRING_FORMAT(fromaddrstr), entrytype, + MDNS_STRING_FORMAT(entrystr), MDNS_STRING_FORMAT(txtbuffer[itxt].key)); + } + } + } else { + printf("%.*s : %s %.*s type %u rclass 0x%x ttl %u length %d\n", + MDNS_STRING_FORMAT(fromaddrstr), entrytype, MDNS_STRING_FORMAT(entrystr), rtype, + rclass, ttl, (int)record_length); + } + return 0; +} + +// Callback handling questions incoming on service sockets +static int +service_callback(int sock, const struct sockaddr* from, size_t addrlen, mdns_entry_type_t entry, + uint16_t query_id, uint16_t rtype, uint16_t rclass, uint32_t ttl, const void* data, + size_t size, size_t name_offset, size_t name_length, size_t record_offset, + size_t record_length, void* user_data) { + (void)sizeof(ttl); + if (entry != MDNS_ENTRYTYPE_QUESTION) + return 0; + + const char dns_sd[] = "_services._dns-sd._udp.local."; + const service_t* service = (const service_t*)user_data; + + mdns_string_t fromaddrstr = ip_address_to_string(addrbuffer, sizeof(addrbuffer), from, addrlen); + + size_t offset = name_offset; + mdns_string_t name = mdns_string_extract(data, size, &offset, namebuffer, sizeof(namebuffer)); + + const char* record_name = 0; + if (rtype == MDNS_RECORDTYPE_PTR) + record_name = "PTR"; + else if (rtype == MDNS_RECORDTYPE_SRV) + record_name = "SRV"; + else if (rtype == MDNS_RECORDTYPE_A) + record_name = "A"; + else if (rtype == MDNS_RECORDTYPE_AAAA) + record_name = "AAAA"; + else if (rtype == MDNS_RECORDTYPE_TXT) + record_name = "TXT"; + else if (rtype == MDNS_RECORDTYPE_ANY) + record_name = "ANY"; + else + return 0; + printf("Query %s %.*s\n", record_name, MDNS_STRING_FORMAT(name)); + + if ((name.length == (sizeof(dns_sd) - 1)) && + (strncmp(name.str, dns_sd, sizeof(dns_sd) - 1) == 0)) { + if ((rtype == MDNS_RECORDTYPE_PTR) || (rtype == MDNS_RECORDTYPE_ANY)) { + // The PTR query was for the DNS-SD domain, send answer with a PTR record for the + // service name we advertise, typically on the "<_service-name>._tcp.local." format + + // Answer PTR record reverse mapping "<_service-name>._tcp.local." to + // ".<_service-name>._tcp.local." + mdns_record_t answer = { + .name = name, .type = MDNS_RECORDTYPE_PTR, .data.ptr.name = service->service}; + + // Send the answer, unicast or multicast depending on flag in query + uint16_t unicast = (rclass & MDNS_UNICAST_RESPONSE); + printf(" --> answer %.*s (%s)\n", MDNS_STRING_FORMAT(answer.data.ptr.name), + (unicast ? "unicast" : "multicast")); + + if (unicast) { + mdns_query_answer_unicast(sock, from, addrlen, sendbuffer, sizeof(sendbuffer), + query_id, rtype, name.str, name.length, answer, 0, 0, 0, + 0); + } else { + mdns_query_answer_multicast(sock, sendbuffer, sizeof(sendbuffer), answer, 0, 0, 0, + 0); + } + } + } else if ((name.length == service->service.length) && + (strncmp(name.str, service->service.str, name.length) == 0)) { + if ((rtype == MDNS_RECORDTYPE_PTR) || (rtype == MDNS_RECORDTYPE_ANY)) { + // The PTR query was for our service (usually "<_service-name._tcp.local"), answer a PTR + // record reverse mapping the queried service name to our service instance name + // (typically on the ".<_service-name>._tcp.local." format), and add + // additional records containing the SRV record mapping the service instance name to our + // qualified hostname (typically ".local.") and port, as well as any IPv4/IPv6 + // address for the hostname as A/AAAA records, and two test TXT records + + // Answer PTR record reverse mapping "<_service-name>._tcp.local." to + // ".<_service-name>._tcp.local." + mdns_record_t answer = service->record_ptr; + + mdns_record_t additional[5] = {0}; + size_t additional_count = 0; + + // SRV record mapping ".<_service-name>._tcp.local." to + // ".local." with port. Set weight & priority to 0. + additional[additional_count++] = service->record_srv; + + // A/AAAA records mapping ".local." to IPv4/IPv6 addresses + if (service->address_ipv4.sin_family == AF_INET) + additional[additional_count++] = service->record_a; + if (service->address_ipv6.sin6_family == AF_INET6) + additional[additional_count++] = service->record_aaaa; + + // Add two test TXT records for our service instance name, will be coalesced into + // one record with both key-value pair strings by the library + additional[additional_count++] = service->txt_record[0]; + additional[additional_count++] = service->txt_record[1]; + + // Send the answer, unicast or multicast depending on flag in query + uint16_t unicast = (rclass & MDNS_UNICAST_RESPONSE); + printf(" --> answer %.*s (%s)\n", + MDNS_STRING_FORMAT(service->record_ptr.data.ptr.name), + (unicast ? "unicast" : "multicast")); + + if (unicast) { + mdns_query_answer_unicast(sock, from, addrlen, sendbuffer, sizeof(sendbuffer), + query_id, rtype, name.str, name.length, answer, 0, 0, + additional, additional_count); + } else { + mdns_query_answer_multicast(sock, sendbuffer, sizeof(sendbuffer), answer, 0, 0, + additional, additional_count); + } + } + } else if ((name.length == service->service_instance.length) && + (strncmp(name.str, service->service_instance.str, name.length) == 0)) { + if ((rtype == MDNS_RECORDTYPE_SRV) || (rtype == MDNS_RECORDTYPE_ANY)) { + // The SRV query was for our service instance (usually + // ".<_service-name._tcp.local"), answer a SRV record mapping the service + // instance name to our qualified hostname (typically ".local.") and port, as + // well as any IPv4/IPv6 address for the hostname as A/AAAA records, and two test TXT + // records + + // Answer PTR record reverse mapping "<_service-name>._tcp.local." to + // ".<_service-name>._tcp.local." + mdns_record_t answer = service->record_srv; + + mdns_record_t additional[5] = {0}; + size_t additional_count = 0; + + // A/AAAA records mapping ".local." to IPv4/IPv6 addresses + if (service->address_ipv4.sin_family == AF_INET) + additional[additional_count++] = service->record_a; + if (service->address_ipv6.sin6_family == AF_INET6) + additional[additional_count++] = service->record_aaaa; + + // Add two test TXT records for our service instance name, will be coalesced into + // one record with both key-value pair strings by the library + additional[additional_count++] = service->txt_record[0]; + additional[additional_count++] = service->txt_record[1]; + + // Send the answer, unicast or multicast depending on flag in query + uint16_t unicast = (rclass & MDNS_UNICAST_RESPONSE); + printf(" --> answer %.*s port %d (%s)\n", + MDNS_STRING_FORMAT(service->record_srv.data.srv.name), service->port, + (unicast ? "unicast" : "multicast")); + + if (unicast) { + mdns_query_answer_unicast(sock, from, addrlen, sendbuffer, sizeof(sendbuffer), + query_id, rtype, name.str, name.length, answer, 0, 0, + additional, additional_count); + } else { + mdns_query_answer_multicast(sock, sendbuffer, sizeof(sendbuffer), answer, 0, 0, + additional, additional_count); + } + } + } else if ((name.length == service->hostname_qualified.length) && + (strncmp(name.str, service->hostname_qualified.str, name.length) == 0)) { + if (((rtype == MDNS_RECORDTYPE_A) || (rtype == MDNS_RECORDTYPE_ANY)) && + (service->address_ipv4.sin_family == AF_INET)) { + // The A query was for our qualified hostname (typically ".local.") and we + // have an IPv4 address, answer with an A record mappiing the hostname to an IPv4 + // address, as well as any IPv6 address for the hostname, and two test TXT records + + // Answer A records mapping ".local." to IPv4 address + mdns_record_t answer = service->record_a; + + mdns_record_t additional[5] = {0}; + size_t additional_count = 0; + + // AAAA record mapping ".local." to IPv6 addresses + if (service->address_ipv6.sin6_family == AF_INET6) + additional[additional_count++] = service->record_aaaa; + + // Add two test TXT records for our service instance name, will be coalesced into + // one record with both key-value pair strings by the library + additional[additional_count++] = service->txt_record[0]; + additional[additional_count++] = service->txt_record[1]; + + // Send the answer, unicast or multicast depending on flag in query + uint16_t unicast = (rclass & MDNS_UNICAST_RESPONSE); + mdns_string_t addrstr = ip_address_to_string( + addrbuffer, sizeof(addrbuffer), (struct sockaddr*)&service->record_a.data.a.addr, + sizeof(service->record_a.data.a.addr)); + printf(" --> answer %.*s IPv4 %.*s (%s)\n", MDNS_STRING_FORMAT(service->record_a.name), + MDNS_STRING_FORMAT(addrstr), (unicast ? "unicast" : "multicast")); + + if (unicast) { + mdns_query_answer_unicast(sock, from, addrlen, sendbuffer, sizeof(sendbuffer), + query_id, rtype, name.str, name.length, answer, 0, 0, + additional, additional_count); + } else { + mdns_query_answer_multicast(sock, sendbuffer, sizeof(sendbuffer), answer, 0, 0, + additional, additional_count); + } + } else if (((rtype == MDNS_RECORDTYPE_AAAA) || (rtype == MDNS_RECORDTYPE_ANY)) && + (service->address_ipv6.sin6_family == AF_INET6)) { + // The AAAA query was for our qualified hostname (typically ".local.") and we + // have an IPv6 address, answer with an AAAA record mappiing the hostname to an IPv6 + // address, as well as any IPv4 address for the hostname, and two test TXT records + + // Answer AAAA records mapping ".local." to IPv6 address + mdns_record_t answer = service->record_aaaa; + + mdns_record_t additional[5] = {0}; + size_t additional_count = 0; + + // A record mapping ".local." to IPv4 addresses + if (service->address_ipv4.sin_family == AF_INET) + additional[additional_count++] = service->record_a; + + // Add two test TXT records for our service instance name, will be coalesced into + // one record with both key-value pair strings by the library + additional[additional_count++] = service->txt_record[0]; + additional[additional_count++] = service->txt_record[1]; + + // Send the answer, unicast or multicast depending on flag in query + uint16_t unicast = (rclass & MDNS_UNICAST_RESPONSE); + mdns_string_t addrstr = + ip_address_to_string(addrbuffer, sizeof(addrbuffer), + (struct sockaddr*)&service->record_aaaa.data.aaaa.addr, + sizeof(service->record_aaaa.data.aaaa.addr)); + printf(" --> answer %.*s IPv6 %.*s (%s)\n", + MDNS_STRING_FORMAT(service->record_aaaa.name), MDNS_STRING_FORMAT(addrstr), + (unicast ? "unicast" : "multicast")); + + if (unicast) { + mdns_query_answer_unicast(sock, from, addrlen, sendbuffer, sizeof(sendbuffer), + query_id, rtype, name.str, name.length, answer, 0, 0, + additional, additional_count); + } else { + mdns_query_answer_multicast(sock, sendbuffer, sizeof(sendbuffer), answer, 0, 0, + additional, additional_count); + } + } + } + return 0; +} + +// Callback handling questions and answers dump +static int +dump_callback(int sock, const struct sockaddr* from, size_t addrlen, mdns_entry_type_t entry, + uint16_t query_id, uint16_t rtype, uint16_t rclass, uint32_t ttl, const void* data, + size_t size, size_t name_offset, size_t name_length, size_t record_offset, + size_t record_length, void* user_data) { + mdns_string_t fromaddrstr = ip_address_to_string(addrbuffer, sizeof(addrbuffer), from, addrlen); + + size_t offset = name_offset; + mdns_string_t name = mdns_string_extract(data, size, &offset, namebuffer, sizeof(namebuffer)); + + const char* record_name = 0; + if (rtype == MDNS_RECORDTYPE_PTR) + record_name = "PTR"; + else if (rtype == MDNS_RECORDTYPE_SRV) + record_name = "SRV"; + else if (rtype == MDNS_RECORDTYPE_A) + record_name = "A"; + else if (rtype == MDNS_RECORDTYPE_AAAA) + record_name = "AAAA"; + else if (rtype == MDNS_RECORDTYPE_TXT) + record_name = "TXT"; + else if (rtype == MDNS_RECORDTYPE_ANY) + record_name = "ANY"; + else + record_name = ""; + + const char* entry_type = "Question"; + if (entry == MDNS_ENTRYTYPE_ANSWER) + entry_type = "Answer"; + else if (entry == MDNS_ENTRYTYPE_AUTHORITY) + entry_type = "Authority"; + else if (entry == MDNS_ENTRYTYPE_ADDITIONAL) + entry_type = "Additional"; + + printf("%.*s: %s %s %.*s rclass 0x%x ttl %u\n", MDNS_STRING_FORMAT(fromaddrstr), entry_type, + record_name, MDNS_STRING_FORMAT(name), (unsigned int)rclass, ttl); + + return 0; +} + +// Open sockets for sending one-shot multicast queries from an ephemeral port +int +open_client_sockets(int* sockets, int max_sockets, int port) { + // When sending, each socket can only send to one network interface + // Thus we need to open one socket for each interface and address family + int num_sockets = 0; + +#ifdef _WIN32 + + IP_ADAPTER_ADDRESSES* adapter_address = 0; + ULONG address_size = 8000; + unsigned int ret; + unsigned int num_retries = 4; + do { + adapter_address = (IP_ADAPTER_ADDRESSES*)malloc(address_size); + ret = GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_ANYCAST, 0, + adapter_address, &address_size); + if (ret == ERROR_BUFFER_OVERFLOW) { + free(adapter_address); + adapter_address = 0; + address_size *= 2; + } else { + break; + } + } while (num_retries-- > 0); + + if (!adapter_address || (ret != NO_ERROR)) { + free(adapter_address); + printf("Failed to get network adapter addresses\n"); + return num_sockets; + } + + int first_ipv4 = 1; + int first_ipv6 = 1; + for (PIP_ADAPTER_ADDRESSES adapter = adapter_address; adapter; adapter = adapter->Next) { + if (adapter->TunnelType == TUNNEL_TYPE_TEREDO) + continue; + if (adapter->OperStatus != IfOperStatusUp) + continue; + + for (IP_ADAPTER_UNICAST_ADDRESS* unicast = adapter->FirstUnicastAddress; unicast; + unicast = unicast->Next) { + if (unicast->Address.lpSockaddr->sa_family == AF_INET) { + struct sockaddr_in* saddr = (struct sockaddr_in*)unicast->Address.lpSockaddr; + if ((saddr->sin_addr.S_un.S_un_b.s_b1 != 127) || + (saddr->sin_addr.S_un.S_un_b.s_b2 != 0) || + (saddr->sin_addr.S_un.S_un_b.s_b3 != 0) || + (saddr->sin_addr.S_un.S_un_b.s_b4 != 1)) { + int log_addr = 0; + if (first_ipv4) { + service_address_ipv4 = *saddr; + first_ipv4 = 0; + log_addr = 1; + } + has_ipv4 = 1; + if (num_sockets < max_sockets) { + saddr->sin_port = htons((unsigned short)port); + int sock = mdns_socket_open_ipv4(saddr); + if (sock >= 0) { + sockets[num_sockets++] = sock; + log_addr = 1; + } else { + log_addr = 0; + } + } + if (log_addr) { + char buffer[128]; + mdns_string_t addr = ipv4_address_to_string(buffer, sizeof(buffer), saddr, + sizeof(struct sockaddr_in)); + printf("Local IPv4 address: %.*s\n", MDNS_STRING_FORMAT(addr)); + } + } + } else if (unicast->Address.lpSockaddr->sa_family == AF_INET6) { + struct sockaddr_in6* saddr = (struct sockaddr_in6*)unicast->Address.lpSockaddr; + // Ignore link-local addresses + if (saddr->sin6_scope_id) + continue; + static const unsigned char localhost[] = {0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1}; + static const unsigned char localhost_mapped[] = {0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0xff, 0xff, 0x7f, 0, 0, 1}; + if ((unicast->DadState == NldsPreferred) && + memcmp(saddr->sin6_addr.s6_addr, localhost, 16) && + memcmp(saddr->sin6_addr.s6_addr, localhost_mapped, 16)) { + int log_addr = 0; + if (first_ipv6) { + service_address_ipv6 = *saddr; + first_ipv6 = 0; + log_addr = 1; + } + has_ipv6 = 1; + if (num_sockets < max_sockets) { + saddr->sin6_port = htons((unsigned short)port); + int sock = mdns_socket_open_ipv6(saddr); + if (sock >= 0) { + sockets[num_sockets++] = sock; + log_addr = 1; + } else { + log_addr = 0; + } + } + if (log_addr) { + char buffer[128]; + mdns_string_t addr = ipv6_address_to_string(buffer, sizeof(buffer), saddr, + sizeof(struct sockaddr_in6)); + printf("Local IPv6 address: %.*s\n", MDNS_STRING_FORMAT(addr)); + } + } + } + } + } + + free(adapter_address); + +#else + + struct ifaddrs* ifaddr = 0; + struct ifaddrs* ifa = 0; + + if (getifaddrs(&ifaddr) < 0) + printf("Unable to get interface addresses\n"); + + int first_ipv4 = 1; + int first_ipv6 = 1; + for (ifa = ifaddr; ifa; ifa = ifa->ifa_next) { + if (!ifa->ifa_addr) + continue; + if (!(ifa->ifa_flags & IFF_UP) || !(ifa->ifa_flags & IFF_MULTICAST)) + continue; + if ((ifa->ifa_flags & IFF_LOOPBACK) || (ifa->ifa_flags & IFF_POINTOPOINT)) + continue; + + if (ifa->ifa_addr->sa_family == AF_INET) { + struct sockaddr_in* saddr = (struct sockaddr_in*)ifa->ifa_addr; + if (saddr->sin_addr.s_addr != htonl(INADDR_LOOPBACK)) { + int log_addr = 0; + if (first_ipv4) { + service_address_ipv4 = *saddr; + first_ipv4 = 0; + log_addr = 1; + } + has_ipv4 = 1; + if (num_sockets < max_sockets) { + saddr->sin_port = htons(port); + int sock = mdns_socket_open_ipv4(saddr); + if (sock >= 0) { + sockets[num_sockets++] = sock; + log_addr = 1; + } else { + log_addr = 0; + } + } + if (log_addr) { + char buffer[128]; + mdns_string_t addr = ipv4_address_to_string(buffer, sizeof(buffer), saddr, + sizeof(struct sockaddr_in)); + printf("Local IPv4 address: %.*s\n", MDNS_STRING_FORMAT(addr)); + } + } + } else if (ifa->ifa_addr->sa_family == AF_INET6) { + struct sockaddr_in6* saddr = (struct sockaddr_in6*)ifa->ifa_addr; + // Ignore link-local addresses + if (saddr->sin6_scope_id) + continue; + static const unsigned char localhost[] = {0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1}; + static const unsigned char localhost_mapped[] = {0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0xff, 0xff, 0x7f, 0, 0, 1}; + if (memcmp(saddr->sin6_addr.s6_addr, localhost, 16) && + memcmp(saddr->sin6_addr.s6_addr, localhost_mapped, 16)) { + int log_addr = 0; + if (first_ipv6) { + service_address_ipv6 = *saddr; + first_ipv6 = 0; + log_addr = 1; + } + has_ipv6 = 1; + if (num_sockets < max_sockets) { + saddr->sin6_port = htons(port); + int sock = mdns_socket_open_ipv6(saddr); + if (sock >= 0) { + sockets[num_sockets++] = sock; + log_addr = 1; + } else { + log_addr = 0; + } + } + if (log_addr) { + char buffer[128]; + mdns_string_t addr = ipv6_address_to_string(buffer, sizeof(buffer), saddr, + sizeof(struct sockaddr_in6)); + printf("Local IPv6 address: %.*s\n", MDNS_STRING_FORMAT(addr)); + } + } + } + } + + freeifaddrs(ifaddr); + +#endif + + return num_sockets; +} + +// Open sockets to listen to incoming mDNS queries on port 5353 +static int +open_service_sockets(int* sockets, int max_sockets) { + // When recieving, each socket can recieve data from all network interfaces + // Thus we only need to open one socket for each address family + int num_sockets = 0; + + // Call the client socket function to enumerate and get local addresses, + // but not open the actual sockets + open_client_sockets(0, 0, 0); + + if (num_sockets < max_sockets) { + struct sockaddr_in sock_addr; + memset(&sock_addr, 0, sizeof(struct sockaddr_in)); + sock_addr.sin_family = AF_INET; +#ifdef _WIN32 + sock_addr.sin_addr = in4addr_any; +#else + sock_addr.sin_addr.s_addr = INADDR_ANY; +#endif + sock_addr.sin_port = htons(MDNS_PORT); +#ifdef __APPLE__ + sock_addr.sin_len = sizeof(struct sockaddr_in); +#endif + int sock = mdns_socket_open_ipv4(&sock_addr); + if (sock >= 0) + sockets[num_sockets++] = sock; + } + + if (num_sockets < max_sockets) { + struct sockaddr_in6 sock_addr; + memset(&sock_addr, 0, sizeof(struct sockaddr_in6)); + sock_addr.sin6_family = AF_INET6; + sock_addr.sin6_addr = in6addr_any; + sock_addr.sin6_port = htons(MDNS_PORT); +#ifdef __APPLE__ + sock_addr.sin6_len = sizeof(struct sockaddr_in6); +#endif + int sock = mdns_socket_open_ipv6(&sock_addr); + if (sock >= 0) + sockets[num_sockets++] = sock; + } + + return num_sockets; +} + +// Send a DNS-SD query +int +send_dns_sd(void) { + int sockets[32]; + int num_sockets = open_client_sockets(sockets, sizeof(sockets) / sizeof(sockets[0]), 0); + if (num_sockets <= 0) { + printf("Failed to open any client sockets\n"); + return -1; + } + printf("Opened %d socket%s for DNS-SD\n", num_sockets, num_sockets > 1 ? "s" : ""); + + printf("Sending DNS-SD discovery\n"); + for (int isock = 0; isock < num_sockets; ++isock) { + if (mdns_discovery_send(sockets[isock])) + printf("Failed to send DNS-DS discovery: %s\n", strerror(errno)); + } + + size_t capacity = 2048; + void* buffer = malloc(capacity); + void* user_data = 0; + size_t records; + + // This is a simple implementation that loops for 5 seconds or as long as we get replies + int res; + printf("Reading DNS-SD replies\n"); + do { + struct timeval timeout; + timeout.tv_sec = 5; + timeout.tv_usec = 0; + + int nfds = 0; + fd_set readfs; + FD_ZERO(&readfs); + for (int isock = 0; isock < num_sockets; ++isock) { + if (sockets[isock] >= nfds) + nfds = sockets[isock] + 1; + FD_SET(sockets[isock], &readfs); + } + + records = 0; + res = select(nfds, &readfs, 0, 0, &timeout); + if (res > 0) { + for (int isock = 0; isock < num_sockets; ++isock) { + if (FD_ISSET(sockets[isock], &readfs)) { + records += mdns_discovery_recv(sockets[isock], buffer, capacity, query_callback, + user_data); + } + } + } + } while (res > 0); + + free(buffer); + + for (int isock = 0; isock < num_sockets; ++isock) + mdns_socket_close(sockets[isock]); + printf("Closed socket%s\n", num_sockets ? "s" : ""); + + return 0; +} + +// Send a mDNS query +static int +send_mdns_query(mdns_query_t* query, size_t count) { + int sockets[32]; + int query_id[32]; + int num_sockets = open_client_sockets(sockets, sizeof(sockets) / sizeof(sockets[0]), 0); + if (num_sockets <= 0) { + printf("Failed to open any client sockets\n"); + return -1; + } + printf("Opened %d socket%s for mDNS query\n", num_sockets, num_sockets ? "s" : ""); + + size_t capacity = 2048; + void* buffer = malloc(capacity); + void* user_data = 0; + + printf("Sending mDNS query"); + for (size_t iq = 0; iq < count; ++iq) { + const char* record_name = "PTR"; + if (query[iq].type == MDNS_RECORDTYPE_SRV) + record_name = "SRV"; + else if (query[iq].type == MDNS_RECORDTYPE_A) + record_name = "A"; + else if (query[iq].type == MDNS_RECORDTYPE_AAAA) + record_name = "AAAA"; + else + query[iq].type = MDNS_RECORDTYPE_PTR; + printf(" : %s %s", query[iq].name, record_name); + } + printf("\n"); + for (int isock = 0; isock < num_sockets; ++isock) { + query_id[isock] = + mdns_multiquery_send(sockets[isock], query, count, buffer, capacity, 0); + if (query_id[isock] < 0) + printf("Failed to send mDNS query: %s\n", strerror(errno)); + } + + // This is a simple implementation that loops for 5 seconds or as long as we get replies + int res; + printf("Reading mDNS query replies\n"); + int records = 0; + do { + struct timeval timeout; + timeout.tv_sec = 10; + timeout.tv_usec = 0; + + int nfds = 0; + fd_set readfs; + FD_ZERO(&readfs); + for (int isock = 0; isock < num_sockets; ++isock) { + if (sockets[isock] >= nfds) + nfds = sockets[isock] + 1; + FD_SET(sockets[isock], &readfs); + } + + res = select(nfds, &readfs, 0, 0, &timeout); + if (res > 0) { + for (int isock = 0; isock < num_sockets; ++isock) { + if (FD_ISSET(sockets[isock], &readfs)) { + int rec = mdns_query_recv(sockets[isock], buffer, capacity, query_callback, + user_data, query_id[isock]); + if (rec > 0) + records += rec; + } + FD_SET(sockets[isock], &readfs); + } + } + } while (res > 0); + + printf("Read %d records\n", records); + + free(buffer); + + for (int isock = 0; isock < num_sockets; ++isock) + mdns_socket_close(sockets[isock]); + printf("Closed socket%s\n", num_sockets ? "s" : ""); + + return 0; +} + +// Provide a mDNS service, answering incoming DNS-SD and mDNS queries +static int +service_mdns(const char* hostname, const char* service_name, int service_port) { + int sockets[32]; + int num_sockets = open_service_sockets(sockets, sizeof(sockets) / sizeof(sockets[0])); + if (num_sockets <= 0) { + printf("Failed to open any client sockets\n"); + return -1; + } + printf("Opened %d socket%s for mDNS service\n", num_sockets, num_sockets ? "s" : ""); + + size_t service_name_length = strlen(service_name); + if (!service_name_length) { + printf("Invalid service name\n"); + return -1; + } + + char* service_name_buffer = malloc(service_name_length + 2); + memcpy(service_name_buffer, service_name, service_name_length); + if (service_name_buffer[service_name_length - 1] != '.') + service_name_buffer[service_name_length++] = '.'; + service_name_buffer[service_name_length] = 0; + service_name = service_name_buffer; + + printf("Service mDNS: %s:%d\n", service_name, service_port); + printf("Hostname: %s\n", hostname); + + size_t capacity = 2048; + void* buffer = malloc(capacity); + + mdns_string_t service_string = (mdns_string_t){service_name, strlen(service_name)}; + mdns_string_t hostname_string = (mdns_string_t){hostname, strlen(hostname)}; + + // Build the service instance ".<_service-name>._tcp.local." string + char service_instance_buffer[256] = {0}; + snprintf(service_instance_buffer, sizeof(service_instance_buffer) - 1, "%.*s.%.*s", + MDNS_STRING_FORMAT(hostname_string), MDNS_STRING_FORMAT(service_string)); + mdns_string_t service_instance_string = + (mdns_string_t){service_instance_buffer, strlen(service_instance_buffer)}; + + // Build the ".local." string + char qualified_hostname_buffer[256] = {0}; + snprintf(qualified_hostname_buffer, sizeof(qualified_hostname_buffer) - 1, "%.*s.local.", + MDNS_STRING_FORMAT(hostname_string)); + mdns_string_t hostname_qualified_string = + (mdns_string_t){qualified_hostname_buffer, strlen(qualified_hostname_buffer)}; + + service_t service = {0}; + service.service = service_string; + service.hostname = hostname_string; + service.service_instance = service_instance_string; + service.hostname_qualified = hostname_qualified_string; + service.address_ipv4 = service_address_ipv4; + service.address_ipv6 = service_address_ipv6; + service.port = service_port; + + // Setup our mDNS records + + // PTR record reverse mapping "<_service-name>._tcp.local." to + // ".<_service-name>._tcp.local." + service.record_ptr = (mdns_record_t){.name = service.service, + .type = MDNS_RECORDTYPE_PTR, + .data.ptr.name = service.service_instance, + .rclass = 0, + .ttl = 0}; + + // SRV record mapping ".<_service-name>._tcp.local." to + // ".local." with port. Set weight & priority to 0. + service.record_srv = (mdns_record_t){.name = service.service_instance, + .type = MDNS_RECORDTYPE_SRV, + .data.srv.name = service.hostname_qualified, + .data.srv.port = service.port, + .data.srv.priority = 0, + .data.srv.weight = 0, + .rclass = 0, + .ttl = 0}; + + // A/AAAA records mapping ".local." to IPv4/IPv6 addresses + service.record_a = (mdns_record_t){.name = service.hostname_qualified, + .type = MDNS_RECORDTYPE_A, + .data.a.addr = service.address_ipv4, + .rclass = 0, + .ttl = 0}; + + service.record_aaaa = (mdns_record_t){.name = service.hostname_qualified, + .type = MDNS_RECORDTYPE_AAAA, + .data.aaaa.addr = service.address_ipv6, + .rclass = 0, + .ttl = 0}; + + // Add two test TXT records for our service instance name, will be coalesced into + // one record with both key-value pair strings by the library + service.txt_record[0] = (mdns_record_t){.name = service.service_instance, + .type = MDNS_RECORDTYPE_TXT, + .data.txt.key = {MDNS_STRING_CONST("test")}, + .data.txt.value = {MDNS_STRING_CONST("1")}, + .rclass = 0, + .ttl = 0}; + service.txt_record[1] = (mdns_record_t){.name = service.service_instance, + .type = MDNS_RECORDTYPE_TXT, + .data.txt.key = {MDNS_STRING_CONST("other")}, + .data.txt.value = {MDNS_STRING_CONST("value")}, + .rclass = 0, + .ttl = 0}; + + // Send an announcement on startup of service + { + printf("Sending announce\n"); + mdns_record_t additional[5] = {0}; + size_t additional_count = 0; + additional[additional_count++] = service.record_srv; + if (service.address_ipv4.sin_family == AF_INET) + additional[additional_count++] = service.record_a; + if (service.address_ipv6.sin6_family == AF_INET6) + additional[additional_count++] = service.record_aaaa; + additional[additional_count++] = service.txt_record[0]; + additional[additional_count++] = service.txt_record[1]; + + for (int isock = 0; isock < num_sockets; ++isock) + mdns_announce_multicast(sockets[isock], buffer, capacity, service.record_ptr, 0, 0, + additional, additional_count); + } + + // This is a crude implementation that checks for incoming queries + while (running) { + int nfds = 0; + fd_set readfs; + FD_ZERO(&readfs); + for (int isock = 0; isock < num_sockets; ++isock) { + if (sockets[isock] >= nfds) + nfds = sockets[isock] + 1; + FD_SET(sockets[isock], &readfs); + } + + struct timeval timeout; + timeout.tv_sec = 0; + timeout.tv_usec = 100000; + + if (select(nfds, &readfs, 0, 0, &timeout) >= 0) { + for (int isock = 0; isock < num_sockets; ++isock) { + if (FD_ISSET(sockets[isock], &readfs)) { + mdns_socket_listen(sockets[isock], buffer, capacity, service_callback, + &service); + } + FD_SET(sockets[isock], &readfs); + } + } else { + break; + } + } + + // Send a goodbye on end of service + { + printf("Sending goodbye\n"); + mdns_record_t additional[5] = {0}; + size_t additional_count = 0; + additional[additional_count++] = service.record_srv; + if (service.address_ipv4.sin_family == AF_INET) + additional[additional_count++] = service.record_a; + if (service.address_ipv6.sin6_family == AF_INET6) + additional[additional_count++] = service.record_aaaa; + additional[additional_count++] = service.txt_record[0]; + additional[additional_count++] = service.txt_record[1]; + + for (int isock = 0; isock < num_sockets; ++isock) + mdns_goodbye_multicast(sockets[isock], buffer, capacity, service.record_ptr, 0, 0, + additional, additional_count); + } + + free(buffer); + free(service_name_buffer); + + for (int isock = 0; isock < num_sockets; ++isock) + mdns_socket_close(sockets[isock]); + printf("Closed socket%s\n", num_sockets ? "s" : ""); + + return 0; +} + + +// Dump all incoming mDNS queries and answers +static int +dump_mdns(void) { + int sockets[32]; + int num_sockets = open_service_sockets(sockets, sizeof(sockets) / sizeof(sockets[0])); + if (num_sockets <= 0) { + printf("Failed to open any client sockets\n"); + return -1; + } + printf("Opened %d socket%s for mDNS dump\n", num_sockets, num_sockets ? "s" : ""); + + size_t capacity = 2048; + void* buffer = malloc(capacity); + + // This is a crude implementation that checks for incoming queries and answers + while (running) { + int nfds = 0; + fd_set readfs; + FD_ZERO(&readfs); + for (int isock = 0; isock < num_sockets; ++isock) { + if (sockets[isock] >= nfds) + nfds = sockets[isock] + 1; + FD_SET(sockets[isock], &readfs); + } + + struct timeval timeout; + timeout.tv_sec = 0; + timeout.tv_usec = 100000; + + if (select(nfds, &readfs, 0, 0, &timeout) >= 0) { + for (int isock = 0; isock < num_sockets; ++isock) { + if (FD_ISSET(sockets[isock], &readfs)) { + mdns_socket_listen(sockets[isock], buffer, capacity, dump_callback, 0); + } + FD_SET(sockets[isock], &readfs); + } + } else { + break; + } + } + + free(buffer); + + for (int isock = 0; isock < num_sockets; ++isock) + mdns_socket_close(sockets[isock]); + printf("Closed socket%s\n", num_sockets ? "s" : ""); + + return 0; +} + +#ifdef MDNS_FUZZING + +#undef printf + +// Fuzzing by piping random data into the recieve functions +static void +fuzz_mdns(void) { +#define MAX_FUZZ_SIZE 4096 +#define MAX_PASSES (1024 * 1024 * 1024) + + static uint8_t fuzz_mdns_services_query[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, '_', + 's', 'e', 'r', 'v', 'i', 'c', 'e', 's', 0x07, '_', 'd', 'n', 's', '-', + 's', 'd', 0x04, '_', 'u', 'd', 'p', 0x05, 'l', 'o', 'c', 'a', 'l', 0x00}; + + uint8_t* buffer = malloc(MAX_FUZZ_SIZE); + uint8_t* strbuffer = malloc(MAX_FUZZ_SIZE); + for (int ipass = 0; ipass < MAX_PASSES; ++ipass) { + size_t size = rand() % MAX_FUZZ_SIZE; + for (size_t i = 0; i < size; ++i) + buffer[i] = rand() & 0xFF; + + if (ipass % 4) { + // Crafted fuzzing, make sure header is reasonable + memcpy(buffer, fuzz_mdns_services_query, sizeof(fuzz_mdns_services_query)); + uint16_t* header = (uint16_t*)buffer; + header[0] = 0; + header[1] = htons(0x8400); + for (int ival = 2; ival < 6; ++ival) + header[ival] = rand() & 0xFF; + } + mdns_discovery_recv(0, (void*)buffer, size, query_callback, 0); + + mdns_socket_listen(0, (void*)buffer, size, service_callback, 0); + + if (ipass % 4) { + // Crafted fuzzing, make sure header is reasonable (1 question claimed). + // Earlier passes will have done completely random data + uint16_t* header = (uint16_t*)buffer; + header[2] = htons(1); + } + mdns_query_recv(0, (void*)buffer, size, query_callback, 0, 0); + + // Fuzzing by piping random data into the parse functions + size_t offset = size ? (rand() % size) : 0; + size_t length = size ? (rand() % (size - offset)) : 0; + mdns_record_parse_ptr(buffer, size, offset, length, strbuffer, MAX_FUZZ_SIZE); + + offset = size ? (rand() % size) : 0; + length = size ? (rand() % (size - offset)) : 0; + mdns_record_parse_srv(buffer, size, offset, length, strbuffer, MAX_FUZZ_SIZE); + + struct sockaddr_in addr_ipv4; + offset = size ? (rand() % size) : 0; + length = size ? (rand() % (size - offset)) : 0; + mdns_record_parse_a(buffer, size, offset, length, &addr_ipv4); + + struct sockaddr_in6 addr_ipv6; + offset = size ? (rand() % size) : 0; + length = size ? (rand() % (size - offset)) : 0; + mdns_record_parse_aaaa(buffer, size, offset, length, &addr_ipv6); + + offset = size ? (rand() % size) : 0; + length = size ? (rand() % (size - offset)) : 0; + mdns_record_parse_txt(buffer, size, offset, length, (mdns_record_txt_t*)strbuffer, + MAX_FUZZ_SIZE); + + if (ipass && !(ipass % 10000)) + printf("Completed fuzzing pass %d\n", ipass); + } + + free(buffer); + free(strbuffer); +} + +#endif + +#ifdef _WIN32 +BOOL console_handler(DWORD signal) { + if (signal == CTRL_C_EVENT) { + running = 0; + } + return TRUE; +} +#else +void signal_handler(int signal) { + running = 0; +} +#endif + +int +__main(int argc, const char* const* argv) { + int mode = 0; + const char* service = "_test-mdns._tcp.local."; + const char* hostname = "dummy-host"; + mdns_query_t query[16]; + size_t query_count = 0; + int service_port = 42424; + +#ifdef _WIN32 + + WORD versionWanted = MAKEWORD(1, 1); + WSADATA wsaData; + if (WSAStartup(versionWanted, &wsaData)) { + printf("Failed to initialize WinSock\n"); + return -1; + } + + char hostname_buffer[256]; + DWORD hostname_size = (DWORD)sizeof(hostname_buffer); + if (GetComputerNameA(hostname_buffer, &hostname_size)) + hostname = hostname_buffer; + + SetConsoleCtrlHandler(console_handler, TRUE); +#else + + char hostname_buffer[256]; + size_t hostname_size = sizeof(hostname_buffer); + if (gethostname(hostname_buffer, hostname_size) == 0) + hostname = hostname_buffer; + signal(SIGINT, signal_handler); +#endif + + for (int iarg = 0; iarg < argc; ++iarg) { + if (strcmp(argv[iarg], "--discovery") == 0) { + mode = 0; + } else if (strcmp(argv[iarg], "--query") == 0) { + // Each query is either a service name, or a pair of record type and a service name + // For example: + // mdns --query _foo._tcp.local. + // mdns --query SRV myhost._foo._tcp.local. + // mdns --query A myhost._tcp.local. _service._tcp.local. + mode = 1; + ++iarg; + while ((iarg < argc) && (query_count < 16)) { + query[query_count].name = argv[iarg++]; + query[query_count].type = MDNS_RECORDTYPE_PTR; + if (iarg < argc) { + mdns_record_type_t record_type = 0; + if (strcmp(query[query_count].name, "PTR") == 0) + record_type = MDNS_RECORDTYPE_PTR; + else if (strcmp(query[query_count].name, "SRV") == 0) + record_type = MDNS_RECORDTYPE_SRV; + else if (strcmp(query[query_count].name, "A") == 0) + record_type = MDNS_RECORDTYPE_A; + else if (strcmp(query[query_count].name, "AAAA") == 0) + record_type = MDNS_RECORDTYPE_AAAA; + if (record_type != 0) { + query[query_count].type = record_type; + query[query_count].name = argv[iarg++]; + } + } + query[query_count].length = strlen(query[query_count].name); + ++query_count; + } + } else if (strcmp(argv[iarg], "--service") == 0) { + mode = 2; + ++iarg; + if (iarg < argc) + service = argv[iarg]; + } else if (strcmp(argv[iarg], "--dump") == 0) { + mode = 3; + } else if (strcmp(argv[iarg], "--hostname") == 0) { + ++iarg; + if (iarg < argc) + hostname = argv[iarg]; + } else if (strcmp(argv[iarg], "--port") == 0) { + ++iarg; + if (iarg < argc) + service_port = atoi(argv[iarg]); + } + } + +#ifdef MDNS_FUZZING + fuzz_mdns(); +#else + int ret; + if (mode == 0) + ret = send_dns_sd(); + else if (mode == 1) + ret = send_mdns_query(query, query_count); + else if (mode == 2) + ret = service_mdns(hostname, service, service_port); + else if (mode == 3) + ret = dump_mdns(); +#endif + +#ifdef _WIN32 + WSACleanup(); +#endif + + return 0; +} diff --git a/deps_src/mdns/mdns.h b/deps_src/mdns/mdns.h new file mode 100644 index 0000000000..c32fd7f505 --- /dev/null +++ b/deps_src/mdns/mdns.h @@ -0,0 +1,1641 @@ +/* mdns.h - mDNS/DNS-SD library - Public Domain - 2017 Mattias Jansson + * + * This library provides a cross-platform mDNS and DNS-SD library in C. + * The implementation is based on RFC 6762 and RFC 6763. + * + * The latest source code is always available at + * + * https://github.com/mjansson/mdns + * + * This library is put in the public domain; you can redistribute it and/or modify it without any + * restrictions. + * + */ + +#pragma once + +#include +#include +#include +#include + +#include +#ifdef _WIN32 +#include +#include +#define strncasecmp _strnicmp +#else +#include +#include +#include +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#define MDNS_INVALID_POS ((size_t)-1) + +#define MDNS_STRING_CONST(s) (s), (sizeof((s)) - 1) +#define MDNS_STRING_ARGS(s) s.str, s.length +#define MDNS_STRING_FORMAT(s) (int)((s).length), s.str + +#define MDNS_POINTER_OFFSET(p, ofs) ((void*)((char*)(p) + (ptrdiff_t)(ofs))) +#define MDNS_POINTER_OFFSET_CONST(p, ofs) ((const void*)((const char*)(p) + (ptrdiff_t)(ofs))) +#define MDNS_POINTER_DIFF(a, b) ((size_t)((const char*)(a) - (const char*)(b))) + +#define MDNS_PORT 5353 +#define MDNS_UNICAST_RESPONSE 0x8000U +#define MDNS_CACHE_FLUSH 0x8000U +#define MDNS_MAX_SUBSTRINGS 64 + +enum mdns_record_type { + MDNS_RECORDTYPE_IGNORE = 0, + // Address + MDNS_RECORDTYPE_A = 1, + // Domain Name pointer + MDNS_RECORDTYPE_PTR = 12, + // Arbitrary text string + MDNS_RECORDTYPE_TXT = 16, + // IP6 Address [Thomson] + MDNS_RECORDTYPE_AAAA = 28, + // Server Selection [RFC2782] + MDNS_RECORDTYPE_SRV = 33, + // Any available records + MDNS_RECORDTYPE_ANY = 255 +}; + +enum mdns_entry_type { + MDNS_ENTRYTYPE_QUESTION = 0, + MDNS_ENTRYTYPE_ANSWER = 1, + MDNS_ENTRYTYPE_AUTHORITY = 2, + MDNS_ENTRYTYPE_ADDITIONAL = 3 +}; + +enum mdns_class { MDNS_CLASS_IN = 1, MDNS_CLASS_ANY = 255 }; + +typedef enum mdns_record_type mdns_record_type_t; +typedef enum mdns_entry_type mdns_entry_type_t; +typedef enum mdns_class mdns_class_t; + +typedef int (*mdns_record_callback_fn)(int sock, const struct sockaddr* from, size_t addrlen, + mdns_entry_type_t entry, uint16_t query_id, uint16_t rtype, + uint16_t rclass, uint32_t ttl, const void* data, size_t size, + size_t name_offset, size_t name_length, size_t record_offset, + size_t record_length, void* user_data); + +typedef struct mdns_string_t mdns_string_t; +typedef struct mdns_string_pair_t mdns_string_pair_t; +typedef struct mdns_string_table_item_t mdns_string_table_item_t; +typedef struct mdns_string_table_t mdns_string_table_t; +typedef struct mdns_record_t mdns_record_t; +typedef struct mdns_record_srv_t mdns_record_srv_t; +typedef struct mdns_record_ptr_t mdns_record_ptr_t; +typedef struct mdns_record_a_t mdns_record_a_t; +typedef struct mdns_record_aaaa_t mdns_record_aaaa_t; +typedef struct mdns_record_txt_t mdns_record_txt_t; +typedef struct mdns_query_t mdns_query_t; + +#ifdef _WIN32 +typedef int mdns_size_t; +typedef int mdns_ssize_t; +#else +typedef size_t mdns_size_t; +typedef ssize_t mdns_ssize_t; +#endif + +struct mdns_string_t { + const char* str; + size_t length; +}; + +struct mdns_ip_mac_str { + char ip[16] ; + char mac[12]; +}; + +struct mdns_string_pair_t { + size_t offset; + size_t length; + int ref; +}; + +struct mdns_string_table_t { + size_t offset[16]; + size_t count; + size_t next; +}; + +struct mdns_record_srv_t { + uint16_t priority; + uint16_t weight; + uint16_t port; + mdns_string_t name; +}; + +struct mdns_record_ptr_t { + mdns_string_t name; +}; + +struct mdns_record_a_t { + struct sockaddr_in addr; +}; + +struct mdns_record_aaaa_t { + struct sockaddr_in6 addr; +}; + +struct mdns_record_txt_t { + mdns_string_t key; + mdns_string_t value; +}; + +struct mdns_record_t { + mdns_string_t name; + mdns_record_type_t type; + union mdns_record_data { + mdns_record_ptr_t ptr; + mdns_record_srv_t srv; + mdns_record_a_t a; + mdns_record_aaaa_t aaaa; + mdns_record_txt_t txt; + } data; + uint16_t rclass; + uint32_t ttl; +}; + +struct mdns_header_t { + uint16_t query_id; + uint16_t flags; + uint16_t questions; + uint16_t answer_rrs; + uint16_t authority_rrs; + uint16_t additional_rrs; +}; + +struct mdns_query_t { + mdns_record_type_t type; + const char* name; + size_t length; +}; + +void discoveryServiceC(); + +// mDNS/DNS-SD public API + +//! Open and setup a IPv4 socket for mDNS/DNS-SD. To bind the socket to a specific interface, pass +//! in the appropriate socket address in saddr, otherwise pass a null pointer for INADDR_ANY. To +//! send one-shot discovery requests and queries pass a null pointer or set 0 as port to assign a +//! random user level ephemeral port. To run discovery service listening for incoming discoveries +//! and queries, you must set MDNS_PORT as port. +static inline int +mdns_socket_open_ipv4(const struct sockaddr_in* saddr); + +//! Setup an already opened IPv4 socket for mDNS/DNS-SD. To bind the socket to a specific interface, +//! pass in the appropriate socket address in saddr, otherwise pass a null pointer for INADDR_ANY. +//! To send one-shot discovery requests and queries pass a null pointer or set 0 as port to assign a +//! random user level ephemeral port. To run discovery service listening for incoming discoveries +//! and queries, you must set MDNS_PORT as port. +static inline int +mdns_socket_setup_ipv4(int sock, const struct sockaddr_in* saddr); + +//! Open and setup a IPv6 socket for mDNS/DNS-SD. To bind the socket to a specific interface, pass +//! in the appropriate socket address in saddr, otherwise pass a null pointer for in6addr_any. To +//! send one-shot discovery requests and queries pass a null pointer or set 0 as port to assign a +//! random user level ephemeral port. To run discovery service listening for incoming discoveries +//! and queries, you must set MDNS_PORT as port. +static inline int +mdns_socket_open_ipv6(const struct sockaddr_in6* saddr); + +//! Setup an already opened IPv6 socket for mDNS/DNS-SD. To bind the socket to a specific interface, +//! pass in the appropriate socket address in saddr, otherwise pass a null pointer for in6addr_any. +//! To send one-shot discovery requests and queries pass a null pointer or set 0 as port to assign a +//! random user level ephemeral port. To run discovery service listening for incoming discoveries +//! and queries, you must set MDNS_PORT as port. +static inline int +mdns_socket_setup_ipv6(int sock, const struct sockaddr_in6* saddr); + +//! Close a socket opened with mdns_socket_open_ipv4 and mdns_socket_open_ipv6. +static inline void +mdns_socket_close(int sock); + +//! Listen for incoming multicast DNS-SD and mDNS query requests. The socket should have been opened +//! on port MDNS_PORT using one of the mdns open or setup socket functions. Buffer must be 32 bit +//! aligned. Parsing is stopped when callback function returns non-zero. Returns the number of +//! queries parsed. +static inline size_t +mdns_socket_listen(int sock, void* buffer, size_t capacity, mdns_record_callback_fn callback, + void* user_data); + +//! Send a multicast DNS-SD reqeuest on the given socket to discover available services. Returns 0 +//! on success, or <0 if error. +static inline int +mdns_discovery_send(int sock); + +//! Recieve unicast responses to a DNS-SD sent with mdns_discovery_send. Any data will be piped to +//! the given callback for parsing. Buffer must be 32 bit aligned. Parsing is stopped when callback +//! function returns non-zero. Returns the number of responses parsed. +static inline size_t +mdns_discovery_recv(int sock, void* buffer, size_t capacity, mdns_record_callback_fn callback, + void* user_data); + + +//! Send a multicast mDNS query on the given socket for the given service name. The supplied buffer +//! will be used to build the query packet and must be 32 bit aligned. The query ID can be set to +//! non-zero to filter responses, however the RFC states that the query ID SHOULD be set to 0 for +//! multicast queries. The query will request a unicast response if the socket is bound to an +//! ephemeral port, or a multicast response if the socket is bound to mDNS port 5353. Returns the +//! used query ID, or <0 if error. +static inline int +mdns_query_send(int sock, mdns_record_type_t type, const char* name, size_t length, void* buffer, + size_t capacity, uint16_t query_id); + +//! Send a multicast mDNS query on the given socket for the given service names. The supplied buffer +//! will be used to build the query packet and must be 32 bit aligned. The query ID can be set to +//! non-zero to filter responses, however the RFC states that the query ID SHOULD be set to 0 for +//! multicast queries. Each additional service name query consists of a triplet - a record type +//! (mdns_record_type_t), a name string pointer (const char*) and a name length (size_t). The list +//! of variable arguments should be terminated with a record type of 0. The query will request a +//! unicast response if the socket is bound to an ephemeral port, or a multicast response if the +//! socket is bound to mDNS port 5353. Returns the used query ID, or <0 if error. +static inline int +mdns_multiquery_send(int sock, const mdns_query_t* query, size_t count, void* buffer, + size_t capacity, uint16_t query_id); + +//! Receive unicast responses to a mDNS query sent with mdns_discovery_recv, optionally filtering +//! out any responses not matching the given query ID. Set the query ID to 0 to parse all responses, +//! even if it is not matching the query ID set in a specific query. Any data will be piped to the +//! given callback for parsing. Buffer must be 32 bit aligned. Parsing is stopped when callback +//! function returns non-zero. Returns the number of responses parsed. +static inline size_t +mdns_query_recv(int sock, void* buffer, size_t capacity, mdns_record_callback_fn callback, + void* user_data, int query_id); + +//! Send a variable unicast mDNS query answer to any question with variable number of records to the +//! given address. Use the top bit of the query class field (MDNS_UNICAST_RESPONSE) in the query +//! recieved to determine if the answer should be sent unicast (bit set) or multicast (bit not set). +//! Buffer must be 32 bit aligned. The record type and name should match the data from the query +//! recieved. Returns 0 if success, or <0 if error. +static inline int +mdns_query_answer_unicast(int sock, const void* address, size_t address_size, void* buffer, + size_t capacity, uint16_t query_id, mdns_record_type_t record_type, + const char* name, size_t name_length, mdns_record_t answer, + const mdns_record_t* authority, size_t authority_count, + const mdns_record_t* additional, size_t additional_count); + +//! Send a variable multicast mDNS query answer to any question with variable number of records. Use +//! the top bit of the query class field (MDNS_UNICAST_RESPONSE) in the query recieved to determine +//! if the answer should be sent unicast (bit set) or multicast (bit not set). Buffer must be 32 bit +//! aligned. Returns 0 if success, or <0 if error. +static inline int +mdns_query_answer_multicast(int sock, void* buffer, size_t capacity, mdns_record_t answer, + const mdns_record_t* authority, size_t authority_count, + const mdns_record_t* additional, size_t additional_count); + +//! Send a variable multicast mDNS announcement (as an unsolicited answer) with variable number of +//! records.Buffer must be 32 bit aligned. Returns 0 if success, or <0 if error. Use this on service +//! startup to announce your instance to the local network. +static inline int +mdns_announce_multicast(int sock, void* buffer, size_t capacity, mdns_record_t answer, + const mdns_record_t* authority, size_t authority_count, + const mdns_record_t* additional, size_t additional_count); + +//! Send a variable multicast mDNS announcement. Use this on service end for removing the resource +//! from the local network. The records must be identical to the according announcement. +static inline int +mdns_goodbye_multicast(int sock, void* buffer, size_t capacity, mdns_record_t answer, + const mdns_record_t* authority, size_t authority_count, + const mdns_record_t* additional, size_t additional_count); + +// Parse records functions + +//! Parse a PTR record, returns the name in the record +static inline mdns_string_t +mdns_record_parse_ptr(const void* buffer, size_t size, size_t offset, size_t length, + char* strbuffer, size_t capacity); + +//! Parse a SRV record, returns the priority, weight, port and name in the record +static inline mdns_record_srv_t +mdns_record_parse_srv(const void* buffer, size_t size, size_t offset, size_t length, + char* strbuffer, size_t capacity); + +//! Parse an A record, returns the IPv4 address in the record +static inline struct sockaddr_in* +mdns_record_parse_a(const void* buffer, size_t size, size_t offset, size_t length, + struct sockaddr_in* addr); + +//! Parse an AAAA record, returns the IPv6 address in the record +static inline struct sockaddr_in6* +mdns_record_parse_aaaa(const void* buffer, size_t size, size_t offset, size_t length, + struct sockaddr_in6* addr); + +//! Parse a TXT record, returns the number of key=value records parsed and stores the key-value +//! pairs in the supplied buffer +static inline size_t +mdns_record_parse_txt(const void* buffer, size_t size, size_t offset, size_t length, + mdns_record_txt_t* records, size_t capacity); + +// Internal functions + +static inline mdns_string_t +mdns_string_extract(const void* buffer, size_t size, size_t* offset, char* str, size_t capacity); + +static inline int +mdns_string_skip(const void* buffer, size_t size, size_t* offset); + +static inline size_t +mdns_string_find(const char* str, size_t length, char c, size_t offset); + +//! Compare if two strings are equal. If the strings are equal it returns >0 and the offset variables are +//! updated to the end of the corresponding strings. If the strings are not equal it returns 0 and +//! the offset variables are NOT updated. +static inline int +mdns_string_equal(const void* buffer_lhs, size_t size_lhs, size_t* ofs_lhs, const void* buffer_rhs, + size_t size_rhs, size_t* ofs_rhs); + +static inline void* +mdns_string_make(void* buffer, size_t capacity, void* data, const char* name, size_t length, + mdns_string_table_t* string_table); + +static inline size_t +mdns_string_table_find(mdns_string_table_t* string_table, const void* buffer, size_t capacity, + const char* str, size_t first_length, size_t total_length); + +// Implementations + +static inline uint16_t +mdns_ntohs(const void* data) { + uint16_t aligned; + memcpy(&aligned, data, sizeof(uint16_t)); + return ntohs(aligned); +} + +static inline uint32_t +mdns_ntohl(const void* data) { + uint32_t aligned; + memcpy(&aligned, data, sizeof(uint32_t)); + return ntohl(aligned); +} + +static inline void* +mdns_htons(void* data, uint16_t val) { + val = htons(val); + memcpy(data, &val, sizeof(uint16_t)); + return MDNS_POINTER_OFFSET(data, sizeof(uint16_t)); +} + +static inline void* +mdns_htonl(void* data, uint32_t val) { + val = htonl(val); + memcpy(data, &val, sizeof(uint32_t)); + return MDNS_POINTER_OFFSET(data, sizeof(uint32_t)); +} + +static inline int +mdns_socket_open_ipv4(const struct sockaddr_in* saddr) { + int sock = (int)socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (sock < 0) + return -1; + if (mdns_socket_setup_ipv4(sock, saddr)) { + mdns_socket_close(sock); + return -1; + } + return sock; +} +//==========================add +int +send_dns_sd(void); +int +open_client_sockets(int* sockets, int max_sockets, int port); + +mdns_string_t +ipv4_address_to_string(char* buffer, size_t capacity, const struct sockaddr_in* addr, + size_t addrlen); +mdns_string_t +ip_address_to_string(char* buffer, size_t capacity, const struct sockaddr* addr, size_t addrlen); + +int +query_callback(int sock, const struct sockaddr* from, size_t addrlen, mdns_entry_type_t entry, + uint16_t query_id, uint16_t rtype, uint16_t rclass, uint32_t ttl, const void* data, + size_t size, size_t name_offset, size_t name_length, size_t record_offset, + size_t record_length, void* user_data); + + +static inline int +mdns_socket_setup_ipv4(int sock, const struct sockaddr_in* saddr) { + unsigned char ttl = 1; + unsigned char loopback = 1; + unsigned int reuseaddr = 1; + struct ip_mreq req; + + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuseaddr, sizeof(reuseaddr)); +#ifdef SO_REUSEPORT + setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, (const char*)&reuseaddr, sizeof(reuseaddr)); +#endif + setsockopt(sock, IPPROTO_IP, IP_MULTICAST_TTL, (const char*)&ttl, sizeof(ttl)); + setsockopt(sock, IPPROTO_IP, IP_MULTICAST_LOOP, (const char*)&loopback, sizeof(loopback)); + + memset(&req, 0, sizeof(req)); + req.imr_multiaddr.s_addr = htonl((((uint32_t)224U) << 24U) | ((uint32_t)251U)); + if (saddr) + req.imr_interface = saddr->sin_addr; + if (setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char*)&req, sizeof(req))) + return -1; + + struct sockaddr_in sock_addr; + if (!saddr) { + memset(&sock_addr, 0, sizeof(struct sockaddr_in)); + sock_addr.sin_family = AF_INET; + sock_addr.sin_addr.s_addr = INADDR_ANY; +#ifdef __APPLE__ + sock_addr.sin_len = sizeof(struct sockaddr_in); +#endif + } else { + memcpy(&sock_addr, saddr, sizeof(struct sockaddr_in)); + setsockopt(sock, IPPROTO_IP, IP_MULTICAST_IF, (const char*)&sock_addr.sin_addr, + sizeof(sock_addr.sin_addr)); +#ifndef _WIN32 + sock_addr.sin_addr.s_addr = INADDR_ANY; +#endif + } + + if (bind(sock, (struct sockaddr*)&sock_addr, sizeof(struct sockaddr_in))) + return -1; + +#ifdef _WIN32 + unsigned long param = 1; + ioctlsocket(sock, FIONBIO, ¶m); +#else + const int flags = fcntl(sock, F_GETFL, 0); + fcntl(sock, F_SETFL, flags | O_NONBLOCK); +#endif + + return 0; +} + +static inline int +mdns_socket_open_ipv6(const struct sockaddr_in6* saddr) { + int sock = (int)socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP); + if (sock < 0) + return -1; + if (mdns_socket_setup_ipv6(sock, saddr)) { + mdns_socket_close(sock); + return -1; + } + return sock; +} + +static inline int +mdns_socket_setup_ipv6(int sock, const struct sockaddr_in6* saddr) { + int hops = 1; + unsigned int loopback = 1; + unsigned int reuseaddr = 1; + struct ipv6_mreq req; + + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuseaddr, sizeof(reuseaddr)); +#ifdef SO_REUSEPORT + setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, (const char*)&reuseaddr, sizeof(reuseaddr)); +#endif + setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, (const char*)&hops, sizeof(hops)); + setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, (const char*)&loopback, sizeof(loopback)); + + memset(&req, 0, sizeof(req)); + req.ipv6mr_multiaddr.s6_addr[0] = 0xFF; + req.ipv6mr_multiaddr.s6_addr[1] = 0x02; + req.ipv6mr_multiaddr.s6_addr[15] = 0xFB; + if (setsockopt(sock, IPPROTO_IPV6, IPV6_JOIN_GROUP, (char*)&req, sizeof(req))) + return -1; + + struct sockaddr_in6 sock_addr; + if (!saddr) { + memset(&sock_addr, 0, sizeof(struct sockaddr_in6)); + sock_addr.sin6_family = AF_INET6; + sock_addr.sin6_addr = in6addr_any; +#ifdef __APPLE__ + sock_addr.sin6_len = sizeof(struct sockaddr_in6); +#endif + } else { + memcpy(&sock_addr, saddr, sizeof(struct sockaddr_in6)); + unsigned int ifindex = 0; + setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_IF, (const char*)&ifindex, sizeof(ifindex)); +#ifndef _WIN32 + sock_addr.sin6_addr = in6addr_any; +#endif + } + + if (bind(sock, (struct sockaddr*)&sock_addr, sizeof(struct sockaddr_in6))) + return -1; + +#ifdef _WIN32 + unsigned long param = 1; + ioctlsocket(sock, FIONBIO, ¶m); +#else + const int flags = fcntl(sock, F_GETFL, 0); + fcntl(sock, F_SETFL, flags | O_NONBLOCK); +#endif + + return 0; +} + +static inline void +mdns_socket_close(int sock) { +#ifdef _WIN32 + closesocket(sock); +#else + close(sock); +#endif +} + +static inline int +mdns_is_string_ref(uint8_t val) { + return (0xC0 == (val & 0xC0)); +} + +static inline mdns_string_pair_t +mdns_get_next_substring(const void* rawdata, size_t size, size_t offset) { + const uint8_t* buffer = (const uint8_t*)rawdata; + mdns_string_pair_t pair = {MDNS_INVALID_POS, 0, 0}; + if (offset >= size) + return pair; + if (!buffer[offset]) { + pair.offset = offset; + return pair; + } + int recursion = 0; + while (mdns_is_string_ref(buffer[offset])) { + if (size < offset + 2) + return pair; + + offset = mdns_ntohs(MDNS_POINTER_OFFSET(buffer, offset)) & 0x3fff; + if (offset >= size) + return pair; + + pair.ref = 1; + if (++recursion > 16) + return pair; + } + + size_t length = (size_t)buffer[offset++]; + if (size < offset + length) + return pair; + + pair.offset = offset; + pair.length = length; + + return pair; +} + +static inline int +mdns_string_skip(const void* buffer, size_t size, size_t* offset) { + size_t cur = *offset; + mdns_string_pair_t substr; + unsigned int counter = 0; + do { + substr = mdns_get_next_substring(buffer, size, cur); + if ((substr.offset == MDNS_INVALID_POS) || (counter++ > MDNS_MAX_SUBSTRINGS)) + return 0; + if (substr.ref) { + *offset = cur + 2; + return 1; + } + cur = substr.offset + substr.length; + } while (substr.length); + + *offset = cur + 1; + return 1; +} + +static inline int +mdns_string_equal(const void* buffer_lhs, size_t size_lhs, size_t* ofs_lhs, const void* buffer_rhs, + size_t size_rhs, size_t* ofs_rhs) { + size_t lhs_cur = *ofs_lhs; + size_t rhs_cur = *ofs_rhs; + size_t lhs_end = MDNS_INVALID_POS; + size_t rhs_end = MDNS_INVALID_POS; + mdns_string_pair_t lhs_substr; + mdns_string_pair_t rhs_substr; + unsigned int counter = 0; + do { + lhs_substr = mdns_get_next_substring(buffer_lhs, size_lhs, lhs_cur); + rhs_substr = mdns_get_next_substring(buffer_rhs, size_rhs, rhs_cur); + if ((lhs_substr.offset == MDNS_INVALID_POS) || (rhs_substr.offset == MDNS_INVALID_POS) || + (counter++ > MDNS_MAX_SUBSTRINGS)) + return 0; + if (lhs_substr.length != rhs_substr.length) + return 0; + if (strncasecmp((const char*)MDNS_POINTER_OFFSET_CONST(buffer_rhs, rhs_substr.offset), + (const char*)MDNS_POINTER_OFFSET_CONST(buffer_lhs, lhs_substr.offset), + rhs_substr.length)) + return 0; + if (lhs_substr.ref && (lhs_end == MDNS_INVALID_POS)) + lhs_end = lhs_cur + 2; + if (rhs_substr.ref && (rhs_end == MDNS_INVALID_POS)) + rhs_end = rhs_cur + 2; + lhs_cur = lhs_substr.offset + lhs_substr.length; + rhs_cur = rhs_substr.offset + rhs_substr.length; + } while (lhs_substr.length); + + if (lhs_end == MDNS_INVALID_POS) + lhs_end = lhs_cur + 1; + *ofs_lhs = lhs_end; + + if (rhs_end == MDNS_INVALID_POS) + rhs_end = rhs_cur + 1; + *ofs_rhs = rhs_end; + + return 1; +} + +static inline mdns_string_t +mdns_string_extract(const void* buffer, size_t size, size_t* offset, char* str, size_t capacity) { + size_t cur = *offset; + size_t end = MDNS_INVALID_POS; + mdns_string_pair_t substr; + mdns_string_t result; + result.str = str; + result.length = 0; + char* dst = str; + unsigned int counter = 0; + size_t remain = capacity; + do { + substr = mdns_get_next_substring(buffer, size, cur); + if ((substr.offset == MDNS_INVALID_POS) || (counter++ > MDNS_MAX_SUBSTRINGS)) + return result; + if (substr.ref && (end == MDNS_INVALID_POS)) + end = cur + 2; + if (substr.length) { + size_t to_copy = (substr.length < remain) ? substr.length : remain; + memcpy(dst, (const char*)buffer + substr.offset, to_copy); + dst += to_copy; + remain -= to_copy; + if (remain) { + *dst++ = '.'; + --remain; + } + } + cur = substr.offset + substr.length; + } while (substr.length); + + if (end == MDNS_INVALID_POS) + end = cur + 1; + *offset = end; + + result.length = capacity - remain; + return result; +} + +static inline size_t +mdns_string_table_find(mdns_string_table_t* string_table, const void* buffer, size_t capacity, + const char* str, size_t first_length, size_t total_length) { + if (!string_table) + return MDNS_INVALID_POS; + + for (size_t istr = 0; istr < string_table->count; ++istr) { + if (string_table->offset[istr] >= capacity) + continue; + size_t offset = 0; + mdns_string_pair_t sub_string = + mdns_get_next_substring(buffer, capacity, string_table->offset[istr]); + if (!sub_string.length || (sub_string.length != first_length)) + continue; + if (memcmp(str, MDNS_POINTER_OFFSET(buffer, sub_string.offset), sub_string.length)) + continue; + + // Initial substring matches, now match all remaining substrings + offset += first_length + 1; + while (offset < total_length) { + size_t dot_pos = mdns_string_find(str, total_length, '.', offset); + if (dot_pos == MDNS_INVALID_POS) + dot_pos = total_length; + size_t current_length = dot_pos - offset; + + sub_string = + mdns_get_next_substring(buffer, capacity, sub_string.offset + sub_string.length); + if (!sub_string.length || (sub_string.length != current_length)) + break; + if (memcmp(str + offset, MDNS_POINTER_OFFSET(buffer, sub_string.offset), + sub_string.length)) + break; + + offset = dot_pos + 1; + } + + // Return reference offset if entire string matches + if (offset >= total_length) + return string_table->offset[istr]; + } + + return MDNS_INVALID_POS; +} + +static inline void +mdns_string_table_add(mdns_string_table_t* string_table, size_t offset) { + if (!string_table) + return; + + string_table->offset[string_table->next] = offset; + + size_t table_capacity = sizeof(string_table->offset) / sizeof(string_table->offset[0]); + if (++string_table->count > table_capacity) + string_table->count = table_capacity; + if (++string_table->next >= table_capacity) + string_table->next = 0; +} + +static inline size_t +mdns_string_find(const char* str, size_t length, char c, size_t offset) { + const void* found; + if (offset >= length) + return MDNS_INVALID_POS; + found = memchr(str + offset, c, length - offset); + if (found) + return (size_t)MDNS_POINTER_DIFF(found, str); + return MDNS_INVALID_POS; +} + +static inline void* +mdns_string_make_ref(void* data, size_t capacity, size_t ref_offset) { + if (capacity < 2) + return 0; + return mdns_htons(data, 0xC000 | (uint16_t)ref_offset); +} + +static inline void* +mdns_string_make(void* buffer, size_t capacity, void* data, const char* name, size_t length, + mdns_string_table_t* string_table) { + size_t last_pos = 0; + size_t remain = capacity - MDNS_POINTER_DIFF(data, buffer); + if (name[length - 1] == '.') + --length; + while (last_pos < length) { + size_t pos = mdns_string_find(name, length, '.', last_pos); + size_t sub_length = ((pos != MDNS_INVALID_POS) ? pos : length) - last_pos; + size_t total_length = length - last_pos; + + size_t ref_offset = + mdns_string_table_find(string_table, buffer, capacity, + (char*)MDNS_POINTER_OFFSET(name, last_pos), sub_length, + total_length); + if (ref_offset != MDNS_INVALID_POS) + return mdns_string_make_ref(data, remain, ref_offset); + + if (remain <= (sub_length + 1)) + return 0; + + *(unsigned char*)data = (unsigned char)sub_length; + memcpy(MDNS_POINTER_OFFSET(data, 1), name + last_pos, sub_length); + mdns_string_table_add(string_table, MDNS_POINTER_DIFF(data, buffer)); + + data = MDNS_POINTER_OFFSET(data, sub_length + 1); + last_pos = ((pos != MDNS_INVALID_POS) ? pos + 1 : length); + remain = capacity - MDNS_POINTER_DIFF(data, buffer); + } + + if (!remain) + return 0; + + *(unsigned char*)data = 0; + return MDNS_POINTER_OFFSET(data, 1); +} + +static inline size_t +mdns_records_parse(int sock, const struct sockaddr* from, size_t addrlen, const void* buffer, + size_t size, size_t* offset, mdns_entry_type_t type, uint16_t query_id, + size_t records, mdns_record_callback_fn callback, void* user_data) { + size_t parsed = 0; + for (size_t i = 0; i < records; ++i) { + size_t name_offset = *offset; + mdns_string_skip(buffer, size, offset); + if (((*offset) + 10) > size) + return parsed; + size_t name_length = (*offset) - name_offset; + const uint16_t* data = (const uint16_t*)MDNS_POINTER_OFFSET(buffer, *offset); + + uint16_t rtype = mdns_ntohs(data++); + uint16_t rclass = mdns_ntohs(data++); + uint32_t ttl = mdns_ntohl(data); + data += 2; + uint16_t length = mdns_ntohs(data++); + + *offset += 10; + + if (length <= (size - (*offset))) { + ++parsed; + if (callback && + callback(sock, from, addrlen, type, query_id, rtype, rclass, ttl, buffer, size, + name_offset, name_length, *offset, length, user_data)) + break; + } + + *offset += length; + } + return parsed; +} + +static inline int +mdns_unicast_send(int sock, const void* address, size_t address_size, const void* buffer, + size_t size) { + if (sendto(sock, (const char*)buffer, (mdns_size_t)size, 0, (const struct sockaddr*)address, + (socklen_t)address_size) < 0) + return -1; + return 0; +} + +static inline int +mdns_multicast_send(int sock, const void* buffer, size_t size) { + struct sockaddr_storage addr_storage; + struct sockaddr_in addr; + struct sockaddr_in6 addr6; + struct sockaddr* saddr = (struct sockaddr*)&addr_storage; + socklen_t saddrlen = sizeof(struct sockaddr_storage); + if (getsockname(sock, saddr, &saddrlen)) + return -1; + if (saddr->sa_family == AF_INET6) { + memset(&addr6, 0, sizeof(addr6)); + addr6.sin6_family = AF_INET6; +#ifdef __APPLE__ + addr6.sin6_len = sizeof(addr6); +#endif + addr6.sin6_addr.s6_addr[0] = 0xFF; + addr6.sin6_addr.s6_addr[1] = 0x02; + addr6.sin6_addr.s6_addr[15] = 0xFB; + addr6.sin6_port = htons((unsigned short)MDNS_PORT); + saddr = (struct sockaddr*)&addr6; + saddrlen = sizeof(addr6); + } else { + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; +#ifdef __APPLE__ + addr.sin_len = sizeof(addr); +#endif + addr.sin_addr.s_addr = htonl((((uint32_t)224U) << 24U) | ((uint32_t)251U)); + addr.sin_port = htons((unsigned short)MDNS_PORT); + saddr = (struct sockaddr*)&addr; + saddrlen = sizeof(addr); + } + + if (sendto(sock, (const char*)buffer, (mdns_size_t)size, 0, saddr, saddrlen) < 0) + return -1; + return 0; +} + +static const uint8_t mdns_services_query[] = { + // Query ID + 0x00, 0x00, + // Flags + 0x00, 0x00, + // 1 question + 0x00, 0x01, + // No answer, authority or additional RRs + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // _services._dns-sd._udp.local. + 0x09, '_', 's', 'e', 'r', 'v', 'i', 'c', 'e', 's', 0x07, '_', 'd', 'n', 's', '-', 's', 'd', + 0x04, '_', 'u', 'd', 'p', 0x05, 'l', 'o', 'c', 'a', 'l', 0x00, + // PTR record + 0x00, MDNS_RECORDTYPE_PTR, + // QU (unicast response) and class IN + 0x80, MDNS_CLASS_IN}; + +static inline int +mdns_discovery_send(int sock) { + return mdns_multicast_send(sock, mdns_services_query, sizeof(mdns_services_query)); +} + + +static inline size_t +mdns_discovery_recv(int sock, void* buffer, size_t capacity, mdns_record_callback_fn callback, + void* user_data) { + struct sockaddr_in6 addr; + struct sockaddr* saddr = (struct sockaddr*)&addr; + socklen_t addrlen = sizeof(addr); + memset(&addr, 0, sizeof(addr)); +#ifdef __APPLE__ + saddr->sa_len = sizeof(addr); +#endif + mdns_ssize_t ret = recvfrom(sock, (char*)buffer, (mdns_size_t)capacity, 0, saddr, &addrlen); + if (ret <= 0) + return 0; + + size_t data_size = (size_t)ret; + size_t records = 0; + const uint16_t* data = (uint16_t*)buffer; + + uint16_t query_id = mdns_ntohs(data++); + uint16_t flags = mdns_ntohs(data++); + uint16_t questions = mdns_ntohs(data++); + uint16_t answer_rrs = mdns_ntohs(data++); + uint16_t authority_rrs = mdns_ntohs(data++); + uint16_t additional_rrs = mdns_ntohs(data++); + + // According to RFC 6762 the query ID MUST match the sent query ID (which is 0 in our case) + if (query_id || (flags != 0x8400)) + return 0; // Not a reply to our question + + // It seems some implementations do not fill the correct questions field, + // so ignore this check for now and only validate answer string + // if (questions != 1) + // return 0; + + int i; + for (i = 0; i < questions; ++i) { + size_t offset = MDNS_POINTER_DIFF(data, buffer); + size_t verify_offset = 12; + // Verify it's our question, _services._dns-sd._udp.local. + if (!mdns_string_equal(buffer, data_size, &offset, mdns_services_query, + sizeof(mdns_services_query), &verify_offset)) + return 0; + data = (const uint16_t*)MDNS_POINTER_OFFSET(buffer, offset); + + uint16_t rtype = mdns_ntohs(data++); + uint16_t rclass = mdns_ntohs(data++); + + // Make sure we get a reply based on our PTR question for class IN + if ((rtype != MDNS_RECORDTYPE_PTR) || ((rclass & 0x7FFF) != MDNS_CLASS_IN)) + return 0; + } + + for (i = 0; i < answer_rrs; ++i) { + size_t offset = MDNS_POINTER_DIFF(data, buffer); + size_t verify_offset = 12; + // Verify it's an answer to our question, _services._dns-sd._udp.local. + size_t name_offset = offset; + int is_answer = mdns_string_equal(buffer, data_size, &offset, mdns_services_query, + sizeof(mdns_services_query), &verify_offset); + if (!is_answer && !mdns_string_skip(buffer, data_size, &offset)) + break; + size_t name_length = offset - name_offset; + if ((offset + 10) > data_size) + return records; + data = (const uint16_t*)MDNS_POINTER_OFFSET(buffer, offset); + + uint16_t rtype = mdns_ntohs(data++); + uint16_t rclass = mdns_ntohs(data++); + uint32_t ttl = mdns_ntohl(data); + data += 2; + uint16_t length = mdns_ntohs(data++); + if (length > (data_size - offset)) + return 0; + + if (is_answer) { + ++records; + offset = MDNS_POINTER_DIFF(data, buffer); + if (callback && + callback(sock, saddr, addrlen, MDNS_ENTRYTYPE_ANSWER, query_id, rtype, rclass, ttl, + buffer, data_size, name_offset, name_length, offset, length, user_data)) + return records; + } + data = (const uint16_t*)MDNS_POINTER_OFFSET_CONST(data, length); + } + + size_t total_records = records; + size_t offset = MDNS_POINTER_DIFF(data, buffer); + records = + mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, + MDNS_ENTRYTYPE_AUTHORITY, query_id, authority_rrs, callback, user_data); + total_records += records; + if (records != authority_rrs) + return total_records; + + records = mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, + MDNS_ENTRYTYPE_ADDITIONAL, query_id, additional_rrs, callback, + user_data); + total_records += records; + if (records != additional_rrs) + return total_records; + + return total_records; +} + +static inline size_t +mdns_socket_listen(int sock, void* buffer, size_t capacity, mdns_record_callback_fn callback, + void* user_data) { + struct sockaddr_in6 addr; + struct sockaddr* saddr = (struct sockaddr*)&addr; + socklen_t addrlen = sizeof(addr); + memset(&addr, 0, sizeof(addr)); +#ifdef __APPLE__ + saddr->sa_len = sizeof(addr); +#endif + mdns_ssize_t ret = recvfrom(sock, (char*)buffer, (mdns_size_t)capacity, 0, saddr, &addrlen); + if (ret <= 0) + return 0; + + size_t data_size = (size_t)ret; + const uint16_t* data = (const uint16_t*)buffer; + + uint16_t query_id = mdns_ntohs(data++); + uint16_t flags = mdns_ntohs(data++); + uint16_t questions = mdns_ntohs(data++); + uint16_t answer_rrs = mdns_ntohs(data++); + uint16_t authority_rrs = mdns_ntohs(data++); + uint16_t additional_rrs = mdns_ntohs(data++); + + size_t records; + size_t total_records = 0; + for (int iquestion = 0; iquestion < questions; ++iquestion) { + size_t question_offset = MDNS_POINTER_DIFF(data, buffer); + size_t offset = question_offset; + size_t verify_offset = 12; + int dns_sd = 0; + if (mdns_string_equal(buffer, data_size, &offset, mdns_services_query, + sizeof(mdns_services_query), &verify_offset)) { + dns_sd = 1; + } else if (!mdns_string_skip(buffer, data_size, &offset)) { + break; + } + size_t length = offset - question_offset; + data = (const uint16_t*)MDNS_POINTER_OFFSET_CONST(buffer, offset); + + uint16_t rtype = mdns_ntohs(data++); + uint16_t rclass = mdns_ntohs(data++); + uint16_t class_without_flushbit = rclass & ~MDNS_CACHE_FLUSH; + + // Make sure we get a question of class IN or ANY + if (!((class_without_flushbit == MDNS_CLASS_IN) || + (class_without_flushbit == MDNS_CLASS_ANY))) { + break; + } + + if (dns_sd && flags) + continue; + + ++total_records; + if (callback && callback(sock, saddr, addrlen, MDNS_ENTRYTYPE_QUESTION, query_id, rtype, + rclass, 0, buffer, data_size, question_offset, length, + question_offset, length, user_data)) + return total_records; + } + + size_t offset = MDNS_POINTER_DIFF(data, buffer); + records = mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, + MDNS_ENTRYTYPE_ANSWER, query_id, answer_rrs, callback, user_data); + total_records += records; + if (records != answer_rrs) + return total_records; + + records = + mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, + MDNS_ENTRYTYPE_AUTHORITY, query_id, authority_rrs, callback, user_data); + total_records += records; + if (records != authority_rrs) + return total_records; + + records = mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, + MDNS_ENTRYTYPE_ADDITIONAL, query_id, additional_rrs, callback, + user_data); + + return total_records; +} + +static inline int +mdns_query_send(int sock, mdns_record_type_t type, const char* name, size_t length, void* buffer, + size_t capacity, uint16_t query_id) { + mdns_query_t query; + query.type = type; + query.name = name; + query.length = length; + return mdns_multiquery_send(sock, &query, 1, buffer, capacity, query_id); +} + +static inline int +mdns_multiquery_send(int sock, const mdns_query_t* query, size_t count, void* buffer, size_t capacity, + uint16_t query_id) { + if (!count || (capacity < (sizeof(struct mdns_header_t) + (6 * count)))) + return -1; + + // Ask for a unicast response since it's a one-shot query + uint16_t rclass = MDNS_CLASS_IN | MDNS_UNICAST_RESPONSE; + + struct sockaddr_storage addr_storage; + struct sockaddr* saddr = (struct sockaddr*)&addr_storage; + socklen_t saddrlen = sizeof(addr_storage); + if (getsockname(sock, saddr, &saddrlen) == 0) { + if ((saddr->sa_family == AF_INET) && + (ntohs(((struct sockaddr_in*)saddr)->sin_port) == MDNS_PORT)) + rclass &= ~MDNS_UNICAST_RESPONSE; + else if ((saddr->sa_family == AF_INET6) && + (ntohs(((struct sockaddr_in6*)saddr)->sin6_port) == MDNS_PORT)) + rclass &= ~MDNS_UNICAST_RESPONSE; + } + + struct mdns_header_t* header = (struct mdns_header_t*)buffer; + // Query ID + header->query_id = htons((unsigned short)query_id); + // Flags + header->flags = 0; + // Questions + header->questions = htons((unsigned short)count); + // No answer, authority or additional RRs + header->answer_rrs = 0; + header->authority_rrs = 0; + header->additional_rrs = 0; + // Fill in questions + void* data = MDNS_POINTER_OFFSET(buffer, sizeof(struct mdns_header_t)); + for (size_t iq = 0; iq < count; ++iq) { + // Name string + data = mdns_string_make(buffer, capacity, data, query[iq].name, query[iq].length, 0); + if (!data) + return -1; + // Record type + data = mdns_htons(data, query[iq].type); + //! Optional unicast response based on local port, class IN + data = mdns_htons(data, rclass); + } + + size_t tosend = MDNS_POINTER_DIFF(data, buffer); + if (mdns_multicast_send(sock, buffer, (size_t)tosend)) + return -1; + return query_id; +} + +static inline size_t +mdns_query_recv(int sock, void* buffer, size_t capacity, mdns_record_callback_fn callback, + void* user_data, int only_query_id) { + struct sockaddr_in6 addr; + struct sockaddr* saddr = (struct sockaddr*)&addr; + socklen_t addrlen = sizeof(addr); + memset(&addr, 0, sizeof(addr)); +#ifdef __APPLE__ + saddr->sa_len = sizeof(addr); +#endif + mdns_ssize_t ret = recvfrom(sock, (char*)buffer, (mdns_size_t)capacity, 0, saddr, &addrlen); + if (ret <= 0) + return 0; + + size_t data_size = (size_t)ret; + const uint16_t* data = (const uint16_t*)buffer; + + uint16_t query_id = mdns_ntohs(data++); + uint16_t flags = mdns_ntohs(data++); + uint16_t questions = mdns_ntohs(data++); + uint16_t answer_rrs = mdns_ntohs(data++); + uint16_t authority_rrs = mdns_ntohs(data++); + uint16_t additional_rrs = mdns_ntohs(data++); + (void)sizeof(flags); + + if ((only_query_id > 0) && (query_id != only_query_id)) + return 0; // Not a reply to the wanted one-shot query + + if (questions > 1) + return 0; + + // Skip questions part + int i; + for (i = 0; i < questions; ++i) { + size_t offset = MDNS_POINTER_DIFF(data, buffer); + if (!mdns_string_skip(buffer, data_size, &offset)) + return 0; + data = (const uint16_t*)MDNS_POINTER_OFFSET_CONST(buffer, offset); + // Record type and class not used, skip + // uint16_t rtype = mdns_ntohs(data++); + // uint16_t rclass = mdns_ntohs(data++); + data += 2; + } + + size_t records = 0; + size_t total_records = 0; + size_t offset = MDNS_POINTER_DIFF(data, buffer); + records = mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, + MDNS_ENTRYTYPE_ANSWER, query_id, answer_rrs, callback, user_data); + total_records += records; + if (records != answer_rrs) + return total_records; + + records = + mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, + MDNS_ENTRYTYPE_AUTHORITY, query_id, authority_rrs, callback, user_data); + total_records += records; + if (records != authority_rrs) + return total_records; + + records = mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, + MDNS_ENTRYTYPE_ADDITIONAL, query_id, additional_rrs, callback, + user_data); + total_records += records; + if (records != additional_rrs) + return total_records; + + return total_records; +} + +static inline void* +mdns_answer_add_question_unicast(void* buffer, size_t capacity, void* data, + mdns_record_type_t record_type, const char* name, + size_t name_length, mdns_string_table_t* string_table) { + data = mdns_string_make(buffer, capacity, data, name, name_length, string_table); + if (!data) + return 0; + size_t remain = capacity - MDNS_POINTER_DIFF(data, buffer); + if (remain < 4) + return 0; + + data = mdns_htons(data, record_type); + data = mdns_htons(data, MDNS_UNICAST_RESPONSE | MDNS_CLASS_IN); + + return data; +} + +static inline void* +mdns_answer_add_record_header(void* buffer, size_t capacity, void* data, mdns_record_t record, + mdns_string_table_t* string_table) { + data = mdns_string_make(buffer, capacity, data, record.name.str, record.name.length, string_table); + if (!data) + return 0; + size_t remain = capacity - MDNS_POINTER_DIFF(data, buffer); + if (remain < 10) + return 0; + + data = mdns_htons(data, record.type); + data = mdns_htons(data, record.rclass); + data = mdns_htonl(data, record.ttl); + data = mdns_htons(data, 0); // Length, to be filled later + return data; +} + +static inline void* +mdns_answer_add_record(void* buffer, size_t capacity, void* data, mdns_record_t record, + mdns_string_table_t* string_table) { + // TXT records will be coalesced into one record later + if (!data || (record.type == MDNS_RECORDTYPE_TXT)) + return data; + + data = mdns_answer_add_record_header(buffer, capacity, data, record, string_table); + if (!data) + return 0; + + // Pointer to length of record to be filled at end + void* record_length = MDNS_POINTER_OFFSET(data, -2); + void* record_data = data; + + size_t remain = capacity - MDNS_POINTER_DIFF(data, buffer); + switch (record.type) { + case MDNS_RECORDTYPE_PTR: + data = mdns_string_make(buffer, capacity, data, record.data.ptr.name.str, + record.data.ptr.name.length, string_table); + break; + + case MDNS_RECORDTYPE_SRV: + if (remain <= 6) + return 0; + data = mdns_htons(data, record.data.srv.priority); + data = mdns_htons(data, record.data.srv.weight); + data = mdns_htons(data, record.data.srv.port); + data = mdns_string_make(buffer, capacity, data, record.data.srv.name.str, + record.data.srv.name.length, string_table); + break; + + case MDNS_RECORDTYPE_A: + if (remain < 4) + return 0; + memcpy(data, &record.data.a.addr.sin_addr.s_addr, 4); + data = MDNS_POINTER_OFFSET(data, 4); + break; + + case MDNS_RECORDTYPE_AAAA: + if (remain < 16) + return 0; + memcpy(data, &record.data.aaaa.addr.sin6_addr, 16); // ipv6 address + data = MDNS_POINTER_OFFSET(data, 16); + break; + + default: + break; + } + + if (!data) + return 0; + + // Fill record length + mdns_htons(record_length, (uint16_t)MDNS_POINTER_DIFF(data, record_data)); + return data; +} + +static inline void +mdns_record_update_rclass_ttl(mdns_record_t* record, uint16_t rclass, uint32_t ttl) { + if (!record->rclass) + record->rclass = rclass; + if (!record->ttl || !ttl) + record->ttl = ttl; + record->rclass &= (uint16_t)(MDNS_CLASS_IN | MDNS_CACHE_FLUSH); + // Never flush PTR record + if (record->type == MDNS_RECORDTYPE_PTR) + record->rclass &= ~(uint16_t)MDNS_CACHE_FLUSH; +} + +static inline void* +mdns_answer_add_txt_record(void* buffer, size_t capacity, void* data, const mdns_record_t* records, + size_t record_count, uint16_t rclass, uint32_t ttl, + mdns_string_table_t* string_table) { + // Pointer to length of record to be filled at end + void* record_length = 0; + void* record_data = 0; + + size_t remain = 0; + for (size_t irec = 0; data && (irec < record_count); ++irec) { + if (records[irec].type != MDNS_RECORDTYPE_TXT) + continue; + + mdns_record_t record = records[irec]; + mdns_record_update_rclass_ttl(&record, rclass, ttl); + if (!record_data) { + data = mdns_answer_add_record_header(buffer, capacity, data, record, string_table); + if (!data) + return data; + record_length = MDNS_POINTER_OFFSET(data, -2); + record_data = data; + } + + // TXT strings are unlikely to be shared, just make then raw. Also need one byte for + // termination, thus the <= check + size_t string_length = record.data.txt.key.length + record.data.txt.value.length + 1; + if (!data) + return 0; + remain = capacity - MDNS_POINTER_DIFF(data, buffer); + if ((remain <= string_length) || (string_length > 0x3FFF)) + return 0; + + unsigned char* strdata = (unsigned char*)data; + *strdata++ = (unsigned char)string_length; + memcpy(strdata, record.data.txt.key.str, record.data.txt.key.length); + strdata += record.data.txt.key.length; + *strdata++ = '='; + memcpy(strdata, record.data.txt.value.str, record.data.txt.value.length); + strdata += record.data.txt.value.length; + + data = strdata; + } + + // Fill record length + if (record_data) + mdns_htons(record_length, (uint16_t)MDNS_POINTER_DIFF(data, record_data)); + + return data; +} + +static inline uint16_t +mdns_answer_get_record_count(const mdns_record_t* records, size_t record_count) { + // TXT records will be coalesced into one record + uint16_t total_count = 0; + uint16_t txt_record = 0; + for (size_t irec = 0; irec < record_count; ++irec) { + if (records[irec].type == MDNS_RECORDTYPE_TXT) + txt_record = 1; + else + ++total_count; + } + return total_count + txt_record; +} + +static inline int +mdns_query_answer_unicast(int sock, const void* address, size_t address_size, void* buffer, + size_t capacity, uint16_t query_id, mdns_record_type_t record_type, + const char* name, size_t name_length, mdns_record_t answer, + const mdns_record_t* authority, size_t authority_count, + const mdns_record_t* additional, size_t additional_count) { + if (capacity < (sizeof(struct mdns_header_t) + 32 + 4)) + return -1; + + // According to RFC 6762: + // The cache-flush bit MUST NOT be set in any resource records in a response message + // sent in legacy unicast responses to UDP ports other than 5353. + uint16_t rclass = MDNS_CLASS_IN; + uint32_t ttl = 10; + + // Basic answer structure + struct mdns_header_t* header = (struct mdns_header_t*)buffer; + header->query_id = htons(query_id); + header->flags = htons(0x8400); + header->questions = htons(1); + header->answer_rrs = htons(1); + header->authority_rrs = htons(mdns_answer_get_record_count(authority, authority_count)); + header->additional_rrs = htons(mdns_answer_get_record_count(additional, additional_count)); + + mdns_string_table_t string_table = {{0}, 0, 0}; + void* data = MDNS_POINTER_OFFSET(buffer, sizeof(struct mdns_header_t)); + + // Fill in question + data = mdns_answer_add_question_unicast(buffer, capacity, data, record_type, name, name_length, + &string_table); + + // Fill in answer + answer.rclass = rclass; + answer.ttl = ttl; + data = mdns_answer_add_record(buffer, capacity, data, answer, &string_table); + + // Fill in authority records + for (size_t irec = 0; data && (irec < authority_count); ++irec) { + mdns_record_t record = authority[irec]; + record.rclass = rclass; + if (!record.ttl) + record.ttl = ttl; + data = mdns_answer_add_record(buffer, capacity, data, record, &string_table); + } + data = mdns_answer_add_txt_record(buffer, capacity, data, authority, authority_count, + rclass, ttl, &string_table); + + // Fill in additional records + for (size_t irec = 0; data && (irec < additional_count); ++irec) { + mdns_record_t record = additional[irec]; + record.rclass = rclass; + if (!record.ttl) + record.ttl = ttl; + data = mdns_answer_add_record(buffer, capacity, data, record, &string_table); + } + data = mdns_answer_add_txt_record(buffer, capacity, data, additional, additional_count, + rclass, ttl, &string_table); + if (!data) + return -1; + + size_t tosend = MDNS_POINTER_DIFF(data, buffer); + return mdns_unicast_send(sock, address, address_size, buffer, tosend); +} + +static inline int +mdns_answer_multicast_rclass_ttl(int sock, void* buffer, size_t capacity, mdns_record_t answer, + const mdns_record_t* authority, size_t authority_count, + const mdns_record_t* additional, size_t additional_count, + uint16_t rclass, uint32_t ttl) { + if (capacity < (sizeof(struct mdns_header_t) + 32 + 4)) + return -1; + + // Basic answer structure + struct mdns_header_t* header = (struct mdns_header_t*)buffer; + header->query_id = 0; + header->flags = htons(0x8400); + header->questions = 0; + header->answer_rrs = htons(1); + header->authority_rrs = htons(mdns_answer_get_record_count(authority, authority_count)); + header->additional_rrs = htons(mdns_answer_get_record_count(additional, additional_count)); + + mdns_string_table_t string_table = {{0}, 0, 0}; + void* data = MDNS_POINTER_OFFSET(buffer, sizeof(struct mdns_header_t)); + + // Fill in answer + mdns_record_t record = answer; + mdns_record_update_rclass_ttl(&record, rclass, ttl); + data = mdns_answer_add_record(buffer, capacity, data, record, &string_table); + + // Fill in authority records + for (size_t irec = 0; data && (irec < authority_count); ++irec) { + record = authority[irec]; + mdns_record_update_rclass_ttl(&record, rclass, ttl); + data = mdns_answer_add_record(buffer, capacity, data, record, &string_table); + } + data = mdns_answer_add_txt_record(buffer, capacity, data, authority, authority_count, + rclass, ttl, &string_table); + + // Fill in additional records + for (size_t irec = 0; data && (irec < additional_count); ++irec) { + record = additional[irec]; + mdns_record_update_rclass_ttl(&record, rclass, ttl); + data = mdns_answer_add_record(buffer, capacity, data, record, &string_table); + } + data = mdns_answer_add_txt_record(buffer, capacity, data, additional, additional_count, + rclass, ttl, &string_table); + if (!data) + return -1; + + size_t tosend = MDNS_POINTER_DIFF(data, buffer); + return mdns_multicast_send(sock, buffer, tosend); +} + +static inline int +mdns_query_answer_multicast(int sock, void* buffer, size_t capacity, mdns_record_t answer, + const mdns_record_t* authority, size_t authority_count, + const mdns_record_t* additional, size_t additional_count) { + return mdns_answer_multicast_rclass_ttl(sock, buffer, capacity, answer, authority, + authority_count, additional, additional_count, + MDNS_CLASS_IN, 60); +} + +static inline int +mdns_announce_multicast(int sock, void* buffer, size_t capacity, mdns_record_t answer, + const mdns_record_t* authority, size_t authority_count, + const mdns_record_t* additional, size_t additional_count) { + return mdns_answer_multicast_rclass_ttl(sock, buffer, capacity, answer, authority, + authority_count, additional, additional_count, + MDNS_CLASS_IN | MDNS_CACHE_FLUSH, 60); +} + +static inline int +mdns_goodbye_multicast(int sock, void* buffer, size_t capacity, mdns_record_t answer, + const mdns_record_t* authority, size_t authority_count, + const mdns_record_t* additional, size_t additional_count) { + // Goodbye should have ttl of 0 + return mdns_answer_multicast_rclass_ttl(sock, buffer, capacity, answer, authority, + authority_count, additional, additional_count, + MDNS_CLASS_IN, 0); +} + +static inline mdns_string_t +mdns_record_parse_ptr(const void* buffer, size_t size, size_t offset, size_t length, + char* strbuffer, size_t capacity) { + // PTR record is just a string + if ((size >= offset + length) && (length >= 2)) + return mdns_string_extract(buffer, size, &offset, strbuffer, capacity); + mdns_string_t empty = {0, 0}; + return empty; +} + +static inline mdns_record_srv_t +mdns_record_parse_srv(const void* buffer, size_t size, size_t offset, size_t length, + char* strbuffer, size_t capacity) { + mdns_record_srv_t srv; + memset(&srv, 0, sizeof(mdns_record_srv_t)); + // Read the service priority, weight, port number and the discovery name + // SRV record format (http://www.ietf.org/rfc/rfc2782.txt): + // 2 bytes network-order unsigned priority + // 2 bytes network-order unsigned weight + // 2 bytes network-order unsigned port + // string: discovery (domain) name, minimum 2 bytes when compressed + if ((size >= offset + length) && (length >= 8)) { + const uint16_t* recorddata = (const uint16_t*)MDNS_POINTER_OFFSET_CONST(buffer, offset); + srv.priority = mdns_ntohs(recorddata++); + srv.weight = mdns_ntohs(recorddata++); + srv.port = mdns_ntohs(recorddata++); + offset += 6; + srv.name = mdns_string_extract(buffer, size, &offset, strbuffer, capacity); + } + return srv; +} + +static inline struct sockaddr_in* +mdns_record_parse_a(const void* buffer, size_t size, size_t offset, size_t length, + struct sockaddr_in* addr) { + memset(addr, 0, sizeof(struct sockaddr_in)); + addr->sin_family = AF_INET; +#ifdef __APPLE__ + addr->sin_len = sizeof(struct sockaddr_in); +#endif + if ((size >= offset + length) && (length == 4)) + memcpy(&addr->sin_addr.s_addr, MDNS_POINTER_OFFSET(buffer, offset), 4); + return addr; +} + +static inline struct sockaddr_in6* +mdns_record_parse_aaaa(const void* buffer, size_t size, size_t offset, size_t length, + struct sockaddr_in6* addr) { + memset(addr, 0, sizeof(struct sockaddr_in6)); + addr->sin6_family = AF_INET6; +#ifdef __APPLE__ + addr->sin6_len = sizeof(struct sockaddr_in6); +#endif + if ((size >= offset + length) && (length == 16)) + memcpy(&addr->sin6_addr, MDNS_POINTER_OFFSET(buffer, offset), 16); + return addr; +} + +static inline size_t +mdns_record_parse_txt(const void* buffer, size_t size, size_t offset, size_t length, + mdns_record_txt_t* records, size_t capacity) { + size_t parsed = 0; + const char* strdata; + size_t end = offset + length; + + if (size < end) + end = size; + + while ((offset < end) && (parsed < capacity)) { + strdata = (const char*)MDNS_POINTER_OFFSET(buffer, offset); + size_t sublength = *(const unsigned char*)strdata; + + ++strdata; + offset += sublength + 1; + + size_t separator = 0; + for (size_t c = 0; c < sublength; ++c) { + // DNS-SD TXT record keys MUST be printable US-ASCII, [0x20, 0x7E] + if ((strdata[c] < 0x20) || (strdata[c] > 0x7E)) + break; + if (strdata[c] == '=') { + separator = c; + break; + } + } + + if (!separator) + continue; + + if (separator < sublength) { + records[parsed].key.str = strdata; + records[parsed].key.length = separator; + records[parsed].value.str = strdata + separator + 1; + records[parsed].value.length = sublength - (separator + 1); + } else { + records[parsed].key.str = strdata; + records[parsed].key.length = sublength; + } + + ++parsed; + } + + return parsed; +} + +#ifdef _WIN32 +#undef strncasecmp +#endif + +#ifdef __cplusplus +} +#endif diff --git a/resources/profiles/Creality/machine/Creality K2 0.2 nozzle.json b/resources/profiles/Creality/machine/Creality K2 0.2 nozzle.json index b482ad9bfe..6652e9eab0 100644 --- a/resources/profiles/Creality/machine/Creality K2 0.2 nozzle.json +++ b/resources/profiles/Creality/machine/Creality K2 0.2 nozzle.json @@ -22,6 +22,8 @@ "fan_speedup_overhangs": "1", "fan_speedup_time": "0", "gcode_flavor": "klipper", + "host_type": "crealityprint", + "printer_agent": "crealityprint", "high_current_on_filament_swap": "0", "machine_end_gcode": "END_PRINT", "machine_load_filament_time": "0", diff --git a/resources/profiles/Creality/machine/Creality K2 0.4 nozzle.json b/resources/profiles/Creality/machine/Creality K2 0.4 nozzle.json index 84a097d8af..33083cfd29 100644 --- a/resources/profiles/Creality/machine/Creality K2 0.4 nozzle.json +++ b/resources/profiles/Creality/machine/Creality K2 0.4 nozzle.json @@ -188,7 +188,8 @@ "creality_flush_time": "86", "disable_m73": "0", "enable_long_retraction_when_cut": "2", - "host_type": "octoprint", + "host_type": "crealityprint", + "printer_agent": "crealityprint", "machine_LED_light_exist": "1", "machine_min_extruding_rate": "0,0", "machine_min_travel_rate": "0,0", diff --git a/resources/profiles/Creality/machine/Creality K2 0.6 nozzle.json b/resources/profiles/Creality/machine/Creality K2 0.6 nozzle.json index eff01f1c05..da3501a1fd 100644 --- a/resources/profiles/Creality/machine/Creality K2 0.6 nozzle.json +++ b/resources/profiles/Creality/machine/Creality K2 0.6 nozzle.json @@ -189,7 +189,8 @@ "creality_flush_time": "86", "disable_m73": "0", "enable_long_retraction_when_cut": "0", - "host_type": "octoprint", + "host_type": "crealityprint", + "printer_agent": "crealityprint", "machine_LED_light_exist": "1", "machine_min_extruding_rate": "0,0", "machine_min_travel_rate": "0,0", diff --git a/resources/profiles/Creality/machine/Creality K2 0.8 nozzle.json b/resources/profiles/Creality/machine/Creality K2 0.8 nozzle.json index 2579b7e240..cb89d24692 100644 --- a/resources/profiles/Creality/machine/Creality K2 0.8 nozzle.json +++ b/resources/profiles/Creality/machine/Creality K2 0.8 nozzle.json @@ -189,7 +189,8 @@ "creality_flush_time": "86", "disable_m73": "0", "enable_long_retraction_when_cut": "0", - "host_type": "octoprint", + "host_type": "crealityprint", + "printer_agent": "crealityprint", "machine_LED_light_exist": "1", "machine_min_extruding_rate": "0,0", "machine_min_travel_rate": "0,0", diff --git a/resources/profiles/Creality/machine/Creality K2 Plus 0.2 nozzle.json b/resources/profiles/Creality/machine/Creality K2 Plus 0.2 nozzle.json index 11171e9797..8f7d6b1bc6 100644 --- a/resources/profiles/Creality/machine/Creality K2 Plus 0.2 nozzle.json +++ b/resources/profiles/Creality/machine/Creality K2 Plus 0.2 nozzle.json @@ -7,6 +7,8 @@ "instantiation": "true", "printer_model": "Creality K2 Plus", "gcode_flavor": "klipper", + "host_type": "crealityprint", + "printer_agent": "crealityprint", "default_print_profile": "0.14mm Optimal @Creality K2 Plus 0.2 nozzle", "nozzle_diameter": [ "0.2" diff --git a/resources/profiles/Creality/machine/Creality K2 Plus 0.4 nozzle.json b/resources/profiles/Creality/machine/Creality K2 Plus 0.4 nozzle.json index 3c3d99c7d3..848aefa4db 100644 --- a/resources/profiles/Creality/machine/Creality K2 Plus 0.4 nozzle.json +++ b/resources/profiles/Creality/machine/Creality K2 Plus 0.4 nozzle.json @@ -7,6 +7,8 @@ "instantiation": "true", "printer_model": "Creality K2 Plus", "gcode_flavor": "klipper", + "host_type": "crealityprint", + "printer_agent": "crealityprint", "default_print_profile": "0.16mm Optimal @Creality K2 Plus 0.4 nozzle", "nozzle_diameter": [ "0.4" diff --git a/resources/profiles/Creality/machine/Creality K2 Plus 0.6 nozzle.json b/resources/profiles/Creality/machine/Creality K2 Plus 0.6 nozzle.json index 4c277b084a..6592a240eb 100644 --- a/resources/profiles/Creality/machine/Creality K2 Plus 0.6 nozzle.json +++ b/resources/profiles/Creality/machine/Creality K2 Plus 0.6 nozzle.json @@ -7,6 +7,8 @@ "instantiation": "true", "printer_model": "Creality K2 Plus", "gcode_flavor": "klipper", + "host_type": "crealityprint", + "printer_agent": "crealityprint", "default_print_profile": "0.24mm Optimal @Creality K2 Plus 0.6 nozzle", "nozzle_diameter": [ "0.6" diff --git a/resources/profiles/Creality/machine/Creality K2 Plus 0.8 nozzle.json b/resources/profiles/Creality/machine/Creality K2 Plus 0.8 nozzle.json index 74c654db1c..96ed049880 100644 --- a/resources/profiles/Creality/machine/Creality K2 Plus 0.8 nozzle.json +++ b/resources/profiles/Creality/machine/Creality K2 Plus 0.8 nozzle.json @@ -7,6 +7,8 @@ "instantiation": "true", "printer_model": "Creality K2 Plus", "gcode_flavor": "klipper", + "host_type": "crealityprint", + "printer_agent": "crealityprint", "default_print_profile": "0.32mm Optimal @Creality K2 Plus 0.8 nozzle", "nozzle_diameter": [ "0.8" diff --git a/resources/profiles/Creality/machine/Creality K2 Pro 0.2 nozzle.json b/resources/profiles/Creality/machine/Creality K2 Pro 0.2 nozzle.json index 58b82fe26c..2ee489b7fa 100644 --- a/resources/profiles/Creality/machine/Creality K2 Pro 0.2 nozzle.json +++ b/resources/profiles/Creality/machine/Creality K2 Pro 0.2 nozzle.json @@ -7,6 +7,8 @@ "instantiation": "true", "printer_model": "Creality K2 Pro", "gcode_flavor": "klipper", + "host_type": "crealityprint", + "printer_agent": "crealityprint", "default_print_profile": "0.16mm Optimal @Creality K2 Pro 0.2 nozzle", "nozzle_diameter": [ "0.2" diff --git a/resources/profiles/Creality/machine/Creality K2 Pro 0.4 nozzle.json b/resources/profiles/Creality/machine/Creality K2 Pro 0.4 nozzle.json index 7ec1e54616..f3b5a9bedf 100644 --- a/resources/profiles/Creality/machine/Creality K2 Pro 0.4 nozzle.json +++ b/resources/profiles/Creality/machine/Creality K2 Pro 0.4 nozzle.json @@ -162,7 +162,8 @@ "fan_speedup_overhangs": "1", "fan_speedup_time": "0", "high_current_on_filament_swap": "0", - "host_type": "octoprint", + "host_type": "crealityprint", + "printer_agent": "crealityprint", "machine_LED_light_exist": "1", "machine_load_filament_time": "86", "machine_min_extruding_rate": "0,0", diff --git a/resources/profiles/Creality/machine/Creality K2 Pro 0.6 nozzle.json b/resources/profiles/Creality/machine/Creality K2 Pro 0.6 nozzle.json index d2e8df0737..1fce24ea83 100644 --- a/resources/profiles/Creality/machine/Creality K2 Pro 0.6 nozzle.json +++ b/resources/profiles/Creality/machine/Creality K2 Pro 0.6 nozzle.json @@ -162,7 +162,8 @@ "fan_speedup_overhangs": "1", "fan_speedup_time": "0", "high_current_on_filament_swap": "0", - "host_type": "octoprint", + "host_type": "crealityprint", + "printer_agent": "crealityprint", "machine_LED_light_exist": "1", "machine_load_filament_time": "86", "machine_min_extruding_rate": "0,0", diff --git a/resources/profiles/Creality/machine/Creality K2 Pro 0.8 nozzle.json b/resources/profiles/Creality/machine/Creality K2 Pro 0.8 nozzle.json index 0f265182a1..660a32e6aa 100644 --- a/resources/profiles/Creality/machine/Creality K2 Pro 0.8 nozzle.json +++ b/resources/profiles/Creality/machine/Creality K2 Pro 0.8 nozzle.json @@ -162,7 +162,8 @@ "fan_speedup_overhangs": "1", "fan_speedup_time": "0", "high_current_on_filament_swap": "0", - "host_type": "octoprint", + "host_type": "crealityprint", + "printer_agent": "crealityprint", "machine_LED_light_exist": "1", "machine_load_filament_time": "86", "machine_min_extruding_rate": "0,0", diff --git a/resources/profiles/Creality/machine/Creality SPARKX i7 0.2 nozzle.json b/resources/profiles/Creality/machine/Creality SPARKX i7 0.2 nozzle.json index d27e926077..e4f58991e8 100644 --- a/resources/profiles/Creality/machine/Creality SPARKX i7 0.2 nozzle.json +++ b/resources/profiles/Creality/machine/Creality SPARKX i7 0.2 nozzle.json @@ -32,7 +32,8 @@ "file_start_gcode": "; multicolor_method = 1\n", "gcode_flavor": "klipper", "high_current_on_filament_swap": "0", - "host_type": "octoprint", + "host_type": "crealityprint", + "printer_agent": "crealityprint", "machine_LED_light_exist": "1", "machine_end_gcode": "G1 E-0.8 F2400 ; retract\nG2 Z{position[2]+3} I0.86 J0.86 P1 F10000 ; spiral lift a little from second lift\nG92 E0 ; zero the extruder\n{if print_sequence == \"by object\"}\nG0 Z{max_layer_z + 1} F1800\n{endif}\nG1 X0 Y180 F8000\nEND_PRINT", "machine_load_filament_time": "30", @@ -69,8 +70,6 @@ "pellet_modded_printer": "0", "preferred_orientation": "0", "prime_tower_position_type": "Middle Upper", - "print_host": "http://10.10.1.39:4408/", - "print_host_webui": "http://10.10.1.39:4408/", "printable_area": "0x0,260x0,260x260,0x260", "printable_height": "255", "printer_technology": "FFF", diff --git a/resources/profiles/Creality/machine/Creality SPARKX i7 0.4 nozzle.json b/resources/profiles/Creality/machine/Creality SPARKX i7 0.4 nozzle.json index fbf911efaf..0651421606 100644 --- a/resources/profiles/Creality/machine/Creality SPARKX i7 0.4 nozzle.json +++ b/resources/profiles/Creality/machine/Creality SPARKX i7 0.4 nozzle.json @@ -32,7 +32,8 @@ "file_start_gcode": "; multicolor_method = 1\n", "gcode_flavor": "klipper", "high_current_on_filament_swap": "0", - "host_type": "octoprint", + "host_type": "crealityprint", + "printer_agent": "crealityprint", "machine_LED_light_exist": "1", "machine_end_gcode": "G1 E-0.8 F2400 ; retract\nG2 Z{position[2]+3} I0.86 J0.86 P1 F10000 ; spiral lift a little from second lift\nG92 E0 ; zero the extruder\n{if print_sequence == \"by object\"}\nG0 Z{max_layer_z + 1} F1800\n{endif}\nG1 X0 Y180 F8000\nEND_PRINT", "machine_load_filament_time": "30", @@ -69,8 +70,6 @@ "pellet_modded_printer": "0", "preferred_orientation": "0", "prime_tower_position_type": "Middle Upper", - "print_host": "http://10.10.1.39:4408/", - "print_host_webui": "http://10.10.1.39:4408/", "printable_area": "0x0,260x0,260x260,0x260", "printable_height": "255", "printer_technology": "FFF", diff --git a/resources/profiles/Creality/machine/Creality SPARKX i7 0.6 nozzle.json b/resources/profiles/Creality/machine/Creality SPARKX i7 0.6 nozzle.json index 11c2564972..769a753561 100644 --- a/resources/profiles/Creality/machine/Creality SPARKX i7 0.6 nozzle.json +++ b/resources/profiles/Creality/machine/Creality SPARKX i7 0.6 nozzle.json @@ -32,7 +32,8 @@ "file_start_gcode": "; multicolor_method = 1\n", "gcode_flavor": "klipper", "high_current_on_filament_swap": "0", - "host_type": "octoprint", + "host_type": "crealityprint", + "printer_agent": "crealityprint", "machine_LED_light_exist": "1", "machine_end_gcode": "G1 E-0.8 F2400 ; retract\nG2 Z{position[2]+3} I0.86 J0.86 P1 F10000 ; spiral lift a little from second lift\nG92 E0 ; zero the extruder\n{if print_sequence == \"by object\"}\nG0 Z{max_layer_z + 1} F1800\n{endif}\nG1 X0 Y180 F8000\nEND_PRINT", "machine_load_filament_time": "30", @@ -69,8 +70,6 @@ "pellet_modded_printer": "0", "preferred_orientation": "0", "prime_tower_position_type": "Middle Upper", - "print_host": "http://10.10.1.39:4408/", - "print_host_webui": "http://10.10.1.39:4408/", "printable_area": "0x0,260x0,260x260,0x260", "printable_height": "255", "printer_technology": "FFF", diff --git a/resources/profiles/Creality/machine/Creality SPARKX i7 0.8 nozzle.json b/resources/profiles/Creality/machine/Creality SPARKX i7 0.8 nozzle.json index 460e1eb607..0a63417d6d 100644 --- a/resources/profiles/Creality/machine/Creality SPARKX i7 0.8 nozzle.json +++ b/resources/profiles/Creality/machine/Creality SPARKX i7 0.8 nozzle.json @@ -32,7 +32,8 @@ "file_start_gcode": "; multicolor_method = 1\n", "gcode_flavor": "klipper", "high_current_on_filament_swap": "0", - "host_type": "octoprint", + "host_type": "crealityprint", + "printer_agent": "crealityprint", "machine_LED_light_exist": "1", "machine_end_gcode": "G1 E-0.8 F2400 ; retract\nG2 Z{position[2]+3} I0.86 J0.86 P1 F10000 ; spiral lift a little from second lift\nG92 E0 ; zero the extruder\n{if print_sequence == \"by object\"}\nG0 Z{max_layer_z + 1} F1800\n{endif}\nG1 X0 Y180 F8000\nEND_PRINT", "machine_load_filament_time": "30", @@ -69,8 +70,6 @@ "pellet_modded_printer": "0", "preferred_orientation": "0", "prime_tower_position_type": "Middle Upper", - "print_host": "http://10.10.1.39:4408/", - "print_host_webui": "http://10.10.1.39:4408/", "printable_area": "0x0,260x0,260x260,0x260", "printable_height": "255", "printer_technology": "FFF", diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 7acb698ee0..969dcb693a 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -56,6 +56,8 @@ set(SLIC3R_GUI_SOURCES GUI/BitmapComboBox.hpp GUI/BonjourDialog.cpp GUI/BonjourDialog.hpp + GUI/CrealityDiscoveryDialog.cpp + GUI/CrealityDiscoveryDialog.hpp GUI/calib_dlg.cpp GUI/calib_dlg.hpp GUI/Calibration.cpp @@ -582,6 +584,10 @@ set(SLIC3R_GUI_SOURCES Utils/ColorSpaceConvert.hpp Utils/CrealityPrint.cpp Utils/CrealityPrint.hpp + Utils/CrealityPrintAgent.cpp + Utils/CrealityPrintAgent.hpp + Utils/CrealityHostDiscovery.cpp + Utils/CrealityHostDiscovery.hpp Utils/Duet.cpp Utils/Duet.hpp Utils/ElegooLink.cpp @@ -758,7 +764,7 @@ else() set(_opengl_link_lib OpenGL::GL) endif() -target_link_libraries(libslic3r_gui libslic3r cereal::cereal imgui imguizmo minilzo libvgcode md4c-html glad ${_opengl_link_lib} hidapi ${wxWidgets_LIBRARIES} glfw libcurl OpenSSL::SSL OpenSSL::Crypto noise::noise) +target_link_libraries(libslic3r_gui libslic3r cereal::cereal imgui imguizmo minilzo libvgcode md4c-html glad ${_opengl_link_lib} hidapi mdns ${wxWidgets_LIBRARIES} glfw libcurl OpenSSL::SSL OpenSSL::Crypto noise::noise) if (MSVC) diff --git a/src/slic3r/GUI/CrealityDiscoveryDialog.cpp b/src/slic3r/GUI/CrealityDiscoveryDialog.cpp new file mode 100644 index 0000000000..4f0c59038d --- /dev/null +++ b/src/slic3r/GUI/CrealityDiscoveryDialog.cpp @@ -0,0 +1,118 @@ +#include "CrealityDiscoveryDialog.hpp" +#include "slic3r/Utils/CrealityHostDiscovery.hpp" +#include "GUI_App.hpp" +#include "I18N.hpp" +#include "Widgets/DialogButtons.hpp" + +#include +#include +#include +#include +#include + +#include + +namespace Slic3r { +namespace GUI { + +CrealityDiscoveryDialog::CrealityDiscoveryDialog(wxWindow* parent) + : wxDialog(parent, wxID_ANY, _L("Detect Creality K-series printer"), + wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) +{ + const int em = wxGetApp().em_unit(); + + m_status = new wxStaticText(this, wxID_ANY, _L("Click Scan to look for K-series printers on your network.")); + + m_list = new wxListView(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, + wxLC_REPORT | wxSIMPLE_BORDER | wxLC_SINGLE_SEL); + m_list->SetMinSize(wxSize(50 * em, 18 * em)); + m_list->AppendColumn(_L("Model"), wxLIST_FORMAT_LEFT, 8 * em); + m_list->AppendColumn(_L("Hostname"), wxLIST_FORMAT_LEFT, 14 * em); + m_list->AppendColumn(_L("IP"), wxLIST_FORMAT_LEFT, 14 * em); + + auto* dlg_btns = new DialogButtons(this, {"Scan", "OK", "Cancel"}, "", 1 /*left_aligned*/); + auto* ok_btn = dlg_btns->GetOK(); + ok_btn->SetLabel(_L("Use Selected")); + ok_btn->Disable(); + + auto* vsizer = new wxBoxSizer(wxVERTICAL); + vsizer->Add(m_status, 0, wxEXPAND | wxALL, em); + vsizer->Add(m_list, 1, wxEXPAND | wxALL, em); + vsizer->Add(dlg_btns, 0, wxEXPAND); + SetSizerAndFit(vsizer); + + dlg_btns->GetFIRST()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { run_discovery(); }); + + m_list->Bind(wxEVT_LIST_ITEM_SELECTED, [ok_btn](wxListEvent&) { ok_btn->Enable(); }); + m_list->Bind(wxEVT_LIST_ITEM_DESELECTED, [ok_btn](wxListEvent&) { ok_btn->Disable(); }); + m_list->Bind(wxEVT_LIST_ITEM_ACTIVATED, [this](wxListEvent&) { on_ok(); EndModal(wxID_OK); }); + + ok_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_ok(); EndModal(wxID_OK); }); + dlg_btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); }); + + wxGetApp().UpdateDlgDarkUI(this); + + // Run discovery synchronously from the ctor so results are ready by the + // time the dialog is shown. Posting an async CallAfter from a ShowModal + // override risked the event firing after the modal loop had exited -- the + // captured `this` would then be a dangling stack pointer and subsequent + // UI access could fault. + run_discovery(); +} + +void CrealityDiscoveryDialog::run_discovery() +{ + m_status->SetLabel(_L("Scanning the LAN for K-series printers... this takes a few seconds.")); + m_list->DeleteAllItems(); + m_rows.clear(); + Layout(); + Update(); + + std::vector hosts; + { + wxBusyCursor cursor; + wxWindowDisabler disabler(this); + hosts = CrealityHostDiscovery::scan(/*probe_info=*/true); + } + + for (const auto& h : hosts) { + Row row; + row.ip = h.ip; + row.hostname = h.hostname; + if (!h.model_name.empty()) + row.model = h.model_name; + else if (h.cfs_capable) + row.model = "(unknown K-series)"; + else + row.model = "Creality"; + m_rows.push_back(std::move(row)); + } + + for (size_t i = 0; i < m_rows.size(); ++i) { + long idx = m_list->InsertItem(i, wxString::FromUTF8(m_rows[i].model)); + m_list->SetItem(idx, 1, wxString::FromUTF8(m_rows[i].hostname)); + m_list->SetItem(idx, 2, wxString::FromUTF8(m_rows[i].ip)); + } + + if (m_rows.empty()) { + m_status->SetLabel(_L( + "No K-series printers found. Make sure the printer is on the same " + "network and not blocked by Wi-Fi client isolation, then click Scan again.")); + } else { + m_status->SetLabel(wxString::Format( + _L("Found %zu Creality printer(s). Select one and click Use Selected."), + m_rows.size())); + m_list->Select(0); + } +} + +void CrealityDiscoveryDialog::on_ok() +{ + auto sel = m_list->GetFirstSelected(); + if (sel >= 0 && sel < int(m_rows.size())) { + m_selected_ip = m_rows[sel].ip; + } +} + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/CrealityDiscoveryDialog.hpp b/src/slic3r/GUI/CrealityDiscoveryDialog.hpp new file mode 100644 index 0000000000..2aa347a456 --- /dev/null +++ b/src/slic3r/GUI/CrealityDiscoveryDialog.hpp @@ -0,0 +1,49 @@ +#ifndef slic3r_CrealityDiscoveryDialog_hpp_ +#define slic3r_CrealityDiscoveryDialog_hpp_ + +#include + +#include +#include + +class wxListView; +class wxStaticText; + +namespace Slic3r { +namespace GUI { + +// Modal dialog that finds Creality K-series printers on the LAN via DNS-SD +// mDNS (vendored mjansson/mdns + cxmdns wrapper) and lets the user pick one. +// +// Discovery is synchronous (~5 second mDNS listen + ~2-4 sec /info probe per +// host) and runs during ShowModal() with a busy cursor. The user-facing busy +// time is bounded by the mDNS listener's 5-second deadline plus any in-flight +// probes; in practice 5-10 seconds total for a typical LAN with one K2. +// +// After ShowModal() returns wxID_OK, selected_ip() yields the chosen +// printer's IPv4 address. Returns the empty string on Cancel or if no match +// was found. +class CrealityDiscoveryDialog : public wxDialog +{ +public: + CrealityDiscoveryDialog(wxWindow* parent); + ~CrealityDiscoveryDialog() override = default; + + std::string selected_ip() const { return m_selected_ip; } + +private: + void run_discovery(); + void on_ok(); + + struct Row { std::string ip; std::string model; std::string hostname; }; + + wxListView* m_list = nullptr; + wxStaticText* m_status = nullptr; + std::vector m_rows; + std::string m_selected_ip; +}; + +} // namespace GUI +} // namespace Slic3r + +#endif diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.cpp b/src/slic3r/GUI/PhysicalPrinterDialog.cpp index 4a371a46a6..94972cc9af 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.cpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp @@ -37,6 +37,7 @@ #include "RemovableDriveManager.hpp" #include "BitmapCache.hpp" #include "BonjourDialog.hpp" +#include "CrealityDiscoveryDialog.hpp" #include "MsgDialog.hpp" #include "OAuthDialog.hpp" #include "SimplyPrint.hpp" @@ -203,11 +204,28 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr return sizer; }; - auto printhost_browse = [=](wxWindow* parent) + auto printhost_browse = [=](wxWindow* parent) { auto sizer = create_sizer_with_btn(parent, &m_printhost_browse_btn, "printer_host_browser", _L("Browse") + " " + dots); m_printhost_browse_btn->Bind(wxEVT_BUTTON, [=](wxCommandEvent& e) { const auto host_type = m_config->opt_enum("host_type"); + + // Creality K-series printers announce themselves via DNS-SD under a + // per-device-unique service type _Creality-._udp, so the + // standard fixed-service-name Bonjour browser does not find them. + // Dispatch to the Creality-specific scanner instead. + if (host_type == htCrealityPrint) { + CrealityDiscoveryDialog dialog(this); + if (dialog.ShowModal() == wxID_OK && !dialog.selected_ip().empty()) { + // set_value expects the value wrapped as wxString -- TextCtrl::set_value + // any_casts to wxString, so a raw std::string throws bad_any_cast. + wxString new_url = wxString::FromUTF8("http://" + dialog.selected_ip()); + m_optgroup->set_value("print_host", new_url, true); + m_optgroup->get_field("print_host")->field_changed(); + } + return; + } + if (host_type == htFlashforge) { wxBusyCursor wait; std::vector printers; @@ -232,12 +250,13 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr update_printhost_buttons(); } } - } else { - BonjourDialog dialog(this, Preset::printer_technology(*m_config)); - if (dialog.show_and_lookup()) { - m_optgroup->set_value("print_host", dialog.get_selected(), true); - m_optgroup->get_field("print_host")->field_changed(); - } + return; + } + + BonjourDialog dialog(this, Preset::printer_technology(*m_config)); + if (dialog.show_and_lookup()) { + m_optgroup->set_value("print_host", dialog.get_selected(), true); + m_optgroup->get_field("print_host")->field_changed(); } }); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index b694d219d2..7959ba6c6f 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -65,6 +65,7 @@ #include "libslic3r/SLAPrint.hpp" #include "libslic3r/Utils.hpp" #include "libslic3r/PresetBundle.hpp" +#include "slic3r/Utils/CrealityPrint.hpp" #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/ObjColorUtils.hpp" // For stl export @@ -3673,6 +3674,7 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "finish pop_finsish_sync_ams_dialog"; } + bool Sidebar::should_show_SEMM_buttons() { PresetBundle &preset_bundle = *wxGetApp().preset_bundle; @@ -16110,6 +16112,11 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us pDlg = std::make_unique(default_output_file, upload_job.printhost->get_post_upload_actions(), groups, storage_paths, storage_names, config->get_bool("open_device_tab_post_upload")); + } else if (host_type == htCrealityPrint) { + pDlg = std::make_unique(default_output_file, upload_job.printhost->get_post_upload_actions(), groups, + storage_paths, storage_names, + config->get_bool("open_device_tab_post_upload"), + upload_job.printhost.get()); } else if (flashforge_local_api) { auto* flashforge_host = dynamic_cast(upload_job.printhost.get()); if (flashforge_host == nullptr) { diff --git a/src/slic3r/GUI/PrintHostDialogs.cpp b/src/slic3r/GUI/PrintHostDialogs.cpp index 1d9b30fd1b..42a0108383 100644 --- a/src/slic3r/GUI/PrintHostDialogs.cpp +++ b/src/slic3r/GUI/PrintHostDialogs.cpp @@ -35,6 +35,11 @@ #include "NotificationManager.hpp" #include "ExtraRenderers.hpp" #include "format.hpp" +#include "../Utils/CrealityPrint.hpp" +#include "BitmapComboBox.hpp" +#include "wxExtensions.hpp" + +#include namespace fs = boost::filesystem; using json = nlohmann::json; @@ -1854,4 +1859,251 @@ void ElegooPrintHostSendDialog::refresh() this->Fit(); } +CrealityPrintHostSendDialog::CrealityPrintHostSendDialog(const fs::path& path, + PrintHostPostUploadActions post_actions, + const wxArrayString& groups, + const wxArrayString& storage_paths, + const wxArrayString& storage_names, + bool switch_to_device_tab, + PrintHost* printhost) + : PrintHostSendDialog(path, post_actions, groups, storage_paths, storage_names, switch_to_device_tab) + , m_enableSelfTest(false) + , m_printhost(printhost) +{} + +void CrealityPrintHostSendDialog::init() +{ + PrintHostSendDialog::init(); + + auto* creality_host = static_cast(m_printhost); + bool multi_color; + std::string printer_name; + { + wxBusyCursor wait; + multi_color = creality_host->supports_multi_color_print(); + if (multi_color) + printer_name = creality_host->model_name(); + } + if (!multi_color) + return; + + auto* group_box = new wxStaticBox(this, wxID_ANY, + wxString::Format(_L("Printer: %s"), printer_name)); + auto* group_sizer = new wxStaticBoxSizer(group_box, wxVERTICAL); + content_sizer->Add(group_sizer, 0, wxEXPAND); + + const AppConfig* app_config = wxGetApp().app_config; + std::string saved = app_config->get("recent", CONFIG_KEY_ENABLESELFTEST); + if (!saved.empty()) { + try { m_enableSelfTest = std::stoi(saved) != 0; } catch (...) {} + } + + // Calibration checkbox + { + auto checkbox_sizer = new wxBoxSizer(wxHORIZONTAL); + auto checkbox = new ::CheckBox(this); + checkbox->SetValue(m_enableSelfTest); + checkbox->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& e) { + m_enableSelfTest = e.IsChecked(); + AppConfig* ac = wxGetApp().app_config; + ac->set("recent", CONFIG_KEY_ENABLESELFTEST, m_enableSelfTest ? "1" : "0"); + e.Skip(); + }); + checkbox_sizer->Add(checkbox, 0, wxALL | wxALIGN_CENTER, FromDIP(2)); + auto checkbox_text = new wxStaticText(this, wxID_ANY, _L("Calibrate before printing"), wxDefaultPosition, wxDefaultSize, 0); + checkbox_sizer->Add(checkbox_text, 0, wxALL | wxALIGN_CENTER, FromDIP(2)); + checkbox_text->SetFont(::Label::Body_13); + checkbox_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D"))); + group_sizer->Add(checkbox_sizer); + group_sizer->AddSpacer(VERT_SPACING); + } + + // --- Color mapping UI --- + // Get gcode filament info from slicer + auto preset_bundle = wxGetApp().preset_bundle; + auto full_config = preset_bundle->full_config(); + auto* filament_colors = full_config.option("filament_colour"); + auto* filament_types = full_config.option("filament_type"); + int gcode_filament_count = filament_colors ? (int)filament_colors->values.size() : 0; + + // Query printer for loaded materials + { + wxBusyCursor wait; + std::string boxes_json = creality_host->query_boxes_info(); + if (!boxes_json.empty()) { + try { + auto resp = nlohmann::json::parse(boxes_json); + if (resp.contains("boxsInfo") && resp["boxsInfo"].contains("materialBoxs")) { + for (auto& box : resp["boxsInfo"]["materialBoxs"]) { + int box_id = box["id"].get(); + int box_type = box.value("type", 0); + // Skip inactive CFS boxes (type 0 with state != 1) + // Spool holder (type 1) is always available + if (box_type == 0 && box.value("state", 0) != 1) + continue; + for (auto& mat : box["materials"]) { + int slot_id = mat["id"].get(); + std::string tool_id = "T" + std::to_string(box_id) + std::string(1, 'A' + slot_id); + // Creality uses "#0RRGGBB" (7 hex digits), normalize to "#RRGGBB" + std::string color = mat.value("color", "#FFFFFF"); + if (color.size() == 8 && color[0] == '#') + color = "#" + color.substr(2); + m_printer_slots.push_back({ + tool_id, + mat.value("type", ""), + color, + box_id, + slot_id + }); + } + } + } + } catch (const nlohmann::json::exception& e) { + BOOST_LOG_TRIVIAL(error) << "CrealityPrint dialog: Failed to parse boxsInfo: " << e.what(); + } + } + } + + if (gcode_filament_count > 0 && !m_printer_slots.empty()) { + auto* label = new wxStaticText(this, wxID_ANY, _L("Filament Mapping:")); + label->SetFont(::Label::Body_13); + label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D"))); + group_sizer->Add(label); + group_sizer->AddSpacer(4); + + for (int i = 0; i < gcode_filament_count; i++) { + auto* row_sizer = new wxBoxSizer(wxHORIZONTAL); + + // Left side: gcode filament color swatch + type + std::string gc_color = (filament_colors && i < (int)filament_colors->values.size()) + ? filament_colors->values[i] : "#FFFFFF"; + std::string gc_type = (filament_types && i < (int)filament_types->values.size()) + ? filament_types->values[i] : "?"; + + // Color indicator panel + auto* color_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(16), FromDIP(16))); + color_panel->SetBackgroundColour(wxColour(gc_color)); + color_panel->SetMinSize(wxSize(FromDIP(16), FromDIP(16))); + row_sizer->Add(color_panel, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); + + auto* type_label = new wxStaticText(this, wxID_ANY, + wxString::Format("%d (%s)", i + 1, gc_type.c_str())); + type_label->SetFont(::Label::Body_13); + type_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D"))); + type_label->SetMinSize(wxSize(FromDIP(80), -1)); + row_sizer->Add(type_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + + // Arrow + auto* arrow_label = new wxStaticText(this, wxID_ANY, wxString::FromUTF8("\xe2\x86\x92")); + arrow_label->SetFont(::Label::Body_13); + row_sizer->Add(arrow_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + + // Right side: dropdown with color icons per slot + int icon_sz = FromDIP(16); + auto* combo = new BitmapComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY); + for (auto& slot : m_printer_slots) { + wxBitmap* bmp = get_extruder_color_icon(slot.color, "", icon_sz, icon_sz); + wxString label_str; + if (slot.box_id == 0) + label_str = wxString::Format("Ext - %s", slot.type.c_str()); + else + label_str = wxString::Format("%s - %s", slot.tool_id.substr(1).c_str(), slot.type.c_str()); + combo->Append(label_str, bmp ? *bmp : wxNullBitmap); + } + // Find best default: CFS exact color+type, CFS type-only, + // Ext exact, Ext type-only, else positional + int default_sel = (i < (int)m_printer_slots.size()) ? i : 0; + bool matched = false; + for (int pass = 0; pass < 4 && !matched; pass++) { + for (int s = 0; s < (int)m_printer_slots.size(); s++) { + bool is_ext = (m_printer_slots[s].box_id == 0); + bool type_match = (m_printer_slots[s].type == gc_type); + bool color_match = (wxColour(m_printer_slots[s].color) == wxColour(gc_color)); + bool hit = false; + switch (pass) { + case 0: hit = !is_ext && type_match && color_match; break; + case 1: hit = !is_ext && type_match; break; + case 2: hit = is_ext && type_match && color_match; break; + case 3: hit = is_ext && type_match; break; + } + if (hit) { + default_sel = s; + matched = true; + break; + } + } + } + combo->SetSelection(default_sel); + row_sizer->Add(combo, 0, wxALIGN_CENTER_VERTICAL); + + group_sizer->Add(row_sizer); + group_sizer->AddSpacer(4); + m_slot_combos.push_back(combo); + } + + int ext_slot_idx = -1; + for (int s = 0; s < (int)m_printer_slots.size(); s++) { + if (m_printer_slots[s].box_id == 0) { + ext_slot_idx = s; + break; + } + } + if (ext_slot_idx >= 0) { + for (int ci = 0; ci < (int)m_slot_combos.size(); ci++) { + int sel = m_slot_combos[ci]->GetSelection(); + if (sel >= 0 && sel < (int)m_printer_slots.size() && + m_printer_slots[sel].box_id == 0) { + for (int cj = 0; cj < (int)m_slot_combos.size(); cj++) { + if (cj != ci) + m_slot_combos[cj]->Enable(false); + } + break; + } + } + + for (auto* c : m_slot_combos) { + c->Bind(wxEVT_COMBOBOX, [this, ext_slot_idx](wxCommandEvent& e) { + int sel = e.GetSelection(); + if (sel >= 0 && sel < (int)m_printer_slots.size() && + m_printer_slots[sel].box_id == 0) { + for (auto* c2 : m_slot_combos) { + if (c2 != e.GetEventObject()) + c2->Enable(false); + } + } else { + for (auto* c2 : m_slot_combos) + c2->Enable(true); + } + e.Skip(); + }); + } + } + } + + this->Layout(); + this->Fit(); +} + +std::map CrealityPrintHostSendDialog::extendedInfo() const +{ + std::map info; + info["enableSelfTest"] = m_enableSelfTest ? "1" : "0"; + + // Color mapping: colorMatch_0, colorMatch_1, ... tab-delimited + for (int i = 0; i < (int)m_slot_combos.size(); i++) { + int sel = m_slot_combos[i]->GetSelection(); + if (sel >= 0 && sel < (int)m_printer_slots.size()) { + auto& slot = m_printer_slots[sel]; + // id = gcode tool index (T1A for first filament, T1B for second, ...), + // not the destination CFS slot — firmware matches by gcode tool. + std::string gcode_tool = "T1" + std::string(1, 'A' + i); + info["colorMatch_" + std::to_string(i)] = + gcode_tool + "\t" + slot.type + "\t" + slot.color + "\t" + + std::to_string(slot.box_id) + "\t" + std::to_string(slot.material_id); + } + } + + return info; +} + }} diff --git a/src/slic3r/GUI/PrintHostDialogs.hpp b/src/slic3r/GUI/PrintHostDialogs.hpp index cc4670f514..988d4c8171 100644 --- a/src/slic3r/GUI/PrintHostDialogs.hpp +++ b/src/slic3r/GUI/PrintHostDialogs.hpp @@ -25,6 +25,8 @@ class wxStaticText; class wxWrapSizer; class CheckBox; +namespace Slic3r { namespace GUI { class BitmapComboBox; } } + namespace Slic3r { namespace GUI { @@ -186,6 +188,37 @@ private: BedType m_BedType; }; +class CrealityPrintHostSendDialog : public PrintHostSendDialog +{ +public: + CrealityPrintHostSendDialog(const boost::filesystem::path& path, + PrintHostPostUploadActions post_actions, + const wxArrayString& groups, + const wxArrayString& storage_paths, + const wxArrayString& storage_names, + bool switch_to_device_tab, + PrintHost* printhost); + + virtual void init() override; + virtual std::map extendedInfo() const; + +private: + static constexpr const char* CONFIG_KEY_ENABLESELFTEST = "crealityprint_enable_self_test"; + + bool m_enableSelfTest; + PrintHost* m_printhost; + + struct SlotInfo { + std::string tool_id; // e.g. "T1A" + std::string type; // e.g. "PLA" + std::string color; // e.g. "#ffffff" + int box_id; + int material_id; + }; + std::vector m_printer_slots; + std::vector m_slot_combos; // one per gcode filament +}; + class FlashforgePrintHostSendDialog : public PrintHostSendDialog { public: diff --git a/src/slic3r/Utils/CrealityHostDiscovery.cpp b/src/slic3r/Utils/CrealityHostDiscovery.cpp new file mode 100644 index 0000000000..8fcf16749d --- /dev/null +++ b/src/slic3r/Utils/CrealityHostDiscovery.cpp @@ -0,0 +1,128 @@ +#include "CrealityHostDiscovery.hpp" +#include "cxmdns.h" +#include "Http.hpp" + +#include +#include + +#include + +namespace Slic3r { + +namespace { + +struct ModelEntry { const char* code; const char* name; }; +constexpr ModelEntry kCfsCapableModels[] = { + {"F008", "K2 Plus"}, + {"F012", "K2 Pro"}, + {"F021", "K2"}, +}; + +bool is_cfs_capable(const std::string& code) +{ + for (const auto& m : kCfsCapableModels) + if (code == m.code) return true; + return false; +} + +std::string model_name_for(const std::string& code) +{ + for (const auto& m : kCfsCapableModels) + if (code == m.code) return m.name; + return {}; +} + +// Extract the device suffix from a service name like +// "_Creality-543324280CDB19._udp.local." and synthesise a hostname-ish label +// (e.g. "K2-DB19" using the last 4 hex of the MAC-derived suffix). +std::string hostname_from_service(const std::string& service_name) +{ + auto dash = service_name.find_last_of('-'); + if (dash == std::string::npos) return {}; + auto dot = service_name.find('.', dash); + if (dot == std::string::npos) return {}; + std::string suffix = service_name.substr(dash + 1, dot - dash - 1); + if (suffix.size() >= 4) { + return "K2-" + suffix.substr(suffix.size() - 4); + } + return suffix.empty() ? std::string{} : "K2-" + suffix; +} + +// Synchronously probe http:///info for {model, mac}. Short timeout -- +// we don't want one slow host to drag down discovery. +void probe_info(CrealityHost& host) +{ + const std::string url = "http://" + host.ip + "/info"; + auto http = Http::get(url); + http.timeout_connect(2) + .timeout_max(4) + .on_complete([&host](std::string body, unsigned /*status*/) { + try { + auto j = nlohmann::json::parse(body); + if (j.contains("model") && j["model"].is_string()) + host.model_code = j["model"].get(); + if (j.contains("mac") && j["mac"].is_string()) + host.mac = j["mac"].get(); + if (is_cfs_capable(host.model_code)) { + host.cfs_capable = true; + host.model_name = model_name_for(host.model_code); + } + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) + << "CrealityHostDiscovery: /info parse failed for " + << host.ip << ": " << e.what(); + } + }) + .on_error([&host](std::string /*body*/, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(info) + << "CrealityHostDiscovery: /info GET failed for " + << host.ip << ": " << error << " (HTTP " << status << ")"; + }) + .perform_sync(); +} + +} // namespace + +std::vector CrealityHostDiscovery::scan(bool probe) +{ + const std::vector prefixes{ "Creality", "creality" }; + + BOOST_LOG_TRIVIAL(info) + << "CrealityHostDiscovery: starting DNS-SD discovery (prefixes: Creality, creality)"; + + auto raw = cxnet::syncDiscoveryService(prefixes); + + BOOST_LOG_TRIVIAL(info) + << "CrealityHostDiscovery: mDNS returned " << raw.size() << " match(es)"; + + std::vector hosts; + hosts.reserve(raw.size()); + + // Dedupe by IP -- one printer may announce twice if multi-homed or if + // we capture both IPv4/IPv6 replies. + std::vector seen_ips; + for (const auto& m : raw) { + if (m.machineIp.empty()) continue; + if (std::find(seen_ips.begin(), seen_ips.end(), m.machineIp) != seen_ips.end()) + continue; + seen_ips.push_back(m.machineIp); + + CrealityHost h; + h.ip = m.machineIp; + h.service_name = m.answer; + h.hostname = hostname_from_service(m.answer); + + if (probe) { + probe_info(h); + } + + hosts.push_back(std::move(h)); + } + + BOOST_LOG_TRIVIAL(info) + << "CrealityHostDiscovery: " << hosts.size() << " unique host(s) after dedup"; + + return hosts; +} + +} // namespace Slic3r diff --git a/src/slic3r/Utils/CrealityHostDiscovery.hpp b/src/slic3r/Utils/CrealityHostDiscovery.hpp new file mode 100644 index 0000000000..be9b85faa1 --- /dev/null +++ b/src/slic3r/Utils/CrealityHostDiscovery.hpp @@ -0,0 +1,45 @@ +#ifndef slic3r_CrealityHostDiscovery_hpp_ +#define slic3r_CrealityHostDiscovery_hpp_ + +#include +#include + +namespace Slic3r { + +// One discovered Creality K-series host on the LAN. +struct CrealityHost +{ + std::string ip; // dotted-quad IPv4 + std::string service_name; // raw mDNS service type, e.g. "_Creality-543324280CDB19._udp.local." + std::string hostname; // e.g. "K2-DB19" (derived from service-name suffix) + std::string model_code; // "F008" / "F012" / "F021" (empty if /info probe failed) + std::string model_name; // "K2 Plus" / "K2 Pro" / "K2" (empty if model not in our table) + std::string mac; // from /info if probed + bool cfs_capable = false; // true when model_code is in the K2 family +}; + +// Synchronous LAN discovery for Creality K-series printers via DNS-SD mDNS. +// +// Sends a meta-discovery query (_services._dns-sd._udp.local.) and listens +// for ~5 seconds for service announcements whose type-name contains the +// "Creality" / "creality" substring. K-series firmware announces each +// printer under a per-device-unique type _Creality-._udp.local, +// so a fixed-name query does not work -- the meta-discovery is the only +// reliable way to find them. +// +// When probe_info is true, each discovered host is followed up with an HTTP +// GET http:///info call to fetch the printer's model code (F008/F012/F021) +// and MAC. The probe step adds ~2-4 seconds per host but yields enriched +// results that let the UI display "K2" / "K2 Plus" / "K2 Pro" instead of +// just an IP. +// +// Call from a background thread -- the function blocks for at least 5 seconds. +class CrealityHostDiscovery +{ +public: + static std::vector scan(bool probe_info = true); +}; + +} // namespace Slic3r + +#endif diff --git a/src/slic3r/Utils/CrealityPrint.cpp b/src/slic3r/Utils/CrealityPrint.cpp index 1df8c72d4b..220d046d2e 100644 --- a/src/slic3r/Utils/CrealityPrint.cpp +++ b/src/slic3r/Utils/CrealityPrint.cpp @@ -1,6 +1,7 @@ #include "CrealityPrint.hpp" #include +#include #include #include #include @@ -88,7 +89,8 @@ bool CrealityPrint::test(wxString& msg) const // Here we do not have to add custom "Host" header - the url contains host filled by user and libCurl will set the header by itself. auto http = Http::get(std::move(url)); set_auth(http); - http.on_error([&](std::string body, std::string error, unsigned status) { + http.timeout_max(5) + .on_error([&](std::string body, std::string error, unsigned status) { BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error getting version: %2%, HTTP %3%, body: `%4%`") % name % error % status % body; res = false; @@ -96,6 +98,15 @@ bool CrealityPrint::test(wxString& msg) const }) .on_complete([&, this](std::string body, unsigned) { BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got version: %2%") % name % body; + try { + auto info = json::parse(body); + if (info.contains("model")) { + m_model = info["model"].get(); + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Detected model: %2%") % name % m_model; + } + } catch (const json::exception& e) { + BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: Failed to parse /info response: %2%") % name % e.what(); + } }) #ifdef WIN32 .ssl_revoke_best_effort(m_ssl_revoke_best_effort) @@ -130,14 +141,19 @@ bool CrealityPrint::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, auto http = Http::post(url); // std::move(url)); set_auth(http); - http.form_add("path", upload_parent_path.string()) - .form_add_file("file", upload_data.source_path.string(), upload_filename.string()) + if (!supports_multi_color_print()) + http.form_add("path", upload_parent_path.string()); + http.form_add_file("file", upload_data.source_path.string(), upload_filename.string()) .on_complete([&](std::string body, unsigned status) { BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: File uploaded: HTTP %2%: %3%") % name % status % body; if (upload_data.post_action == PrintHostPostUploadAction::StartPrint) { - start_print(safe_filename(upload_filename.string())); + wxString errormsg; + if (!start_print(errormsg, safe_filename(upload_filename.string()), upload_data.extended_info)) { + error_fn(std::move(errormsg)); + res = false; + } } }) .on_error([&](std::string body, std::string error, unsigned status) { @@ -182,53 +198,231 @@ std::string CrealityPrint::safe_filename(const std::string &filename) const return safe_filename; } -void CrealityPrint::start_print(const std::string &filename) const +static void ws_connect(net::io_context& ioc, websocket::stream& ws, + const std::string& host_url, const std::string& port) +{ + std::string host = Http::get_host_from_url(host_url); + + tcp::resolver resolver{ioc}; + beast::get_lowest_layer(ws).expires_after(std::chrono::seconds(5)); + auto const results = resolver.resolve(host, port); + beast::get_lowest_layer(ws).connect(results); + host += ':' + std::to_string(beast::get_lowest_layer(ws).socket().remote_endpoint().port()); + + ws.set_option(websocket::stream_base::decorator( + [](websocket::request_type& req) { + req.set(http::field::user_agent, + std::string(BOOST_BEAST_VERSION_STRING) + " websocket-client-coro"); + })); + ws.handshake(host, "/"); + +#ifdef _WIN32 + DWORD recv_timeout = 3000; +#else + struct timeval recv_timeout = {3, 0}; +#endif + setsockopt(beast::get_lowest_layer(ws).socket().native_handle(), + SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast(&recv_timeout), sizeof(recv_timeout)); +} + +static std::string ws_send_and_read(websocket::stream& ws, const json& cmd, const std::string& expected_key, int max_reads = 20) +{ + ws.write(net::buffer(to_string(cmd))); + + for (int i = 0; i < max_reads; i++) { + beast::flat_buffer buf; + beast::error_code ec; + ws.read(buf, ec); + if (ec == net::error::would_block) + break; + if (ec) + throw beast::system_error{ec}; + std::string msg = beast::buffers_to_string(buf.data()); + if (msg.find(expected_key) != std::string::npos) + return msg; + } + BOOST_LOG_TRIVIAL(warning) << "CrealityPrint: No '" << expected_key << "' response after " << max_reads << " messages"; + return {}; +} + +void CrealityPrint::query_model() const +{ + if (!m_model.empty()) + return; + + wxString msg; + test(msg); +} + +bool CrealityPrint::supports_multi_color_print() const +{ + query_model(); + // K2-platform printers with CFS support + return m_model == "F008" // K2 Plus + || m_model == "F012" // K2 Pro + || m_model == "F021" // K2 + || m_model == "F022"; // SPARKX i7 +} + +std::string CrealityPrint::model_name() const +{ + static const std::map names = { + {"F008", "K2 Plus"}, + {"F012", "K2 Pro"}, + {"F021", "K2"}, + {"F022", "SPARKX i7"}, + }; + query_model(); + if (m_model.empty()) + return "unreachable"; + auto it = names.find(m_model); + return it != names.end() ? it->second : "unknown (" + m_model + ")"; +} + +std::string CrealityPrint::query_boxes_info() const { try { - std::string host = m_host; - auto const port = "9999"; + net::io_context ioc; + websocket::stream ws{ioc}; + ws_connect(ioc, ws, m_host, "9999"); - json j2 = { - { "method", "set" }, - { - "params", { - { "opGcodeFile", "printprt:/usr/data/printer_data/gcodes/" + filename } - } - } - }; + json boxs_query = {{"method", "get"}, {"params", {{"boxsInfo", 1}}}}; + std::string result = ws_send_and_read(ws, boxs_query, "boxsInfo"); + ws.close(websocket::close_code::normal); + return result; + } catch (std::exception const& e) { + BOOST_LOG_TRIVIAL(error) << "CrealityPrint: Failed to query boxsInfo: " << e.what(); + return {}; + } +} + +std::string CrealityPrint::get_print_host_webui(DynamicPrintConfig* config) +{ + // K-series printers (K2 / K2 Plus / K2 Pro) ship with Mainsail on port 4408. + // Port 80 hosts only the Creality control / upload API, which returns 404 + // for unknown paths and therefore renders as a blank/404 page in Orca's + // Device WebView. Default to the Mainsail URL when the user hasn't + // explicitly set print_host_webui. + if (config == nullptr) + return {}; + + std::string explicit_url = config->opt_string("print_host_webui"); + if (!explicit_url.empty()) + return explicit_url; + + std::string host = config->opt_string("print_host"); + if (host.empty()) + return {}; + + if (boost::algorithm::istarts_with(host, "http://")) + host = host.substr(7); + else if (boost::algorithm::istarts_with(host, "https://")) + host = host.substr(8); + if (auto slash = host.find('/'); slash != std::string::npos) + host = host.substr(0, slash); + if (auto colon = host.find(':'); colon != std::string::npos) + host = host.substr(0, colon); + + return "http://" + host + ":4408/"; +} + +bool CrealityPrint::start_print(wxString &msg, const std::string &filename, const std::map& extended_info) const +{ + try { + const std::string gcode_path = "/mnt/UDISK/printer_data/gcodes/" + filename; net::io_context ioc; + websocket::stream ws{ioc}; + ws_connect(ioc, ws, m_host, "9999"); - tcp::resolver resolver{ioc}; - websocket::stream ws{ioc}; + if (supports_multi_color_print()) { + // Build colorMatch list from the mapping provided by the dialog + bool use_spool_holder = false; + json color_list = json::array(); + for (int i = 0; ; i++) { + auto it = extended_info.find("colorMatch_" + std::to_string(i)); + if (it == extended_info.end()) + break; + // Value format: "toolId\ttype\tcolor\tboxId\tmaterialId" + auto val = it->second; + std::vector parts; + std::istringstream iss(val); + std::string part; + while (std::getline(iss, part, '\t')) + parts.push_back(part); + if (parts.size() >= 5) { + int box_id = std::stoi(parts[3]); + if (box_id == 0) + use_spool_holder = true; + color_list.push_back({ + {"id", parts[0]}, + {"type", parts[1]}, + {"color", parts[2]}, + {"boxId", box_id}, + {"materialId", std::stoi(parts[4])} + }); + } + } - auto const results = resolver.resolve(host, port); - - auto ep = net::connect(ws.next_layer(), results); - - host += ':' + std::to_string(ep.port()); - - ws.set_option(websocket::stream_base::decorator( - [](websocket::request_type& req) + int enable_self_test = 0; { - req.set(http::field::user_agent, - std::string(BOOST_BEAST_VERSION_STRING) + - " websocket-client-coro"); - })); + auto it = extended_info.find("enableSelfTest"); + if (it != extended_info.end()) + enable_self_test = std::stoi(it->second); + } - ws.handshake(host, "/"); - - ws.write(net::buffer(to_string(j2))); + if (use_spool_holder) { + json cmd = { + {"method", "set"}, + {"params", { + {"opGcodeFile", "printprt:" + gcode_path}, + {"enableSelfTest", enable_self_test} + }} + }; + ws.write(net::buffer(to_string(cmd))); + } else { + json color_match = { + {"method", "set"}, + {"params", { + {"colorMatch", { + {"path", gcode_path}, + {"list", color_list} + }} + }} + }; + ws.write(net::buffer(to_string(color_match))); - beast::flat_buffer buffer; + json multi_color_print = { + {"method", "set"}, + {"params", { + {"multiColorPrint", { + {"gcode", gcode_path}, + {"enableSelfTest", enable_self_test} + }} + }} + }; + ws.write(net::buffer(to_string(multi_color_print))); + } + } else { + json cmd = { + {"method", "set"}, + {"params", { + {"opGcodeFile", "printprt:/usr/data/printer_data/gcodes/" + filename} + }} + }; + ws.write(net::buffer(to_string(cmd))); - ws.read(buffer); + beast::flat_buffer buffer; + ws.read(buffer); + } ws.close(websocket::close_code::normal); + return true; } catch(std::exception const& e) { - std::cerr << "Error: " << e.what() << std::endl; + BOOST_LOG_TRIVIAL(error) << "CrealityPrint: Error starting print: " << e.what(); + msg = wxString::FromUTF8(e.what()); + return false; } - } } diff --git a/src/slic3r/Utils/CrealityPrint.hpp b/src/slic3r/Utils/CrealityPrint.hpp index 14d133277f..7a3728dce6 100644 --- a/src/slic3r/Utils/CrealityPrint.hpp +++ b/src/slic3r/Utils/CrealityPrint.hpp @@ -1,6 +1,7 @@ #ifndef slic3r_CrealityPrint_hpp_ #define slic3r_CrealityPrint_hpp_ +#include #include #include #include @@ -29,6 +30,13 @@ public: virtual bool test(wxString& curl_msg) const override; PrintHostPostUploadActions get_post_upload_actions() const; bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const override; + bool supports_multi_color_print() const; + std::string query_boxes_info() const; + std::string model_name() const; + + // Mainsail on K-series printers listens on port 4408. Use that as the + // default Device-tab WebView URL when the user has not set print_host_webui. + static std::string get_print_host_webui(DynamicPrintConfig *config); protected: virtual void set_auth(Http& http) const; @@ -39,10 +47,12 @@ private: std::string m_cafile; std::string m_web_ui; bool m_ssl_revoke_best_effort; + mutable std::string m_model; std::string make_url(const std::string& path) const; - void start_print(const std::string& path) const; + bool start_print(wxString& msg, const std::string& filename, const std::map& extended_info) const; std::string safe_filename(const std::string& filename) const; + void query_model() const; }; } // namespace Slic3r diff --git a/src/slic3r/Utils/CrealityPrintAgent.cpp b/src/slic3r/Utils/CrealityPrintAgent.cpp new file mode 100644 index 0000000000..55d99ff07b --- /dev/null +++ b/src/slic3r/Utils/CrealityPrintAgent.cpp @@ -0,0 +1,334 @@ +#include "CrealityPrintAgent.hpp" +#include "CrealityPrint.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/PrintConfig.hpp" +#include "slic3r/GUI/GUI_App.hpp" + +#include +#include + +#include +#include + +namespace Slic3r { + +namespace { + +constexpr const char* CrealityPrintAgent_VERSION = "0.1.0"; + +bool has_visible_base_preset(const PresetCollection& filaments, const std::string& filament_id) +{ + for (const auto& p : filaments.get_presets()) { + if (p.is_visible && p.is_compatible + && filaments.get_preset_base(p) == &p + && p.filament_id == filament_id) + return true; + } + return false; +} + +} // namespace + +// Score visible compatible filament presets against the CFS spool metadata and +// return the best-matching filament_id. Scoring: +// +20 preset name contains brand_name as a substring +// (e.g. "Hyper PLA" in "Hyper PLA @Creality K2 0.4 nozzle") +// +10 preset name contains the vendor substring (e.g. "Creality") +// Tiebreak: prefer the SYSTEM (shipped) preset over user copies. Brand- +// specific system presets carry their own filament_id; user copies of +// generic presets inherit a generic filament_id from their parent, so +// preferring the user copy can collapse a brand-specific match back to +// "Generic PLA" via the inherited id. Plus: this code targets upstream +// OrcaSlicer where shipping the user's local tuning would be wrong. +// Requires the preset's declared filament_type to equal the spool's base type +// (PLA/PETG/ABS/...) so we never auto-pick a PETG preset for a PLA spool. +// Falls back to filaments.filament_id_by_type(base_type) when nothing scores. +std::string CrealityPrintAgent::match_filament_preset(const PresetCollection& filaments, + const std::string& vendor, + const std::string& brand_name, + const std::string& base_type) +{ + auto to_lower = [](std::string s) { + for (auto& c : s) c = static_cast(std::tolower(static_cast(c))); + return s; + }; + + const std::string vendor_lower = to_lower(vendor); + const std::string brand_lower = to_lower(brand_name); + const std::string type_lower = to_lower(base_type); + + struct Match { + const Preset* preset; + int score; + bool is_user; + }; + std::vector matches; + + int considered = 0; + for (const auto& p : filaments.get_presets()) { + if (!p.is_visible || !p.is_compatible) continue; + // Note: we deliberately do NOT filter on get_preset_base(p) == &p. + // K2 owners frequently keep tweaked copies of system presets + // (e.g. "Creality Hyper PLA @K2 (Harky)" with their per-spool PA), + // which are derived presets — filtering to bases-only would skip + // exactly the presets users care about most. + ++considered; + + std::string preset_type; + if (const auto* ft = p.config.option("filament_type")) + if (!ft->values.empty()) preset_type = ft->values.front(); + if (to_lower(preset_type) != type_lower) continue; + + const std::string name_lower = to_lower(p.name); + int score = 0; + if (!brand_lower.empty() && name_lower.find(brand_lower) != std::string::npos) + score += 20; + if (!vendor_lower.empty() && name_lower.find(vendor_lower) != std::string::npos) + score += 10; + + if (score > 0) + matches.push_back({&p, score, !p.is_system && !p.is_default}); + } + + if (matches.empty()) { + const std::string fallback = filaments.filament_id_by_type(base_type); + const bool fallback_ok = has_visible_base_preset(filaments, fallback); + BOOST_LOG_TRIVIAL(info) + << "CrealityPrintAgent: no preset scored for spool {" << vendor << " " + << brand_name << " (" << base_type << ")} after considering " << considered + << " presets; falling back to generic preset id \"" << fallback << "\"" + << (fallback_ok ? "" : " (NOT visible — returning empty)"); + return fallback_ok ? fallback : std::string(); + } + + std::sort(matches.begin(), matches.end(), + [](const Match& a, const Match& b) { + if (a.score != b.score) return a.score > b.score; + if (a.is_user != b.is_user) return !a.is_user; // prefer system over user + return false; + }); + + BOOST_LOG_TRIVIAL(info) + << "CrealityPrintAgent: matched spool {" << vendor << " " << brand_name + << " (" << base_type << ")} -> preset \"" << matches.front().preset->name + << "\" (score=" << matches.front().score + << ", " << matches.size() << " candidate(s) of " << considered << " considered)"; + + return matches.front().preset->filament_id; +} + +CrealityPrintAgent::CrealityPrintAgent(std::string log_dir) + : MoonrakerPrinterAgent(std::move(log_dir)) +{ +} + +AgentInfo CrealityPrintAgent::get_agent_info_static() +{ + return AgentInfo{ + "crealityprint", + "CrealityPrint", + CrealityPrintAgent_VERSION, + "Creality K-series printer agent (CFS-aware filament sync)" + }; +} + +std::string CrealityPrintAgent::normalize_filament_type(const std::string& filament_type) +{ + static const std::vector bases = { + "PETG", "PET", "PLA", "ABS", "ASA", "TPU", "PC", "PA", "PVA", "HIPS" + }; + for (const auto& base : bases) { + if (filament_type.rfind(base, 0) == 0) return base; + } + return filament_type; +} + +// Parse the boxsInfo JSON returned by CrealityPrint::query_boxes_info(). +// Schema (verified 2026-05-06 against K2 Combo F021 firmware v1.1.260206): +// { "boxsInfo": { "materialBoxs": [ +// { "id": int, "state": int, "type": int, // type 0 = CFS, 1 = single-spool external +// "materials": [ +// { "id": int, "state": int, // state 1 = loaded +// "vendor": str, "type": str, "name": str, +// "color": "#0RRGGBB" }, ... +// ]}, ... +// ]}} +bool CrealityPrintAgent::parse_cfs_response(const std::string& response, + std::vector& slots, + int& box_count, + std::string& error) +{ + using nlohmann::json; + + slots.clear(); + box_count = 0; + + if (response.empty()) { + error = "empty response"; + return false; + } + + json resp; + try { + resp = json::parse(response); + } catch (const std::exception& e) { + error = std::string("JSON parse error: ") + e.what(); + return false; + } + + if (!resp.contains("boxsInfo") || !resp["boxsInfo"].contains("materialBoxs")) { + error = "invalid schema (missing boxsInfo.materialBoxs)"; + return false; + } + + // Sequential AMS-style index for accepted CFS boxes. The K2's raw box.id has + // gaps (id 0 is the external spool holder, type=1, skipped) — using the raw id + // would publish phantom slots for the gap. Renumber accepted boxes 0,1,2,... + int cfs_count = 0; + for (const auto& box : resp["boxsInfo"]["materialBoxs"]) { + const int box_st = box.value("state", 0); + const int box_type = box.value("type", 0); + if (box_st != 1) continue; // inactive boxes + if (box_type != 0) continue; // non-CFS (external spool holder, handled separately by upload dialog) + + const int cfs_index = cfs_count++; + + if (!box.contains("materials") || !box["materials"].is_array()) + continue; + + for (const auto& mat : box["materials"]) { + // CFS slot state encoding observed across K2 family firmwares: + // * K2 (base) / K2 Pro : 0 = empty, 1 = loaded. + // * K2 Plus (1.1.5.5/CFS 1.4.2 onwards): 0 = empty, + // 1 = loaded AND currently + // selected as the active + // spool for printing, + // 2 = loaded but not selected. + // We treat anything non-zero as loaded. Belt-and-braces: also skip + // entries that look blank (no vendor and no type) regardless of state. + const int s_state = mat.value("state", 0); + const std::string s_vendor = mat.value("vendor", std::string()); + const std::string s_type = mat.value("type", std::string()); + if (s_state == 0) continue; // explicitly empty + if (s_vendor.empty() && s_type.empty()) continue; // blank entry — likely empty under a different state encoding + + CFSSlot s; + s.box_id = cfs_index; + s.slot_id = mat.value("id", 0); + s.vendor = s_vendor; + s.brand_name = mat.value("name", ""); + s.filament_type = s_type; + s.color_hex = mat.value("color", "#FFFFFF"); + + // Creality reports colour as "#0RRGGBB" (8 chars with a leading zero + // after '#'). Normalise to standard "#RRGGBB". + if (s.color_hex.size() == 8 && s.color_hex[0] == '#') + s.color_hex = "#" + s.color_hex.substr(2); + + slots.push_back(std::move(s)); + } + } + + box_count = cfs_count; + return true; +} + +bool CrealityPrintAgent::fetch_filament_info(std::string dev_id) +{ + if (device_info.dev_ip.empty()) { + BOOST_LOG_TRIVIAL(warning) + << "CrealityPrintAgent::fetch_filament_info: no device IP, falling back to base agent"; + return MoonrakerPrinterAgent::fetch_filament_info(std::move(dev_id)); + } + + // Build a CrealityPrint helper so we can use its model detection + WS helpers + // (added in upstream PR #13291). + DynamicPrintConfig cfg; + cfg.set_key_value("print_host", new ConfigOptionString("http://" + device_info.dev_ip)); + cfg.set_key_value("print_host_webui", new ConfigOptionString("")); + cfg.set_key_value("printhost_cafile", new ConfigOptionString("")); + cfg.set_key_value("printhost_port", new ConfigOptionString("")); + cfg.set_key_value("printhost_apikey", new ConfigOptionString(device_info.api_key)); + cfg.set_key_value("printhost_ssl_ignore_revoke", new ConfigOptionBool(false)); + + CrealityPrint host(&cfg); + + // Defer to base if this isn't a K-series board with CFS firmware support. + if (!host.supports_multi_color_print()) { + BOOST_LOG_TRIVIAL(info) + << "CrealityPrintAgent: " << host.model_name() + << " is not CFS-capable, deferring to base Moonraker agent"; + return MoonrakerPrinterAgent::fetch_filament_info(std::move(dev_id)); + } + + BOOST_LOG_TRIVIAL(info) + << "CrealityPrintAgent: querying CFS slots on " << host.model_name(); + + const std::string response = host.query_boxes_info(); + + std::vector slots; + int box_count = 0; + std::string parse_err; + if (!parse_cfs_response(response, slots, box_count, parse_err)) { + BOOST_LOG_TRIVIAL(warning) + << "CrealityPrintAgent: CFS query failed (" << parse_err << "), " + << "falling back to base agent"; + return MoonrakerPrinterAgent::fetch_filament_info(std::move(dev_id)); + } + + if (box_count == 0) { + // No active CFS boxes attached — printer is in direct-spool mode. Let the + // base agent take over so the user still gets whatever filament info + // Moonraker exposes. + BOOST_LOG_TRIVIAL(info) + << "CrealityPrintAgent: no active CFS boxes, deferring to base agent"; + return MoonrakerPrinterAgent::fetch_filament_info(std::move(dev_id)); + } + + BOOST_LOG_TRIVIAL(info) + << "CrealityPrintAgent: " << box_count << " CFS box(es), " + << slots.size() << " loaded slot(s)"; + + // Index loaded slots by (box, slot) for O(1) lookup as we walk the full + // box_count * 4 grid, emitting an AmsTrayData entry for each physical slot. + std::map, const CFSSlot*> by_position; + for (const auto& s : slots) + by_position[{s.box_id, s.slot_id}] = &s; + + auto* bundle = GUI::wxGetApp().preset_bundle; + + const int max_slots = box_count * 4; + std::vector trays; + trays.reserve(max_slots); + + for (int box = 0; box < box_count; ++box) { + for (int idx = 0; idx < 4; ++idx) { + AmsTrayData tray; + tray.slot_index = box * 4 + idx; + + auto it = by_position.find({box, idx}); + if (it == by_position.end()) { + tray.has_filament = false; + trays.push_back(std::move(tray)); + continue; + } + + const CFSSlot& s = *it->second; + tray.has_filament = true; + tray.tray_type = normalize_filament_type(s.filament_type); + tray.tray_color = s.color_hex; + + if (bundle) { + tray.tray_info_idx = match_filament_preset( + bundle->filaments, s.vendor, s.brand_name, tray.tray_type); + } + + trays.push_back(std::move(tray)); + } + } + + build_ams_payload(box_count, max_slots - 1, trays); + return true; +} + +} // namespace Slic3r diff --git a/src/slic3r/Utils/CrealityPrintAgent.hpp b/src/slic3r/Utils/CrealityPrintAgent.hpp new file mode 100644 index 0000000000..e1096db536 --- /dev/null +++ b/src/slic3r/Utils/CrealityPrintAgent.hpp @@ -0,0 +1,67 @@ +#ifndef __CREALITY_PRINT_AGENT_HPP__ +#define __CREALITY_PRINT_AGENT_HPP__ + +#include "MoonrakerPrinterAgent.hpp" + +#include +#include + +namespace Slic3r { + +class PresetCollection; + +// Filament sync for Creality K-series printers with CFS. +// +// Inherits MoonrakerPrinterAgent for all communication / certificates / discovery / +// binding / print-job operations. Overrides fetch_filament_info() to query the +// K-series CFS over its port-9999 WebSocket, convert each loaded slot to an +// AmsTrayData entry, and publish via the base-class build_ams_payload() — the +// same shape used by QidiPrinterAgent and SnapmakerPrinterAgent. +// +// Model detection delegated to CrealityPrint::supports_multi_color_print() (PR #13291). +// For non-CFS K-series boards or when the WS query fails, falls back to the base +// MoonrakerPrinterAgent behaviour. + +class CrealityPrintAgent final : public MoonrakerPrinterAgent +{ +public: + struct CFSSlot + { + int box_id = 0; // CFS unit index (0 for first box, 1 for chained second box) + int slot_id = 0; // Slot index within the box (0..3) + std::string color_hex; // "#RRGGBB" + std::string filament_type; // "PLA", "ABS", "PETG", ... + std::string brand_name; // "Hyper PLA", ... + std::string vendor; // "Creality", "eSUN", or "" if unknown + }; + + explicit CrealityPrintAgent(std::string log_dir); + ~CrealityPrintAgent() override = default; + + static AgentInfo get_agent_info_static(); + AgentInfo get_agent_info() override { return get_agent_info_static(); } + + bool fetch_filament_info(std::string dev_id) override; + + // Parse the boxsInfo JSON returned by CrealityPrint::query_boxes_info() into + // a flat list of loaded slots, plus the count of CFS boxes the printer reports. + static bool parse_cfs_response(const std::string& response, + std::vector& slots, + int& box_count, + std::string& error); + + // Strip PLA/PETG/... subtype suffixes ("PLA Silk", "PLA+", "ABS Pro") to base + // type so the preset_bundle->filaments.filament_id_by_type() lookup succeeds. + static std::string normalize_filament_type(const std::string& filament_type); + + // Score visible compatible filament presets against the CFS spool metadata and + // return the best-matching filament_id. See implementation for scoring details. + static std::string match_filament_preset(const PresetCollection& filaments, + const std::string& vendor, + const std::string& brand_name, + const std::string& base_type); +}; + +} // namespace Slic3r + +#endif diff --git a/src/slic3r/Utils/ElegooLink.cpp b/src/slic3r/Utils/ElegooLink.cpp index 6a72d48373..72276bf7b2 100644 --- a/src/slic3r/Utils/ElegooLink.cpp +++ b/src/slic3r/Utils/ElegooLink.cpp @@ -170,77 +170,12 @@ namespace Slic3r { } } - std::string get_host_from_url(const std::string& url_in) - { - std::string url = url_in; - // add http:// if there is no scheme - size_t double_slash = url.find("//"); - if (double_slash == std::string::npos) - url = "http://" + url; - std::string out = url; - CURLU* hurl = curl_url(); - if (hurl) { - // Parse the input URL. - CURLUcode rc = curl_url_set(hurl, CURLUPART_URL, url.c_str(), 0); - if (rc == CURLUE_OK) { - // Replace the address. - char* host; - rc = curl_url_get(hurl, CURLUPART_HOST, &host, 0); - if (rc == CURLUE_OK) { - char* port; - rc = curl_url_get(hurl, CURLUPART_PORT, &port, 0); - if (rc == CURLUE_OK && port != nullptr) { - out = std::string(host) + ":" + port; - curl_free(port); - } else { - out = host; - curl_free(host); - } - } - else - BOOST_LOG_TRIVIAL(error) << "ElegooLink get_host_from_url: failed to get host form URL " << url; - } - else - BOOST_LOG_TRIVIAL(error) << "ElegooLink get_host_from_url: failed to parse URL " << url; - curl_url_cleanup(hurl); - } - else - BOOST_LOG_TRIVIAL(error) << "ElegooLink get_host_from_url: failed to allocate curl_url"; - return out; - } - - std::string get_host_from_url_no_port(const std::string& url_in) - { - std::string url = url_in; - // add http:// if there is no scheme - size_t double_slash = url.find("//"); - if (double_slash == std::string::npos) - url = "http://" + url; - std::string out = url; - CURLU* hurl = curl_url(); - if (hurl) { - // Parse the input URL. - CURLUcode rc = curl_url_set(hurl, CURLUPART_URL, url.c_str(), 0); - if (rc == CURLUE_OK) { - // Replace the address. - char* host; - rc = curl_url_get(hurl, CURLUPART_HOST, &host, 0); - if (rc == CURLUE_OK) { - out = host; - curl_free(host); - } - else - BOOST_LOG_TRIVIAL(error) << "ElegooLink get_host_from_url: failed to get host form URL " << url; - } - else - BOOST_LOG_TRIVIAL(error) << "ElegooLink get_host_from_url: failed to parse URL " << url; - curl_url_cleanup(hurl); - } - else - BOOST_LOG_TRIVIAL(error) << "ElegooLink get_host_from_url: failed to allocate curl_url"; - return out; - } - + // NOTE (merge): host parsing was moved into Http::get_host_from_url / + // Http::get_host_header_value by the K2 discovery refactor on this branch, so the + // former ElegooLink-local get_host_from_url/get_host_from_url_no_port helpers are gone. + // main only added the CC2 serial-number lookup below; it is kept here and routed through + // Http::get_host_header_value, which has the same host:port semantics the SN cache key + // relies on. std::string lookup_cc2_serial_impl(const std::string& printer_model, const std::string& print_host, const std::string& apikey) @@ -248,7 +183,7 @@ namespace Slic3r { if (classify_printer_model(printer_model) != ElegooPrinterType::CC2) return {}; - const std::string host_ip = get_host_from_url(print_host); + const std::string host_ip = Http::get_host_header_value(print_host); const std::string token = get_cc2_token(apikey); std::string sn = lookup_sn(host_ip, token); if (sn.empty()) @@ -408,9 +343,8 @@ namespace Slic3r { std::string web_path = resources_dir() + "/web/elegoolink/lan_service_web/index.html"; std::replace(web_path.begin(), web_path.end(), '\\', '/'); web_path = "file://" + web_path; - const std::string token = get_cc2_token(config->opt_string("printhost_apikey")); - const std::string host_ip = get_host_from_url(host); + const std::string host_ip = Http::get_host_header_value(host); // Pass sn= so the panel can subscribe to the correct MQTT topics. std::string sn = lookup_cc2_serial(config); @@ -551,7 +485,7 @@ namespace Slic3r { msg = format_error(body, error_message.empty() ? "CC2 device not detected" : error_message, status); return; } - persist_sn(get_host_from_url(m_host), token, serial_number); + persist_sn(Http::get_host_header_value(m_host), token, serial_number); res = true; }) #ifdef WIN32 @@ -574,7 +508,6 @@ namespace Slic3r { // Msg contains ip string. auto url = substitute_host(make_url(""), GUI::into_u8(msg)); msg.Clear(); - std::string host = get_host_from_url(m_host); auto http = Http::get(url); // std::move(url)); // "Host" header is necessary here. We have resolved IP address and subsituted it into "url" variable. // And when creating Http object above, libcurl automatically includes "Host" header from address it got. @@ -582,7 +515,7 @@ namespace Slic3r { // Not changing the host would work on the most cases (where there is 1 service on 1 hostname) but would break when f.e. reverse // proxy is used (issue #9734). Also when allow_ip_resolve = 0, this is not needed, but it should not break anything if it stays. // https://www.rfc-editor.org/rfc/rfc7230#section-5.4 - http.header("Host", host); + http.header("Host", Http::get_host_header_value(m_host)); set_auth(http); http.on_error([&](std::string body, std::string error, unsigned status) { BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error getting version at %2% : %3%, HTTP %4%, body: `%5%`") % name % url % @@ -626,11 +559,10 @@ namespace Slic3r { bool res = true; const auto token = cc2_token(); auto url = substitute_host(make_cc2_info_url(), GUI::into_u8(msg)); - std::string host_header = get_host_from_url(m_host); auto http = Http::get(url); msg.Clear(); - http.header("Host", host_header); + http.header("Host", Http::get_host_header_value(m_host)); http.header("X-Token", token); http.header("Accept", "application/json"); http.on_error([&](std::string body, std::string error, unsigned status) { @@ -689,7 +621,7 @@ namespace Slic3r { std::string url = substitute_host(make_cc2_upload_url(), resolved_addr.to_string()); info_fn(L"resolve", boost::nowide::widen(url)); - return loopUploadCC2(url, get_host_from_url(m_host), std::move(upload_data), prorgess_fn, error_fn, info_fn); + return loopUploadCC2(url, Http::get_host_header_value(m_host), std::move(upload_data), prorgess_fn, error_fn, info_fn); } wxString legacy_msg = GUI::from_u8(resolved_addr.to_string()); @@ -735,7 +667,7 @@ namespace Slic3r { } #endif // _WIN32 - return loopUploadCC2(url, get_host_from_url(m_host), std::move(upload_data), prorgess_fn, error_fn, info_fn); + return loopUploadCC2(url, Http::get_host_header_value(m_host), std::move(upload_data), prorgess_fn, error_fn, info_fn); } wxString legacy_msg; @@ -874,8 +806,7 @@ namespace Slic3r { // on the most cases (where there is 1 service on 1 hostname) but would break when f.e. reverse proxy is used (issue #9734). Also // when allow_ip_resolve = 0, this is not needed, but it should not break anything if it stays. // https://www.rfc-editor.org/rfc/rfc7230#section-5.4 - std::string host = get_host_from_url(m_host); - http.header("Host", host); + http.header("Host", Http::get_host_header_value(m_host)); http.header("Accept", "application/json, text/plain, */*"); #endif // _WIN32 set_auth(http); @@ -906,7 +837,7 @@ namespace Slic3r { if (res) { if (upload_data.post_action == PrintHostPostUploadAction::StartPrint) { // connect to websocket, since the upload is successful, the file will be printed - std::string wsUrl = get_host_from_url_no_port(m_host); + std::string wsUrl = Http::get_host_from_url(m_host); WebSocketClient client; try { client.connect(wsUrl, "3030", "/websocket"); @@ -1087,7 +1018,7 @@ namespace Slic3r { #ifndef WIN32 return upload_inner_with_host(std::move(upload_data), prorgess_fn, error_fn, info_fn); #else - std::string host = get_host_from_url(m_host); + std::string host = Http::get_host_from_url(m_host); // decide what to do based on m_host - resolve hostname or upload to ip std::vector resolved_addr; diff --git a/src/slic3r/Utils/Http.cpp b/src/slic3r/Utils/Http.cpp index 4f5227167c..f1ff056d10 100644 --- a/src/slic3r/Utils/Http.cpp +++ b/src/slic3r/Utils/Http.cpp @@ -978,6 +978,51 @@ std::string Http::get_filename_from_url(const std::string &url) return path_url.substr(start_pos + 1, path_url.length() - start_pos - 1); } +std::string Http::get_host_from_url(const std::string &url_in, std::string *port) +{ + std::string url = url_in; + if (url.find("//") == std::string::npos) + url = "http://" + url; + + if (port) + port->clear(); + std::string out = url_in; + CURLU *hurl = curl_url(); + if (hurl) { + CURLUcode rc = curl_url_set(hurl, CURLUPART_URL, url.c_str(), 0); + if (rc == CURLUE_OK) { + char *host; + rc = curl_url_get(hurl, CURLUPART_HOST, &host, 0); + if (rc == CURLUE_OK) { + out = host; + curl_free(host); + if (port) { + char *pstr; + rc = curl_url_get(hurl, CURLUPART_PORT, &pstr, 0); + if (rc == CURLUE_OK && pstr) { + *port = pstr; + curl_free(pstr); + } + } + } else + BOOST_LOG_TRIVIAL(error) << "Http::get_host_from_url: failed to get host from URL " << url; + } else + BOOST_LOG_TRIVIAL(error) << "Http::get_host_from_url: failed to parse URL " << url; + curl_url_cleanup(hurl); + } else + BOOST_LOG_TRIVIAL(error) << "Http::get_host_from_url: failed to allocate curl_url"; + return out; +} + +std::string Http::get_host_header_value(const std::string &url) +{ + std::string port; + std::string host = get_host_from_url(url, &port); + if (!port.empty()) + host += ":" + port; + return host; +} + std::ostream& operator<<(std::ostream &os, const Http::Progress &progress) { os << "Http::Progress(" diff --git a/src/slic3r/Utils/Http.hpp b/src/slic3r/Utils/Http.hpp index 4b6967a454..a44a95e605 100644 --- a/src/slic3r/Utils/Http.hpp +++ b/src/slic3r/Utils/Http.hpp @@ -204,6 +204,8 @@ public: static std::string url_decode(const std::string &str); static std::string get_filename_from_url(const std::string &url); + static std::string get_host_from_url(const std::string &url, std::string *port = nullptr); + static std::string get_host_header_value(const std::string &url); private: Http(const std::string &url); diff --git a/src/slic3r/Utils/NetworkAgentFactory.cpp b/src/slic3r/Utils/NetworkAgentFactory.cpp index b783631efd..5f7e0e83d7 100644 --- a/src/slic3r/Utils/NetworkAgentFactory.cpp +++ b/src/slic3r/Utils/NetworkAgentFactory.cpp @@ -6,6 +6,7 @@ #include "QidiPrinterAgent.hpp" #include "SnapmakerPrinterAgent.hpp" #include "MoonrakerPrinterAgent.hpp" +#include "CrealityPrintAgent.hpp" #include #include #include @@ -133,6 +134,9 @@ void NetworkAgentFactory::register_all_agents() register_agent(); register_agent(); register_agent(); + register_agent(); // Must come BEFORE MoonrakerPrinterAgent — + // CrealityPrintAgent extends Moonraker behaviour + // for K-series boards with CFS support. register_agent(); // BBLPrinterAgent takes no constructor args, so register manually diff --git a/src/slic3r/Utils/OctoPrint.cpp b/src/slic3r/Utils/OctoPrint.cpp index 68a28afbf7..ece2680a73 100644 --- a/src/slic3r/Utils/OctoPrint.cpp +++ b/src/slic3r/Utils/OctoPrint.cpp @@ -33,45 +33,6 @@ namespace Slic3r { namespace { #ifdef WIN32 -std::string get_host_from_url(const std::string& url_in) -{ - std::string url = url_in; - // add http:// if there is no scheme - size_t double_slash = url.find("//"); - if (double_slash == std::string::npos) - url = "http://" + url; - std::string out = url; - CURLU* hurl = curl_url(); - if (hurl) { - // Parse the input URL. - CURLUcode rc = curl_url_set(hurl, CURLUPART_URL, url.c_str(), 0); - if (rc == CURLUE_OK) { - // Replace the address. - char* host; - rc = curl_url_get(hurl, CURLUPART_HOST, &host, 0); - if (rc == CURLUE_OK) { - char* port; - rc = curl_url_get(hurl, CURLUPART_PORT, &port, 0); - if (rc == CURLUE_OK && port != nullptr) { - out = std::string(host) + ":" + port; - curl_free(port); - } else { - out = host; - curl_free(host); - } - } - else - BOOST_LOG_TRIVIAL(error) << "OctoPrint get_host_from_url: failed to get host form URL " << url; - } - else - BOOST_LOG_TRIVIAL(error) << "OctoPrint get_host_from_url: failed to parse URL " << url; - curl_url_cleanup(hurl); - } - else - BOOST_LOG_TRIVIAL(error) << "OctoPrint get_host_from_url: failed to allocate curl_url"; - return out; -} - // Workaround for Windows 10/11 mDNS resolve issue, where two mDNS resolves in succession fail. std::string substitute_host(const std::string& orig_addr, std::string sub_addr) { @@ -186,7 +147,6 @@ bool OctoPrint::test_with_resolved_ip(wxString &msg) const BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Get version at: %2%") % name % url; - std::string host = get_host_from_url(m_host); auto http = Http::get(url);//std::move(url)); // "Host" header is necessary here. We have resolved IP address and subsituted it into "url" variable. // And when creating Http object above, libcurl automatically includes "Host" header from address it got. @@ -194,7 +154,7 @@ bool OctoPrint::test_with_resolved_ip(wxString &msg) const // Not changing the host would work on the most cases (where there is 1 service on 1 hostname) but would break when f.e. reverse proxy is used (issue #9734). // Also when allow_ip_resolve = 0, this is not needed, but it should not break anything if it stays. // https://www.rfc-editor.org/rfc/rfc7230#section-5.4 - http.header("Host", host); + http.header("Host", Http::get_host_header_value(m_host)); set_auth(http); http .on_error([&](std::string body, std::string error, unsigned status) { @@ -306,7 +266,7 @@ bool OctoPrint::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, Erro #ifndef WIN32 return upload_inner_with_host(std::move(upload_data), prorgess_fn, error_fn, info_fn); #else - std::string host = get_host_from_url(m_host); + std::string host = Http::get_host_from_url(m_host); // decide what to do based on m_host - resolve hostname or upload to ip std::vector resolved_addr; @@ -393,14 +353,13 @@ bool OctoPrint::upload_inner_with_resolved_ip(PrintHostUpload upload_data, Progr % upload_parent_path.string() % (upload_data.post_action == PrintHostPostUploadAction::StartPrint ? "true" : "false"); - std::string host = get_host_from_url(m_host); auto http = Http::post(url);//std::move(url)); // "Host" header is necessary here. We have resolved IP address and subsituted it into "url" variable. // And when creating Http object above, libcurl automatically includes "Host" header from address it got. // Thus "Host" is set to the resolved IP instead of host filled by user. We need to change it back. // Not changing the host would work on the most cases (where there is 1 service on 1 hostname) but would break when f.e. reverse proxy is used (issue #9734). // https://www.rfc-editor.org/rfc/rfc7230#section-5.4 - http.header("Host", host); + http.header("Host", Http::get_host_header_value(m_host)); set_auth(http); http.form_add("print", upload_data.post_action == PrintHostPostUploadAction::StartPrint ? "true" : "false") .form_add("path", upload_parent_path.string()) // XXX: slashes on windows ??? @@ -486,8 +445,7 @@ bool OctoPrint::upload_inner_with_host(PrintHostUpload upload_data, ProgressFn p // Not changing the host would work on the most cases (where there is 1 service on 1 hostname) but would break when f.e. reverse proxy is used (issue #9734). // Also when allow_ip_resolve = 0, this is not needed, but it should not break anything if it stays. // https://www.rfc-editor.org/rfc/rfc7230#section-5.4 - std::string host = get_host_from_url(m_host); - http.header("Host", host); + http.header("Host", Http::get_host_header_value(m_host)); #endif // _WIN32 set_auth(http); http.form_add("print", upload_data.post_action == PrintHostPostUploadAction::StartPrint ? "true" : "false") @@ -884,7 +842,6 @@ bool PrusaLink::test_with_resolved_ip_and_method_check(wxString& msg, bool& use_ BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Get version at: %2%") % name % url; - std::string host = get_host_from_url(m_host); auto http = Http::get(url);//std::move(url)); // "Host" header is necessary here. We have resolved IP address and subsituted it into "url" variable. // And when creating Http object above, libcurl automatically includes "Host" header from address it got. @@ -892,7 +849,7 @@ bool PrusaLink::test_with_resolved_ip_and_method_check(wxString& msg, bool& use_ // Not changing the host would work on the most cases (where there is 1 service on 1 hostname) but would break when f.e. reverse proxy is used (issue #9734). // Also when allow_ip_resolve = 0, this is not needed, but it should not break anything if it stays. // https://www.rfc-editor.org/rfc/rfc7230#section-5.4 - http.header("Host", host); + http.header("Host", Http::get_host_header_value(m_host)); set_auth(http); http .on_error([&](std::string body, std::string error, unsigned status) { @@ -1053,8 +1010,7 @@ bool PrusaLink::put_inner(PrintHostUpload upload_data, std::string url, const st // Thus "Host" is set to the resolved IP instead of host filled by user. We need to change it back. // Not changing the host would work on the most cases (where there is 1 service on 1 hostname) but would break when f.e. reverse proxy is used (issue #9734). // https://www.rfc-editor.org/rfc/rfc7230#section-5.4 - std::string host = get_host_from_url(m_host); - http.header("Host", host); + http.header("Host", Http::get_host_header_value(m_host)); #endif // _WIN32 set_auth(http); // This is ugly, but works. There was an error at PrusaLink side that accepts any string at Print-After-Upload as true, thus False was also triggering print after upload. @@ -1103,8 +1059,7 @@ bool PrusaLink::post_inner(PrintHostUpload upload_data, std::string url, const s // Thus "Host" is set to the resolved IP instead of host filled by user. We need to change it back. // Not changing the host would work on the most cases (where there is 1 service on 1 hostname) but would break when f.e. reverse proxy is used (issue #9734). // https://www.rfc-editor.org/rfc/rfc7230#section-5.4 - std::string host = get_host_from_url(m_host); - http.header("Host", host); + http.header("Host", Http::get_host_header_value(m_host)); #endif // _WIN32 set_auth(http); set_http_post_header_args(http, upload_data.post_action); diff --git a/src/slic3r/Utils/PrintHost.cpp b/src/slic3r/Utils/PrintHost.cpp index 7f69d5e087..68720c0b9c 100644 --- a/src/slic3r/Utils/PrintHost.cpp +++ b/src/slic3r/Utils/PrintHost.cpp @@ -92,6 +92,10 @@ std::string PrintHost::get_print_host_webui(DynamicPrintConfig* config) webui_url = ElegooLink::get_print_host_webui(config); break; } + case htCrealityPrint: { + webui_url = CrealityPrint::get_print_host_webui(config); + break; + } default: break; }