diff --git a/lib/Slic3r/GUI/MainFrame.pm b/lib/Slic3r/GUI/MainFrame.pm index 034cc2dd5..ee5ec7cb5 100644 --- a/lib/Slic3r/GUI/MainFrame.pm +++ b/lib/Slic3r/GUI/MainFrame.pm @@ -413,8 +413,7 @@ sub _init_menubar { my $hokusMenu = Wx::Menu->new; # XXX: tmp { $self->_append_menu_item($hokusMenu, "Pokus", "Pokus", sub { - # Slic3r::Http::download(); - Slic3r::OctoPrint::send_gcode("10.0.0.46", "70E4CFD0E0D7423CB6B1CF055DBAEFA5", "/home/vojta/prog/tisk/jesterka/jesterka.gcode"); + Slic3r::Http::pokus(); }); } diff --git a/xs/CMakeLists.txt b/xs/CMakeLists.txt index b73f8336d..d8ef51caa 100644 --- a/xs/CMakeLists.txt +++ b/xs/CMakeLists.txt @@ -203,6 +203,8 @@ add_library(libslic3r_gui STATIC ${LIBDIR}/slic3r/Utils/Http.hpp ${LIBDIR}/slic3r/Utils/OctoPrint.cpp ${LIBDIR}/slic3r/Utils/OctoPrint.hpp + ${LIBDIR}/slic3r/Utils/Bonjour.cpp + ${LIBDIR}/slic3r/Utils/Bonjour.hpp ) add_library(admesh STATIC diff --git a/xs/src/slic3r/Utils/Bonjour.cpp b/xs/src/slic3r/Utils/Bonjour.cpp new file mode 100644 index 000000000..d7fb30e64 --- /dev/null +++ b/xs/src/slic3r/Utils/Bonjour.cpp @@ -0,0 +1,602 @@ +#include "Bonjour.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using boost::optional; +using boost::system::error_code; +namespace endian = boost::endian; +namespace asio = boost::asio; +using boost::asio::ip::udp; + + +// TODO: Fuzzing test + +namespace Slic3r { + + +// Miniman implementation of a MDNS client +// This implementation is extremely simple, only the bits that are useful +// for very basic MDNS discovery are present. + +struct DnsName: public std::string +{ + enum + { + MAX_RECURSION = 10, // Keep this low + }; + + static optional decode(const std::vector &buffer, ptrdiff_t &offset, unsigned depth = 0) + { + // We trust that the offset passed is bounds-checked properly, + // including that there is at least one byte beyond that offset. + // Any further arithmetic has to be bounds-checked here though. + + // Check for recursion depth to prevent parsing names that are nested too deeply + // or end up cyclic: + if (depth >= MAX_RECURSION) { + return boost::none; + } + + DnsName res; + const ptrdiff_t bsize = buffer.size(); + + while (true) { + const char* ptr = buffer.data() + offset; + char len = *ptr; + if (len & 0xc0) { + // This is a recursive label + ptrdiff_t pointer = (len & 0x3f) << 8 | ptr[1]; + const auto nested = decode(buffer, pointer, depth + 1); + if (!nested) { + return boost::none; + } else { + if (res.size() > 0) { + res.push_back('.'); + } + res.append(*nested); + offset += 2; + return std::move(res); + } + } else if (len == 0) { + // This is a name terminator + offset++; + break; + } else { + // This is a regular label + len &= 0x3f; + if (len + offset + 1 >= bsize) { + return boost::none; + } + + res.reserve(len); + if (res.size() > 0) { + res.push_back('.'); + } + + ptr++; + for (const auto end = ptr + len; ptr < end; ptr++) { + char c = *ptr; + if (c >= 0x20 && c <= 0x7f) { + res.push_back(c); + } else { + return boost::none; + } + } + + offset += len + 1; + } + } + + if (res.size() > 0) { + return std::move(res); + } else { + return boost::none; + } + } +}; + +struct DnsHeader +{ + uint16_t id; + uint16_t flags; + uint16_t qdcount; + uint16_t ancount; + uint16_t nscount; + uint16_t arcount; + + enum + { + SIZE = 12, + }; + + static DnsHeader decode(const std::vector &buffer) { + DnsHeader res; + const uint16_t *data_16 = reinterpret_cast(buffer.data()); + res.id = endian::big_to_native(data_16[0]); + res.flags = endian::big_to_native(data_16[1]); + res.qdcount = endian::big_to_native(data_16[2]); + res.ancount = endian::big_to_native(data_16[3]); + res.nscount = endian::big_to_native(data_16[4]); + res.arcount = endian::big_to_native(data_16[5]); + return res; + } + + uint32_t rrcount() const { + return ancount + nscount + arcount; + } +}; + +struct DnsQuestion +{ + enum + { + MIN_SIZE = 5, + }; + + DnsName name; + uint16_t type; + uint16_t qclass; + + DnsQuestion() : + type(0), + qclass(0) + {} + + static optional decode(const std::vector &buffer, ptrdiff_t &offset) + { + auto qname = DnsName::decode(buffer, offset); + if (!qname) { + return boost::none; + } + + DnsQuestion res; + res.name = std::move(*qname); + const uint16_t *data_16 = reinterpret_cast(buffer.data() + offset); + res.type = endian::big_to_native(data_16[0]); + res.qclass = endian::big_to_native(data_16[1]); + + offset += 4; + return std::move(res); + } +}; + +struct DnsResource +{ + DnsName name; + uint16_t type; + uint16_t rclass; + uint32_t ttl; + std::vector data; + + DnsResource() : + type(0), + rclass(0), + ttl(0) + {} + + static optional decode(const std::vector &buffer, ptrdiff_t &offset, ptrdiff_t &dataoffset) + { + auto rname = DnsName::decode(buffer, offset); + if (!rname) { + return boost::none; + } + + const ptrdiff_t bsize = buffer.size(); + + if (offset + 10 >= bsize) { + return boost::none; + } + + DnsResource res; + res.name = std::move(*rname); + const uint16_t *data_16 = reinterpret_cast(buffer.data() + offset); + res.type = endian::big_to_native(data_16[0]); + res.rclass = endian::big_to_native(data_16[1]); + res.ttl = endian::big_to_native(*reinterpret_cast(data_16 + 2)); + uint16_t rdlength = endian::big_to_native(data_16[4]); + + offset += 10; + if (offset + rdlength > bsize) { + return boost::none; + } + + dataoffset = offset; + res.data = std::move(std::vector(buffer.begin() + offset, buffer.begin() + offset + rdlength)); + offset += rdlength; + + return std::move(res); + } +}; + +struct DnsRR_A +{ + enum { TAG = 0x1 }; + + asio::ip::address_v4 ip; + + static void decode(optional &result, const DnsResource &rr) + { + if (rr.data.size() == 4) { + DnsRR_A res; + const uint32_t ip = endian::big_to_native(*reinterpret_cast(rr.data.data())); + res.ip = asio::ip::address_v4(ip); + result = std::move(res); + } + } +}; + +struct DnsRR_AAAA +{ + enum { TAG = 0x1c }; + + asio::ip::address_v6 ip; + + static void decode(optional &result, const DnsResource &rr) + { + if (rr.data.size() == 16) { + DnsRR_AAAA res; + std::array ip; + std::copy_n(rr.data.begin(), 16, ip.begin()); + res.ip = asio::ip::address_v6(ip); + result = std::move(res); + } + } +}; + +struct DnsRR_SRV +{ + enum + { + TAG = 0x21, + MIN_SIZE = 8, + }; + + uint16_t priority; + uint16_t weight; + uint16_t port; + std::string name; + std::string service; + DnsName hostname; + + static void decode(std::vector &results, const std::vector &buffer, const DnsResource &rr, ptrdiff_t dataoffset) + { + if (rr.data.size() < MIN_SIZE) { + return; + } + + DnsRR_SRV res; + + { + const auto dot_pos = rr.name.find_first_of('.'); + if (dot_pos > 0 && dot_pos < rr.name.size() - 1) { + res.name = rr.name.substr(0, dot_pos); + res.service = rr.name.substr(dot_pos + 1); + } else { + return; + } + } + + const uint16_t *data_16 = reinterpret_cast(rr.data.data()); + res.priority = endian::big_to_native(data_16[0]); + res.weight = endian::big_to_native(data_16[1]); + res.port = endian::big_to_native(data_16[2]); + + ptrdiff_t offset = dataoffset + 6; + auto hostname = DnsName::decode(buffer, offset); + + if (hostname) { + res.hostname = std::move(*hostname); + results.emplace_back(std::move(res)); + } + } +}; + +struct DnsMessage +{ + enum + { + MAX_SIZE = 4096, + MAX_ANS = 30, + }; + + DnsHeader header; + optional question; + std::vector answers; + + optional rr_a; + optional rr_aaaa; + std::vector rr_srv; + + static optional decode(const std::vector &buffer, optional id_wanted = boost::none) + { + const auto size = buffer.size(); + if (size < DnsHeader::SIZE + DnsQuestion::MIN_SIZE || size > MAX_SIZE) { + return boost::none; + } + + DnsMessage res; + res.header = DnsHeader::decode(buffer); + + if (id_wanted && *id_wanted != res.header.id) { + return boost::none; + } + + if (res.header.qdcount > 1 || res.header.ancount > MAX_ANS) { + return boost::none; + } + + ptrdiff_t offset = DnsHeader::SIZE; + if (res.header.qdcount == 1) { + res.question = DnsQuestion::decode(buffer, offset); + } + + for (unsigned i = 0; i < res.header.rrcount(); i++) { + ptrdiff_t dataoffset = 0; + auto rr = DnsResource::decode(buffer, offset, dataoffset); + if (!rr) { + return boost::none; + } else { + res.parse_rr(buffer, *rr, dataoffset); + res.answers.push_back(std::move(*rr)); + } + } + + return std::move(res); + } +private: + void parse_rr(const std::vector &buffer, const DnsResource &rr, ptrdiff_t dataoffset) + { + switch (rr.type) { + case DnsRR_A::TAG: DnsRR_A::decode(this->rr_a, rr); break; + case DnsRR_AAAA::TAG: DnsRR_AAAA::decode(this->rr_aaaa, rr); break; + case DnsRR_SRV::TAG: DnsRR_SRV::decode(this->rr_srv, buffer, rr, dataoffset); break; + } + } +}; + + +struct BonjourRequest +{ + static const asio::ip::address_v4 MCAST_IP4; + static const uint16_t MCAST_PORT; + + static const char rq_template[]; + + uint16_t id; + std::vector data; + + static optional make(const std::string &service, const std::string &protocol); + +private: + BonjourRequest(uint16_t id, std::vector &&data) : + id(id), + data(std::move(data)) + {} +}; + +const asio::ip::address_v4 BonjourRequest::MCAST_IP4{0xe00000fb}; +const uint16_t BonjourRequest::MCAST_PORT = 5353; + +const char BonjourRequest::rq_template[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x5f, 0x68, 0x74, + 0x74, 0x70, 0x04, 0x5f, 0x74, 0x63, 0x70, 0x05, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x00, 0x00, 0x0c, + 0x00, 0x01, +}; + +optional BonjourRequest::make(const std::string &service, const std::string &protocol) +{ + if (service.size() > 15 || protocol.size() > 15) { + return boost::none; + } + + std::random_device dev; + std::uniform_int_distribution dist; + uint16_t id = dist(dev); + uint16_t id_big = endian::native_to_big(id); + const char *id_char = reinterpret_cast(&id_big); + + std::vector data; + data.reserve(service.size() + 18); + + // Add the transaction ID + data.push_back(id_char[0]); + data.push_back(id_char[1]); + + // Add metadata + static const char rq_meta[] = { + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + std::copy(rq_meta, rq_meta + sizeof(rq_meta), std::back_inserter(data)); + + // Add PTR query name + data.push_back(service.size() + 1); + data.push_back('_'); + data.insert(data.end(), service.begin(), service.end()); + data.push_back(protocol.size() + 1); + data.push_back('_'); + data.insert(data.end(), protocol.begin(), protocol.end()); + + // Add the rest of PTR record + static const char ptr_tail[] = { + 0x05, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x00, 0x00, 0x0c, 0x00, 0x01, + }; + std::copy(ptr_tail, ptr_tail + sizeof(ptr_tail), std::back_inserter(data)); + + return BonjourRequest(id, std::move(data)); +} + + +// API - private part + +struct Bonjour::priv +{ + const std::string service; + const std::string protocol; + const std::string service_dn; + unsigned timeout; + uint16_t rq_id; + + std::vector buffer; + std::thread io_thread; + Bonjour::ReplyFn replyfn; + Bonjour::CompleteFn completefn; + + priv(std::string service, std::string protocol); + + void udp_receive(const error_code &error, size_t bytes); + void lookup_perform(); +}; + +Bonjour::priv::priv(std::string service, std::string protocol) : + service(std::move(service)), + protocol(std::move(protocol)), + service_dn((boost::format("_%1%._%2%.local") % this->service % this->protocol).str()), + timeout(10), + rq_id(0) +{ + buffer.resize(DnsMessage::MAX_SIZE); +} + +void Bonjour::priv::udp_receive(const error_code &error, size_t bytes) +{ + if (error || bytes == 0 || !replyfn) { + return; + } + + buffer.resize(bytes); + const auto dns_msg = DnsMessage::decode(buffer, rq_id); + if (dns_msg) { + std::string ip; + if (dns_msg->rr_a) { ip = dns_msg->rr_a->ip.to_string(); } + else if (dns_msg->rr_aaaa) { ip = dns_msg->rr_aaaa->ip.to_string(); } + + for (const auto &srv : dns_msg->rr_srv) { + if (srv.service == service_dn) { + replyfn(std::move(ip), std::move(srv.hostname), std::move(srv.name)); + } + } + } +} + +void Bonjour::priv::lookup_perform() +{ + const auto brq = BonjourRequest::make(service, protocol); + if (!brq) { + return; + } + + auto self = this; + rq_id = brq->id; + + try { + boost::asio::io_service io_service; + udp::socket socket(io_service); + socket.open(udp::v4()); + socket.set_option(udp::socket::reuse_address(true)); + udp::endpoint mcast(BonjourRequest::MCAST_IP4, BonjourRequest::MCAST_PORT); + socket.send_to(asio::buffer(brq->data), mcast); + + bool timeout = false; + asio::deadline_timer timer(io_service); + timer.expires_from_now(boost::posix_time::seconds(10)); + timer.async_wait([=, &timeout](const error_code &error) { + timeout = true; + if (self->completefn) { + self->completefn(); + } + }); + + const auto recv_handler = [=](const error_code &error, size_t bytes) { + self->udp_receive(error, bytes); + }; + socket.async_receive(asio::buffer(buffer, buffer.size()), recv_handler); + + while (io_service.run_one()) { + if (timeout) { + socket.cancel(); + } else { + buffer.resize(DnsMessage::MAX_SIZE); + socket.async_receive(asio::buffer(buffer, buffer.size()), recv_handler); + } + } + } catch (std::exception& e) { + } +} + + +// API - public part + +Bonjour::Bonjour(std::string service, std::string protocol) : + p(new priv(std::move(service), std::move(protocol))) +{} + +Bonjour::Bonjour(Bonjour &&other) : p(std::move(other.p)) {} + +Bonjour::~Bonjour() +{ + if (p && p->io_thread.joinable()) { + p->io_thread.detach(); + } +} + +Bonjour& Bonjour::set_timeout(unsigned timeout) +{ + if (p) { p->timeout = timeout; } + return *this; +} + +Bonjour& Bonjour::on_reply(ReplyFn fn) +{ + if (p) { p->replyfn = std::move(fn); } + return *this; +} + +Bonjour& Bonjour::on_complete(CompleteFn fn) +{ + if (p) { p->completefn = std::move(fn); } + return *this; +} + +Bonjour::Ptr Bonjour::lookup() +{ + auto self = std::make_shared(std::move(*this)); + + if (self->p) { + auto io_thread = std::thread([self](){ + self->p->lookup_perform(); + }); + self->p->io_thread = std::move(io_thread); + } + + return self; +} + + +void Bonjour::pokus() // XXX +{ + // auto bonjour = Bonjour("http") + // .set_timeout(15) + // .on_reply([](std::string ip, std::string host, std::string service_name) { + // std::cerr << "MDNS: " << ip << " = " << host << " : " << service_name << std::endl; + // }) + // .on_complete([](){ + // std::cerr << "MDNS lookup complete" << std::endl; + // }) + // .lookup(); +} + + +} diff --git a/xs/src/slic3r/Utils/Bonjour.hpp b/xs/src/slic3r/Utils/Bonjour.hpp new file mode 100644 index 000000000..039db3370 --- /dev/null +++ b/xs/src/slic3r/Utils/Bonjour.hpp @@ -0,0 +1,39 @@ +#ifndef slic3r_Bonjour_hpp_ +#define slic3r_Bonjour_hpp_ + +#include +#include +#include + + +namespace Slic3r { + + +/// Bonjour lookup +class Bonjour : public std::enable_shared_from_this { +private: + struct priv; +public: + typedef std::shared_ptr Ptr; + typedef std::function ReplyFn; + typedef std::function CompleteFn; + + Bonjour(std::string service, std::string protocol = "tcp"); + Bonjour(Bonjour &&other); + ~Bonjour(); + + Bonjour& set_timeout(unsigned timeout); + Bonjour& on_reply(ReplyFn fn); + Bonjour& on_complete(CompleteFn fn); + + Ptr lookup(); + + static void pokus(); // XXX: remove +private: + std::unique_ptr p; +}; + + +} + +#endif diff --git a/xs/src/slic3r/Utils/OctoPrint.cpp b/xs/src/slic3r/Utils/OctoPrint.cpp index 019a4e238..58530833b 100644 --- a/xs/src/slic3r/Utils/OctoPrint.cpp +++ b/xs/src/slic3r/Utils/OctoPrint.cpp @@ -15,90 +15,90 @@ namespace Slic3r { OctoPrint::OctoPrint(DynamicPrintConfig *config) : - host(config->opt_string("octoprint_host")), - apikey(config->opt_string("octoprint_apikey")), - cafile(config->opt_string("octoprint_cafile")) + host(config->opt_string("octoprint_host")), + apikey(config->opt_string("octoprint_apikey")), + cafile(config->opt_string("octoprint_cafile")) {} std::string OctoPrint::test() const { - // Since the request is performed synchronously here, - // it is ok to refer to `res` from within the closure - std::string res; + // Since the request is performed synchronously here, + // it is ok to refer to `res` from within the closure + std::string res; - auto http = Http::get(std::move(make_url("api/version"))); - set_auth(http); - http.on_error([&](std::string, std::string error, unsigned status) { - res = format_error(error, status); - }) - .perform_sync(); + auto http = Http::get(std::move(make_url("api/version"))); + set_auth(http); + http.on_error([&](std::string, std::string error, unsigned status) { + res = format_error(error, status); + }) + .perform_sync(); - return res; + return res; } void OctoPrint::send_gcode(int windowId, int completeEvt, int errorEvt, const std::string &filename, bool print) const { - auto http = Http::post(std::move(make_url("api/files/local"))); - set_auth(http); - http.form_add("print", print ? "true" : "false") - .form_add_file("file", filename) - .on_complete([=](std::string body, unsigned status) { - wxWindow *window = GUI::get_widget_by_id(windowId); - wxCommandEvent* evt = new wxCommandEvent(completeEvt); - evt->SetString("G-code file successfully uploaded to the OctoPrint server"); - evt->SetInt(100); - wxQueueEvent(window, evt); - }) - .on_error([=](std::string body, std::string error, unsigned status) { - wxWindow *window = GUI::get_widget_by_id(windowId); + auto http = Http::post(std::move(make_url("api/files/local"))); + set_auth(http); + http.form_add("print", print ? "true" : "false") + .form_add_file("file", filename) + .on_complete([=](std::string body, unsigned status) { + wxWindow *window = GUI::get_widget_by_id(windowId); + wxCommandEvent* evt = new wxCommandEvent(completeEvt); + evt->SetString("G-code file successfully uploaded to the OctoPrint server"); + evt->SetInt(100); + wxQueueEvent(window, evt); + }) + .on_error([=](std::string body, std::string error, unsigned status) { + wxWindow *window = GUI::get_widget_by_id(windowId); - wxCommandEvent* evt_complete = new wxCommandEvent(completeEvt); - evt_complete->SetInt(100); - wxQueueEvent(window, evt_complete); + wxCommandEvent* evt_complete = new wxCommandEvent(completeEvt); + evt_complete->SetInt(100); + wxQueueEvent(window, evt_complete); - wxCommandEvent* evt_error = new wxCommandEvent(errorEvt); - evt_error->SetString(wxString::Format("Error while uploading to the OctoPrint server: %s", format_error(error, status))); - wxQueueEvent(window, evt_error); - }) - .perform(); + wxCommandEvent* evt_error = new wxCommandEvent(errorEvt); + evt_error->SetString(wxString::Format("Error while uploading to the OctoPrint server: %s", format_error(error, status))); + wxQueueEvent(window, evt_error); + }) + .perform(); } void OctoPrint::set_auth(Http &http) const { - http.header("X-Api-Key", apikey); + http.header("X-Api-Key", apikey); - if (! cafile.empty()) { - http.ca_file(cafile); - } + if (! cafile.empty()) { + http.ca_file(cafile); + } } std::string OctoPrint::make_url(const std::string &path) const { - if (host.find("http://") == 0 || host.find("https://") == 0) { - if (host.back() == '/') { - return std::move((boost::format("%1%%2%") % host % path).str()); - } else { - return std::move((boost::format("%1%/%2%") % host % path).str()); - } - } else { - return std::move((boost::format("http://%1%/%2%") % host % path).str()); - } + if (host.find("http://") == 0 || host.find("https://") == 0) { + if (host.back() == '/') { + return std::move((boost::format("%1%%2%") % host % path).str()); + } else { + return std::move((boost::format("%1%/%2%") % host % path).str()); + } + } else { + return std::move((boost::format("http://%1%/%2%") % host % path).str()); + } } std::string OctoPrint::format_error(std::string error, unsigned status) { - if (status != 0) { - std::string res{"HTTP "}; - res.append(std::to_string(status)); + if (status != 0) { + std::string res{"HTTP "}; + res.append(std::to_string(status)); - if (status == 401) { - res.append(": Invalid API key"); - } + if (status == 401) { + res.append(": Invalid API key"); + } - return std::move(res); - } else { - return std::move(error); - } + return std::move(res); + } else { + return std::move(error); + } } diff --git a/xs/src/slic3r/Utils/OctoPrint.hpp b/xs/src/slic3r/Utils/OctoPrint.hpp index c5ed70ab5..1d70a087d 100644 --- a/xs/src/slic3r/Utils/OctoPrint.hpp +++ b/xs/src/slic3r/Utils/OctoPrint.hpp @@ -14,19 +14,19 @@ class Http; class OctoPrint { public: - OctoPrint(DynamicPrintConfig *config); + OctoPrint(DynamicPrintConfig *config); - std::string test() const; - // XXX: style - void send_gcode(int windowId, int completeEvt, int errorEvt, const std::string &filename, bool print = false) const; + std::string test() const; + // XXX: style + void send_gcode(int windowId, int completeEvt, int errorEvt, const std::string &filename, bool print = false) const; private: - std::string host; - std::string apikey; - std::string cafile; + std::string host; + std::string apikey; + std::string cafile; - void set_auth(Http &http) const; - std::string make_url(const std::string &path) const; - static std::string format_error(std::string error, unsigned status); + void set_auth(Http &http) const; + std::string make_url(const std::string &path) const; + static std::string format_error(std::string error, unsigned status); };