diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index f6a8249d4f..e86aa38702 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -898,7 +898,7 @@ target_include_directories(libslic3r_gui PRIVATE Utils ${CMAKE_CURRENT_BINARY_DI if (WIN32) target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../deps/WebView2/include) - target_link_libraries(libslic3r_gui Advapi32) + target_link_libraries(libslic3r_gui Advapi32 Crypt32) endif() source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SLIC3R_GUI_SOURCES}) diff --git a/src/slic3r/GUI/Monitor.cpp b/src/slic3r/GUI/Monitor.cpp index 7352ee3dd7..50eb97f3cc 100644 --- a/src/slic3r/GUI/Monitor.cpp +++ b/src/slic3r/GUI/Monitor.cpp @@ -273,7 +273,10 @@ void MonitorPanel::select_machine(std::string machine_sn) void MonitorPanel::on_timer(wxTimerEvent& event) { - if (update_flag) { + // MediaPlayCtrl may yield the event loop while it joins its camera worker + // during window teardown. Do not let a queued monitor refresh touch panels + // that are already being destroyed. + if (!wxGetApp().is_closing() && update_flag) { update_all(); //Layout(); } diff --git a/src/slic3r/GUI/ReleaseNote.cpp b/src/slic3r/GUI/ReleaseNote.cpp index 58b3a40ca9..7853149ccf 100644 --- a/src/slic3r/GUI/ReleaseNote.cpp +++ b/src/slic3r/GUI/ReleaseNote.cpp @@ -20,14 +20,17 @@ #include #include #include +#include #include #include +#include #include "Plater.hpp" #include "BitmapCache.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "DeviceCore/DevManager.h" #include "DeviceCore/DevStorage.h" +#include "../Utils/Http.hpp" #include "md4c/src/md4c-html.h" namespace Slic3r { namespace GUI { @@ -1461,6 +1464,45 @@ InputIpAddressDialog::InputIpAddressDialog(wxWindow *parent) m_input_top_sizer->Add(0, 0, 0, wxTOP, FromDIP(4)); m_input_top_sizer->Add(m_input_area, 0, wxRIGHT | wxEXPAND, FromDIP(18)); + m_tips_cafile = new Label(ip_input_top_panel, _L("HTTPS CA File")); + m_input_cafile = new wxTextCtrl(ip_input_top_panel, wxID_ANY); + m_input_cafile->SetMinSize(wxSize(FromDIP(260), FromDIP(28))); + m_input_cafile->SetMaxSize(wxSize(FromDIP(260), FromDIP(28))); + + m_button_cafile = new Button(ip_input_top_panel, _L("Browse") + " " + dots); + m_button_cafile->SetStyle(ButtonStyle::Regular, ButtonType::Parameter); + m_button_cafile->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + static const auto filemasks = _L("Certificate files (*.crt, *.pem)|*.crt;*.pem|All files|*.*"); + wxFileDialog openFileDialog(this, _L("Open CA certificate file"), "", "", filemasks, + wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (openFileDialog.ShowModal() != wxID_CANCEL) + m_input_cafile->SetValue(openFileDialog.GetPath()); + }); + + auto cafile_input_sizer = new wxBoxSizer(wxHORIZONTAL); + cafile_input_sizer->Add(m_input_cafile, 1, wxALIGN_CENTER_VERTICAL); + cafile_input_sizer->Add(m_button_cafile, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(10)); + + m_cafile_hint = new Label(ip_input_top_panel, _L("HTTPS CA file is optional. It is only needed if you use HTTPS with a self-signed certificate.")); + m_cafile_hint->Wrap(FromDIP(352)); + + m_input_top_sizer->Add(m_tips_cafile, 0, wxTOP | wxEXPAND, FromDIP(10)); + m_input_top_sizer->Add(cafile_input_sizer, 0, wxTOP | wxEXPAND, FromDIP(4)); + m_input_top_sizer->Add(m_cafile_hint, 0, wxTOP | wxEXPAND, FromDIP(4)); + + if (!Http::ca_file_supported()) { + m_input_cafile->Disable(); + m_button_cafile->Disable(); + m_cafile_hint->SetLabel(_L("This system uses HTTPS certificates from the system Certificate Store or Keychain. To use a custom CA file, import it there.")); + m_cafile_hint->Wrap(FromDIP(352)); + } + + if (wxGetApp().preset_bundle) { + const auto& config = wxGetApp().preset_bundle->printers.get_edited_preset().config; + if (config.has("printhost_cafile")) + m_input_cafile->SetValue(from_u8(config.opt_string("printhost_cafile"))); + } + ip_input_top_panel->SetSizer(m_input_top_sizer); ip_input_top_panel->Layout(); ip_input_top_panel->Fit(); @@ -1736,7 +1778,8 @@ void InputIpAddressDialog::set_machine_obj(MachineObject* obj) auto str_ip = m_input_ip->GetTextCtrl()->GetValue(); auto str_access_code = m_input_access_code->GetTextCtrl()->GetValue(); // ORCA enabling / disabling buttons with conditions enough to change its style - m_button_ok->Enable(isIp(str_ip.ToStdString()) && str_access_code.Length() == 8); + m_button_ok->Enable(isValidEndpoint(str_ip.ToStdString()) && + (str_access_code.IsEmpty() || str_access_code.Length() >= 8)); Layout(); Fit(); @@ -1773,19 +1816,29 @@ void InputIpAddressDialog::update_test_msg(wxString msg,bool connected) Fit(); } -bool InputIpAddressDialog::isIp(std::string ipstr) +bool InputIpAddressDialog::isValidEndpoint(std::string endpoint) { - istringstream ipstream(ipstr); - int num[4]; - char point[3]; - string end; - ipstream >> num[0] >> point[0] >> num[1] >> point[1] >> num[2] >> point[2] >> num[3] >> end; - for (int i = 0; i < 3; ++i) { - if (num[i] < 0 || num[i]>255) return false; - if (point[i] != '.') return false; - } - if (num[3] < 0 || num[3]>255) return false; - if (!end.empty()) return false; + if (endpoint.empty() || std::any_of(endpoint.begin(), endpoint.end(), [](unsigned char c) { + return std::isspace(c) != 0; + })) + return false; + + const bool has_http_scheme = endpoint.rfind("http://", 0) == 0; + const bool has_https_scheme = endpoint.rfind("https://", 0) == 0; + const auto scheme_pos = endpoint.find("://"); + if (scheme_pos != std::string::npos && !has_http_scheme && !has_https_scheme) + return false; + + std::string port; + const std::string host = Http::get_host_from_url(endpoint, &port); + if (host.empty()) + return false; + + // get_host_from_url returns its input when libcurl cannot parse it. For a + // URL with a scheme, that means a failed parse still needs to be rejected. + if (scheme_pos != std::string::npos && host == endpoint) + return false; + return true; } @@ -1801,6 +1854,8 @@ void InputIpAddressDialog::on_ok(wxMouseEvent& evt) m_trouble_shoot->Hide(); std::string str_ip = m_input_ip->GetTextCtrl()->GetValue().ToStdString(); std::string str_access_code = m_input_access_code->GetTextCtrl()->GetValue().ToStdString(); + if (str_access_code.empty()) + str_access_code = "88888888"; std::string str_name = m_input_printer_name->GetTextCtrl()->GetValue().Strip(wxString::both).ToStdString(); // Serial number should not contain lower case letters, and bambu_network plugin crashes // if user entered the wrong serial number, so we call `Upper()` here. @@ -1820,6 +1875,17 @@ void InputIpAddressDialog::on_ok(wxMouseEvent& evt) Layout(); Fit(); + if (wxGetApp().preset_bundle) { + auto& config = wxGetApp().preset_bundle->printers.get_edited_preset().config; + std::string port; + Http::get_host_from_url(str_ip, &port); + config.opt_string("print_host") = str_ip; + if (!port.empty()) + config.opt_string("printhost_port") = port; + if (Http::ca_file_supported()) + config.opt_string("printhost_cafile") = m_input_cafile->GetValue().ToStdString(); + } + token_.reset(this, nop_deleter); m_thread = new boost::thread(boost::bind(&InputIpAddressDialog::workerThreadFunc, this, str_ip, str_access_code, str_sn, str_model_id, str_name)); } @@ -1835,6 +1901,8 @@ void InputIpAddressDialog::on_send_retry() Fit(); wxString ip = m_input_ip->GetTextCtrl()->GetValue(); wxString str_access_code = m_input_access_code->GetTextCtrl()->GetValue(); + if (str_access_code.IsEmpty()) + str_access_code = "88888888"; // check support function if (!m_obj) return; @@ -2056,7 +2124,7 @@ void InputIpAddressDialog::on_text(wxCommandEvent &evt) auto str_ip = m_input_ip->GetTextCtrl()->GetValue(); auto str_access_code = m_input_access_code->GetTextCtrl()->GetValue(); - if (str_access_code.empty()) { + if (str_access_code.IsEmpty()) { str_access_code = "88888888"; } @@ -2072,7 +2140,8 @@ void InputIpAddressDialog::on_text(wxCommandEvent &evt) } // ORCA enabling / disabling buttons with conditions enough to change its style - bool enable_btns = isIp(str_ip.ToStdString()) && str_access_code.Length() == 8 && invalid_access_code; + bool valid_access_code_length = str_access_code.IsEmpty() || str_access_code.Length() >= 8; + bool enable_btns = isValidEndpoint(str_ip.ToStdString()) && valid_access_code_length && invalid_access_code; m_button_manual_setup->Enable(enable_btns); m_button_ok->Enable(enable_btns); @@ -2087,7 +2156,7 @@ InputIpAddressDialog::~InputIpAddressDialog() void InputIpAddressDialog::on_dpi_changed(const wxRect& suggested_rect) { - + m_button_cafile->Rescale(); } diff --git a/src/slic3r/GUI/ReleaseNote.hpp b/src/slic3r/GUI/ReleaseNote.hpp index cfd372bc97..0ac667ac0d 100644 --- a/src/slic3r/GUI/ReleaseNote.hpp +++ b/src/slic3r/GUI/ReleaseNote.hpp @@ -344,7 +344,7 @@ public: void update_title(wxString title); void set_machine_obj(MachineObject* obj); void update_test_msg(wxString msg, bool connected); - bool isIp(std::string ipstr); + bool isValidEndpoint(std::string endpoint); void check_ip_address_failed(int result); void on_check_ip_address_failed(wxCommandEvent& evt); void on_ok(wxMouseEvent& evt); diff --git a/src/slic3r/Utils/Http.cpp b/src/slic3r/Utils/Http.cpp index 6f43df74e4..05e1d6c77c 100644 --- a/src/slic3r/Utils/Http.cpp +++ b/src/slic3r/Utils/Http.cpp @@ -1,5 +1,6 @@ #include "Http.hpp" +#include #include #include #include @@ -14,8 +15,19 @@ #include -#ifdef OPENSSL_CERT_OVERRIDE +#include +#include #include +#include + +#ifdef _WIN32 +# ifndef NOMINMAX +# define NOMINMAX +# endif +# include +# include +// wincrypt.h uses this token for a certificate-name property identifier. +# undef X509_NAME #endif namespace fs = boost::filesystem; @@ -122,7 +134,7 @@ struct Http::priv std::string error_buffer; // Used for CURLOPT_ERRORBUFFER std::string headers; size_t limit; - bool cancel; + std::atomic_bool cancel; std::unique_ptr putFile; std::thread io_thread; @@ -260,9 +272,9 @@ int Http::priv::xfercb(void *userp, curl_off_t dltotal, curl_off_t dlnow, curl_o self->progressfn(progress, cb_cancel); } - if (cb_cancel) { self->cancel = true; } + if (cb_cancel) { self->cancel.store(true); } - return self->cancel; + return self->cancel.load(); } int Http::priv::xfercb_legacy(void *userp, double dltotal, double dlnow, double ultotal, double ulnow) @@ -473,7 +485,7 @@ void Http::priv::http_perform() if (res != CURLE_OK) { if (res == CURLE_ABORTED_BY_CALLBACK) { - if (cancel) { + if (cancel.load()) { // The abort comes from the request being cancelled programatically Progress dummyprogress(0, 0, 0, 0, std::string()); bool cancel = true; @@ -784,7 +796,7 @@ void Http::perform_sync() void Http::cancel() { - if (p) { p->cancel = true; } + if (p) { p->cancel.store(true); } } void Http::print() const @@ -939,6 +951,45 @@ std::string Http::tls_system_cert_store() return ret; } +void Http::add_platform_root_certificates(SSL_CTX* ssl_context) +{ +#ifdef _WIN32 + X509_STORE* openssl_store = SSL_CTX_get_cert_store(ssl_context); + if (!openssl_store) + throw std::runtime_error("unable to get OpenSSL certificate store"); + + const auto load_store = [&](DWORD location) { + HCERTSTORE windows_store = CertOpenStore(CERT_STORE_PROV_SYSTEM_W, 0, 0, + location | CERT_STORE_OPEN_EXISTING_FLAG | CERT_STORE_READONLY_FLAG, + L"ROOT"); + if (!windows_store) + return; + + PCCERT_CONTEXT windows_certificate = nullptr; + while ((windows_certificate = CertEnumCertificatesInStore(windows_store, windows_certificate)) != nullptr) { + const unsigned char* encoded = windows_certificate->pbCertEncoded; + X509* certificate = d2i_X509(nullptr, &encoded, static_cast(windows_certificate->cbCertEncoded)); + if (!certificate) { + ERR_clear_error(); + continue; + } + + ERR_clear_error(); + if (X509_STORE_add_cert(openssl_store, certificate) != 1) + ERR_clear_error(); + X509_free(certificate); + } + + CertCloseStore(windows_store, 0); + }; + + load_store(CERT_SYSTEM_STORE_CURRENT_USER); + load_store(CERT_SYSTEM_STORE_LOCAL_MACHINE); +#else + (void)ssl_context; +#endif +} + std::string Http::url_encode(const std::string &str) { ::CURL *curl = ::curl_easy_init(); diff --git a/src/slic3r/Utils/Http.hpp b/src/slic3r/Utils/Http.hpp index a44a95e605..87dd57d8cb 100644 --- a/src/slic3r/Utils/Http.hpp +++ b/src/slic3r/Utils/Http.hpp @@ -11,6 +11,8 @@ #include "libslic3r/Exception.hpp" #include "libslic3r_version.h" +typedef struct ssl_ctx_st SSL_CTX; + #define MAX_SIZE_TO_FILE 3*1024 namespace Slic3r { @@ -198,6 +200,10 @@ public: // Return empty string on success or error message on fail. static std::string tls_global_init(); static std::string tls_system_cert_store(); + // Add platform root certificates to a standalone OpenSSL context. This + // supplements set_default_verify_paths() on platforms where OpenSSL does + // not use the native certificate store. + static void add_platform_root_certificates(SSL_CTX* ssl_context); // converts the given string to an url_encoded_string static std::string url_encode(const std::string &str); diff --git a/src/slic3r/Utils/Moonraker.cpp b/src/slic3r/Utils/Moonraker.cpp index 73fdb8486a..f70be1906e 100644 --- a/src/slic3r/Utils/Moonraker.cpp +++ b/src/slic3r/Utils/Moonraker.cpp @@ -38,12 +38,25 @@ wxString Moonraker::get_test_failed_msg(wxString &msg) const std::string Moonraker::make_url(const std::string &path) const { - if (m_host.find("http://") == 0 || m_host.find("https://") == 0) { - if (m_host.back() == '/') - return (boost::format("%1%%2%") % m_host % path).str(); - return (boost::format("%1%/%2%") % m_host % path).str(); + const bool has_scheme = m_host.find("http://") == 0 || m_host.find("https://") == 0; + const bool use_ssl = m_host.find("https://") == 0; + std::string base = has_scheme ? m_host : ("http://" + m_host); + + const size_t authority_start = base.find("://") + 3; + const size_t authority_end = base.find('/', authority_start); + const std::string authority = base.substr(authority_start, authority_end - authority_start); + const size_t closing_bracket = authority.rfind(']'); + const bool has_port = closing_bracket != std::string::npos + ? closing_bracket + 1 < authority.size() && authority[closing_bracket + 1] == ':' + : authority.find(':') != std::string::npos; + if (!has_port) { + const std::string default_port = use_ssl ? ":7130" : ":7125"; + base.insert(authority_end == std::string::npos ? base.size() : authority_end, default_port); } - return (boost::format("http://%1%/%2%") % m_host % path).str(); + + if (base.back() == '/') + return (boost::format("%1%%2%") % base % path).str(); + return (boost::format("%1%/%2%") % base % path).str(); } void Moonraker::set_auth(Http &http) const @@ -53,6 +66,8 @@ void Moonraker::set_auth(Http &http) const // filled the user/password fields — those are PrusaLink/OctoPrint conventions. if (!m_apikey.empty()) http.header("X-Api-Key", m_apikey); + const bool use_ssl = m_host.find("https://") == 0; + http.tls_verify(use_ssl); if (!m_cafile.empty()) http.ca_file(m_cafile); }