diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 526a916833..31e1c4acff 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -874,7 +874,7 @@ static std::vector s_Preset_printer_options { "nozzle_type", "nozzle_hrc","auxiliary_fan", "nozzle_volume","upward_compatible_machine", "z_hop_types", "retract_lift_enforce","support_chamber_temp_control","support_air_filtration","printer_structure", "best_object_pos","head_wrap_detect_zone", //SoftFever - "host_type", "print_host", "printhost_apikey", + "host_type", "print_host", "printhost_apikey", "bbl_use_printhost", "print_host_webui", "printhost_cafile","printhost_port","printhost_authorization_type", "printhost_user", "printhost_password", "printhost_ssl_ignore_revoke", "thumbnails", "thumbnails_format", @@ -2941,6 +2941,7 @@ static std::vector s_PhysicalPrinter_opts { "preset_name", // temporary option to compatibility with older Slicer "preset_names", "printer_technology", + "bbl_use_printhost", "host_type", "print_host", "print_host_webui", diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 105ad7b902..f46e0aebcb 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -344,6 +344,13 @@ VendorType PresetBundle::get_current_vendor_type() return t; } +bool PresetBundle::use_bbl_network() +{ + const auto cfg = printers.get_edited_preset().config; + const bool use_bbl_network = is_bbl_vendor() && !cfg.opt_bool("bbl_use_printhost"); + return use_bbl_network; +} + //BBS: load project embedded presets PresetsConfigSubstitutions PresetBundle::load_project_embedded_presets(std::vector project_presets, ForwardCompatibilitySubstitutionRule substitution_rule) { diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index 07b49de4ed..c54ddda576 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -97,6 +97,7 @@ class PresetBundle VendorType get_current_vendor_type(); // Vendor related handy functions bool is_bbl_vendor() { return get_current_vendor_type() == VendorType::Marlin_BBL; } + bool use_bbl_network(); //BBS: project embedded preset logic PresetsConfigSubstitutions load_project_embedded_presets(std::vector project_presets, ForwardCompatibilitySubstitutionRule substitution_rule); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 21c5ffe48f..08671ede78 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -104,7 +104,8 @@ static t_config_enum_values s_keys_map_PrintHostType { { "repetier", htRepetier }, { "mks", htMKS }, { "obico", htObico }, - { "flashforge", htFlashforge} + { "flashforge", htFlashforge }, + { "simplyprint", htSimplyPrint }, }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PrintHostType) @@ -542,6 +543,13 @@ void PrintConfigDef::init_common_params() def->mode = comDevelop; def->set_default_value(new ConfigOptionStrings()); + def = this->add("bbl_use_printhost", coBool); + def->label = L("Use 3rd-party print host"); + def->tooltip = L("Allow controlling BambuLab's printer through 3rd party print hosts"); + def->mode = comAdvanced; + def->cli = ConfigOptionDef::nocli; + def->set_default_value(new ConfigOptionBool(false)); + def = this->add("print_host", coString); def->label = L("Hostname, IP or URL"); def->tooltip = L("Orca Slicer can upload G-code files to a printer host. This field should contain " @@ -3068,6 +3076,7 @@ def = this->add("filament_loading_speed", coFloats); def->enum_values.push_back("mks"); def->enum_values.push_back("obico"); def->enum_values.push_back("flashforge"); + def->enum_values.push_back("simplyprint"); def->enum_labels.push_back("PrusaLink"); def->enum_labels.push_back("PrusaConnect"); def->enum_labels.push_back("Octo/Klipper"); @@ -3078,6 +3087,7 @@ def = this->add("filament_loading_speed", coFloats); def->enum_labels.push_back("MKS"); def->enum_labels.push_back("Obico"); def->enum_labels.push_back("Flashforge"); + def->enum_labels.push_back("SimplyPrint"); def->mode = comAdvanced; def->cli = ConfigOptionDef::nocli; def->set_default_value(new ConfigOptionEnum(htOctoPrint)); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index f99e4c952b..b3739ddfc8 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -59,7 +59,7 @@ enum class FuzzySkinType { }; enum PrintHostType { - htPrusaLink, htPrusaConnect, htOctoPrint, htDuet, htFlashAir, htAstroBox, htRepetier, htMKS, htObico, htFlashforge + htPrusaLink, htPrusaConnect, htOctoPrint, htDuet, htFlashAir, htAstroBox, htRepetier, htMKS, htObico, htFlashforge, htSimplyPrint }; enum AuthorizationType { diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 3250e969ae..94a3b53e98 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -528,10 +528,17 @@ set(SLIC3R_GUI_SOURCES Utils/CalibUtils.hpp GUI/PrinterCloudAuthDialog.cpp GUI/PrinterCloudAuthDialog.hpp - Utils/Obico.cpp + Utils/Obico.cpp Utils/Obico.hpp Utils/Flashforge.cpp Utils/Flashforge.hpp + + GUI/OAuthDialog.cpp + GUI/OAuthDialog.hpp + GUI/Jobs/OAuthJob.cpp + GUI/Jobs/OAuthJob.hpp + Utils/SimplyPrint.cpp + Utils/SimplyPrint.hpp ) if (WIN32) @@ -637,4 +644,4 @@ if (UNIX AND NOT APPLE) endif () # Add a definition so that we can tell we are compiling slic3r. -target_compile_definitions(libslic3r_gui PRIVATE SLIC3R_CURRENTLY_COMPILING_GUI_MODULE) +target_compile_definitions(libslic3r_gui PRIVATE SLIC3R_CURRENTLY_COMPILING_GUI_MODULE) \ No newline at end of file diff --git a/src/slic3r/GUI/HttpServer.cpp b/src/slic3r/GUI/HttpServer.cpp index 44b6ffa4ea..9e45faf0b8 100644 --- a/src/slic3r/GUI/HttpServer.cpp +++ b/src/slic3r/GUI/HttpServer.cpp @@ -7,40 +7,184 @@ namespace Slic3r { namespace GUI { -static std::string parse_params(std::string url, std::string key) +std::string url_get_param(const std::string& url, const std::string& key) { size_t start = url.find(key); - if (start < 0) return ""; + if (start == std::string::npos) return ""; size_t eq = url.find('=', start); - if (eq < 0) return ""; + if (eq == std::string::npos) return ""; std::string key_str = url.substr(start, eq - start); if (key_str != key) return ""; start += key.size() + 1; size_t end = url.find('&', start); - if (end < 0) - return ""; + if (end == std::string::npos) end = url.length(); // Last param std::string result = url.substr(start, end - start); return result; } -std::string http_headers::get_response() +void session::start() +{ + read_first_line(); +} + +void session::stop() +{ + boost::system::error_code ignored_ec; + socket.shutdown(boost::asio::socket_base::shutdown_both, ignored_ec); + socket.close(ignored_ec); +} + +void session::read_first_line() +{ + auto self(shared_from_this()); + + async_read_until(socket, buff, '\r', [this, self](const boost::beast::error_code& e, std::size_t s) { + if (!e) { + std::string line, ignore; + std::istream stream{&buff}; + std::getline(stream, line, '\r'); + std::getline(stream, ignore, '\n'); + headers.on_read_request_line(line); + read_next_line(); + } else if (e != boost::asio::error::operation_aborted) { + server.stop(self); + } + }); +} + +void session::read_body() +{ + auto self(shared_from_this()); + + int nbuffer = 1000; + std::shared_ptr> bufptr = std::make_shared>(nbuffer); + async_read(socket, boost::asio::buffer(*bufptr, nbuffer), + [this, self](const boost::beast::error_code& e, std::size_t s) { server.stop(self); }); +} + +void session::read_next_line() +{ + auto self(shared_from_this()); + + async_read_until(socket, buff, '\r', [this, self](const boost::beast::error_code& e, std::size_t s) { + if (!e) { + std::string line, ignore; + std::istream stream{&buff}; + std::getline(stream, line, '\r'); + std::getline(stream, ignore, '\n'); + headers.on_read_header(line); + + if (line.length() == 0) { + if (headers.content_length() == 0) { + const std::string url_str = Http::url_decode(headers.get_url()); + const auto resp = server.server.m_request_handler(url_str); + std::stringstream ssOut; + resp->write_response(ssOut); + std::shared_ptr str = std::make_shared(ssOut.str()); + async_write(socket, boost::asio::buffer(str->c_str(), str->length()), + [this, self](const boost::beast::error_code& e, std::size_t s) { + std::cout << "done" << std::endl; + server.stop(self); + }); + } else { + read_body(); + } + } else { + read_next_line(); + } + } else if (e != boost::asio::error::operation_aborted) { + server.stop(self); + } + }); +} + +void HttpServer::IOServer::do_accept() +{ + acceptor.async_accept([this](boost::system::error_code ec, boost::asio::ip::tcp::socket socket) { + if (!acceptor.is_open()) { + return; + } + + if (!ec) { + const auto ss = std::make_shared(*this, std::move(socket)); + start(ss); + } + + do_accept(); + }); +} + +void HttpServer::IOServer::start(std::shared_ptr session) +{ + sessions.insert(session); + session->start(); +} + +void HttpServer::IOServer::stop(std::shared_ptr session) +{ + sessions.erase(session); + session->stop(); +} + +void HttpServer::IOServer::stop_all() +{ + for (auto s : sessions) { + s->stop(); + } + sessions.clear(); +} + + +HttpServer::HttpServer(boost::asio::ip::port_type port) : port(port) {} + +void HttpServer::start() +{ + BOOST_LOG_TRIVIAL(info) << "start_http_service..."; + start_http_server = true; + m_http_server_thread = create_thread([this] { + set_current_thread_name("http_server"); + server_ = std::make_unique(*this); + server_->acceptor.listen(); + + server_->do_accept(); + + server_->io_service.run(); + }); +} + +void HttpServer::stop() +{ + start_http_server = false; + if (server_) { + server_->acceptor.close(); + server_->stop_all(); + } + if (m_http_server_thread.joinable()) + m_http_server_thread.join(); + server_.reset(); +} + +void HttpServer::set_request_handler(const std::function(const std::string&)>& request_handler) +{ + this->m_request_handler = request_handler; +} + +std::shared_ptr HttpServer::bbl_auth_handle_request(const std::string& url) { BOOST_LOG_TRIVIAL(info) << "thirdparty_login: get_response"; - std::stringstream ssOut; - std::string url_str = Http::url_decode(url); - if (boost::contains(url_str, "access_token")) { - std::string sHTML = "

redirect to url

"; - std::string redirect_url = parse_params(url_str, "redirect_url"); - std::string access_token = parse_params(url_str, "access_token"); - std::string refresh_token = parse_params(url_str, "refresh_token"); - std::string expires_in_str = parse_params(url_str, "expires_in"); - std::string refresh_expires_in_str = parse_params(url_str, "refresh_expires_in"); - NetworkAgent* agent = wxGetApp().getAgent(); + + if (boost::contains(url, "access_token")) { + std::string redirect_url = url_get_param(url, "redirect_url"); + std::string access_token = url_get_param(url, "access_token"); + std::string refresh_token = url_get_param(url, "refresh_token"); + std::string expires_in_str = url_get_param(url, "expires_in"); + std::string refresh_expires_in_str = url_get_param(url, "refresh_expires_in"); + NetworkAgent* agent = wxGetApp().getAgent(); unsigned int http_code; - std::string http_body; - int result = agent->get_my_profile(access_token, &http_code, &http_body); + std::string http_body; + int result = agent->get_my_profile(access_token, &http_code, &http_body); if (result == 0) { std::string user_id; std::string user_name; @@ -60,91 +204,50 @@ std::string http_headers::get_response() ; } json j; - j["data"]["refresh_token"] = refresh_token; - j["data"]["token"] = access_token; - j["data"]["expires_in"] = expires_in_str; + j["data"]["refresh_token"] = refresh_token; + j["data"]["token"] = access_token; + j["data"]["expires_in"] = expires_in_str; j["data"]["refresh_expires_in"] = refresh_expires_in_str; - j["data"]["user"]["uid"] = user_id; - j["data"]["user"]["name"] = user_name; - j["data"]["user"]["account"] = user_account; - j["data"]["user"]["avatar"] = user_avatar; + j["data"]["user"]["uid"] = user_id; + j["data"]["user"]["name"] = user_name; + j["data"]["user"]["account"] = user_account; + j["data"]["user"]["avatar"] = user_avatar; agent->change_user(j.dump()); if (agent->is_user_login()) { wxGetApp().request_user_login(1); } - GUI::wxGetApp().CallAfter([this] { - wxGetApp().ShowUserLogin(false); - }); - std::string location_str = (boost::format("Location: %1%?result=success") % redirect_url).str(); - ssOut << "HTTP/1.1 302 Found" << std::endl; - ssOut << location_str << std::endl; - ssOut << "content-type: text/html" << std::endl; - ssOut << "content-length: " << sHTML.length() << std::endl; - ssOut << std::endl; - ssOut << sHTML; + GUI::wxGetApp().CallAfter([] { wxGetApp().ShowUserLogin(false); }); + std::string location_str = (boost::format("%1%?result=success") % redirect_url).str(); + return std::make_shared(location_str); } else { - std::string error_str = "get_user_profile_error_" + std::to_string(result); - std::string location_str = (boost::format("Location: %1%?result=fail&error=%2%") % redirect_url % error_str).str(); - ssOut << "HTTP/1.1 302 Found" << std::endl; - ssOut << location_str << std::endl; - ssOut << "content-type: text/html" << std::endl; - ssOut << "content-length: " << sHTML.length() << std::endl; - ssOut << std::endl; - ssOut << sHTML; + std::string error_str = "get_user_profile_error_" + std::to_string(result); + std::string location_str = (boost::format("%1%?result=fail&error=%2%") % redirect_url % error_str).str(); + return std::make_shared(location_str); } } else { - std::string sHTML = "

404 Not Found

There's nothing here.

"; - ssOut << "HTTP/1.1 404 Not Found" << std::endl; - ssOut << "content-type: text/html" << std::endl; - ssOut << "content-length: " << sHTML.length() << std::endl; - ssOut << std::endl; - ssOut << sHTML; + return std::make_shared(); } - return ssOut.str(); -} - - -void accept_and_run(boost::asio::ip::tcp::acceptor& acceptor, boost::asio::io_service& io_service) -{ - std::shared_ptr sesh = std::make_shared(io_service); - acceptor.async_accept(sesh->socket, - [sesh, &acceptor, &io_service](const boost::beast::error_code& accept_error) - { - accept_and_run(acceptor, io_service); - if (!accept_error) - { - session::interact(sesh); - } - }); } -HttpServer::HttpServer() +void HttpServer::ResponseNotFound::write_response(std::stringstream& ssOut) { - ; + const std::string sHTML = "

404 Not Found

There's nothing here.

"; + ssOut << "HTTP/1.1 404 Not Found" << std::endl; + ssOut << "content-type: text/html" << std::endl; + ssOut << "content-length: " << sHTML.length() << std::endl; + ssOut << std::endl; + ssOut << sHTML; } -void HttpServer::start() +void HttpServer::ResponseRedirect::write_response(std::stringstream& ssOut) { - BOOST_LOG_TRIVIAL(info) << "start_http_service..."; - start_http_server = true; - m_http_server_thread = Slic3r::create_thread( - [this] { - boost::asio::io_service io_service; - boost::asio::ip::tcp::endpoint endpoint{ boost::asio::ip::tcp::v4(), LOCALHOST_PORT}; - boost::asio::ip::tcp::acceptor acceptor { io_service, endpoint}; - acceptor.listen(); - accept_and_run(acceptor, io_service); - while (start_http_server) { - io_service.run(); - } - }); -} - -void HttpServer::stop() -{ - start_http_server = false; - if (m_http_server_thread.joinable()) - m_http_server_thread.join(); + const std::string sHTML = "

redirect to url

"; + ssOut << "HTTP/1.1 302 Found" << std::endl; + ssOut << "Location: " << location_str << std::endl; + ssOut << "content-type: text/html" << std::endl; + ssOut << "content-length: " << sHTML.length() << std::endl; + ssOut << std::endl; + ssOut << sHTML; } } // GUI diff --git a/src/slic3r/GUI/HttpServer.hpp b/src/slic3r/GUI/HttpServer.hpp index c545ab9b2a..99925019f2 100644 --- a/src/slic3r/GUI/HttpServer.hpp +++ b/src/slic3r/GUI/HttpServer.hpp @@ -13,14 +13,12 @@ #include #include -using namespace boost::system; -using namespace boost::asio; - #define LOCALHOST_PORT 13618 #define LOCALHOST_URL "http://localhost:" -namespace Slic3r { -namespace GUI { +namespace Slic3r { namespace GUI { + +class session; class http_headers { @@ -31,16 +29,14 @@ class http_headers std::map headers; public: - - std::string get_response(); + std::string get_url() { return url; } int content_length() { auto request = headers.find("content-length"); - if (request != headers.end()) - { + if (request != headers.end()) { std::stringstream ssLength(request->second); - int content_length; + int content_length; ssLength >> content_length; return content_length; } @@ -49,10 +45,10 @@ class http_headers void on_read_header(std::string line) { - //std::cout << "header: " << line << std::endl; + // std::cout << "header: " << line << std::endl; std::stringstream ssHeader(line); - std::string headerName; + std::string headerName; std::getline(ssHeader, headerName, ':'); std::string value; @@ -71,92 +67,92 @@ class http_headers } }; -class session +class HttpServer { - boost::asio::streambuf buff; - http_headers headers; + boost::asio::ip::port_type port; - static void read_body(std::shared_ptr pThis) +public: + class Response { - int nbuffer = 1000; - std::shared_ptr> bufptr = std::make_shared>(nbuffer); - boost::asio::async_read(pThis->socket, boost::asio::buffer(*bufptr, nbuffer), [pThis](const boost::beast::error_code& e, std::size_t s) - { - }); - } + public: + virtual ~Response() = default; + virtual void write_response(std::stringstream& ssOut) = 0; + }; - static void read_next_line(std::shared_ptr pThis) + class ResponseNotFound : public Response { - boost::asio::async_read_until(pThis->socket, pThis->buff, '\r', [pThis](const boost::beast::error_code& e, std::size_t s) - { - std::string line, ignore; - std::istream stream{ &pThis->buff }; - std::getline(stream, line, '\r'); - std::getline(stream, ignore, '\n'); - pThis->headers.on_read_header(line); - - if (line.length() == 0) - { - if (pThis->headers.content_length() == 0) - { - std::shared_ptr str = std::make_shared(pThis->headers.get_response()); - boost::asio::async_write(pThis->socket, boost::asio::buffer(str->c_str(), str->length()), [pThis, str](const boost::beast::error_code& e, std::size_t s) - { - std::cout << "done" << std::endl; - }); - } - else - { - pThis->read_body(pThis); - } - } - else - { - pThis->read_next_line(pThis); - } - }); - } + public: + ~ResponseNotFound() override = default; + void write_response(std::stringstream& ssOut) override; + }; - static void read_first_line(std::shared_ptr pThis) + class ResponseRedirect : public Response { - boost::asio::async_read_until(pThis->socket, pThis->buff, '\r', [pThis](const boost::beast::error_code& e, std::size_t s) - { - std::string line, ignore; - std::istream stream{ &pThis->buff }; - std::getline(stream, line, '\r'); - std::getline(stream, ignore, '\n'); - pThis->headers.on_read_request_line(line); - pThis->read_next_line(pThis); - }); - } + const std::string location_str; -public: - boost::asio::ip::tcp::socket socket; + public: + ResponseRedirect(const std::string& location) : location_str(location) {} + ~ResponseRedirect() override = default; + void write_response(std::stringstream& ssOut) override; + }; - session(io_service& io_service) - :socket(io_service) - { - } + HttpServer(boost::asio::ip::port_type port = LOCALHOST_PORT); + + boost::thread m_http_server_thread; + bool start_http_server = false; + + bool is_started() { return start_http_server; } + void start(); + void stop(); + void set_request_handler(const std::function(const std::string&)>& m_request_handler); + + static std::shared_ptr bbl_auth_handle_request(const std::string& url); - static void interact(std::shared_ptr pThis) +private: + class IOServer { - read_first_line(pThis); - } -}; + public: + HttpServer& server; + boost::asio::io_service io_service; + boost::asio::ip::tcp::acceptor acceptor; + std::set> sessions; -class HttpServer { -public: - HttpServer(); + IOServer(HttpServer& server) : server(server), acceptor(io_service, {boost::asio::ip::tcp::v4(), server.port}) {} + + void do_accept(); - boost::thread m_http_server_thread; - bool start_http_server = false; + void start(std::shared_ptr session); + void stop(std::shared_ptr session); + void stop_all(); + }; + friend class session; - bool is_started() { return start_http_server; } - void start(); - void stop(); + std::unique_ptr server_{nullptr}; + + std::function(const std::string&)> m_request_handler{&HttpServer::bbl_auth_handle_request}; }; -} +class session : public std::enable_shared_from_this +{ + HttpServer::IOServer& server; + boost::asio::ip::tcp::socket socket; + + boost::asio::streambuf buff; + http_headers headers; + + void read_first_line(); + void read_next_line(); + void read_body(); + +public: + session(HttpServer::IOServer& server, boost::asio::ip::tcp::socket socket) : server(server), socket(std::move(socket)) {} + + void start(); + void stop(); }; +std::string url_get_param(const std::string& url, const std::string& key); + +}}; + #endif diff --git a/src/slic3r/GUI/Jobs/OAuthJob.cpp b/src/slic3r/GUI/Jobs/OAuthJob.cpp new file mode 100644 index 0000000000..962056580e --- /dev/null +++ b/src/slic3r/GUI/Jobs/OAuthJob.cpp @@ -0,0 +1,124 @@ +#include "OAuthJob.hpp" + +#include "Http.hpp" +#include "ThreadSafeQueue.hpp" +#include "slic3r/GUI/I18N.hpp" +#include "nlohmann/json.hpp" + + +namespace Slic3r { +namespace GUI { + +wxDEFINE_EVENT(EVT_OAUTH_COMPLETE_MESSAGE, wxCommandEvent); + +OAuthJob::OAuthJob(const OAuthData& input) : local_authorization_server(input.params.callback_port), _data(input) {} + +void OAuthJob::parse_token_response(const std::string& body, bool error, OAuthResult& result) +{ + const auto j = nlohmann::json::parse(body, nullptr, false, true); + if (j.is_discarded()) { + BOOST_LOG_TRIVIAL(warning) << "Invalid or no JSON data on token response: " << body; + result.error_message = _u8L("Unknown error"); + } else if (error) { + if (j.contains("error_description")) { + j.at("error_description").get_to(result.error_message); + } else { + result.error_message = _u8L("Unknown error"); + } + } else { + j.at("access_token").get_to(result.access_token); + j.at("refresh_token").get_to(result.refresh_token); + result.success = true; + } +} + + +void OAuthJob::process(Ctl& ctl) +{ + // Prepare auth process + ThreadSafeQueueSPSC queue; + + // Setup auth server to receive OAuth code from callback url + local_authorization_server.set_request_handler([this, &queue](const std::string& url) -> std::shared_ptr { + if (boost::contains(url, "/callback")) { + const auto code = url_get_param(url, "code"); + const auto state = url_get_param(url, "state"); + + const auto handle_auth_fail = [this, &queue](const std::string& message) -> std::shared_ptr { + queue.push(OAuthResult{false, message}); + return std::make_shared(this->_data.params.auth_fail_redirect_url); + }; + + if (state != _data.params.state) { + BOOST_LOG_TRIVIAL(warning) << "The provided state was not correct. Got " << state << " and expected " << _data.params.state; + return handle_auth_fail(_u8L("The provided state is not correct.")); + } + + if (code.empty()) { + const auto error_code = url_get_param(url, "error_code"); + if (error_code == "user_denied") { + BOOST_LOG_TRIVIAL(debug) << "User did not give the required permission when authorizing this application"; + return handle_auth_fail(_u8L("Please give the required permissions when authorizing this application.")); + } + + BOOST_LOG_TRIVIAL(warning) << "Unexpected error when logging in. Error_code: " << error_code << ", State: " << state; + return handle_auth_fail(_u8L("Something unexpected happened when trying to log in, please try again.")); + } + + + OAuthResult r; + // Request the access token from the authorization server. + auto http = Http::post(_data.params.token_url); + http.timeout_connect(5) + .timeout_max(5) + .form_add("client_id", _data.params.client_id) + .form_add("redirect_uri", _data.params.callback_url) + .form_add("grant_type", "authorization_code") + .form_add("code", code) + .form_add("code_verifier", _data.params.verification_code) + .form_add("scope", _data.params.scope) + .on_complete([&](std::string body, unsigned status) { parse_token_response(body, false, r); }) + .on_error([&](std::string body, std::string error, unsigned status) { parse_token_response(body, true, r); }) + .perform_sync(); + + queue.push(r); + return std::make_shared(r.success ? _data.params.auth_success_redirect_url : + _data.params.auth_fail_redirect_url); + } else { + queue.push(OAuthResult{false}); + return std::make_shared(); + } + }); + + // Run the local server + local_authorization_server.start(); + + // Wait until we received the result + bool received = false; + while (!ctl.was_canceled() && !received ) { + queue.consume_one(BlockingWait{1000}, [this, &received](const OAuthResult& result) { + *_data.result = result; + received = true; + }); + } + + // Handle timeout + if (!received && !ctl.was_canceled()) { + BOOST_LOG_TRIVIAL(debug) << "Timeout when authenticating with the account server."; + _data.result->error_message = _u8L("Timeout when authenticating with the account server."); + } else if (ctl.was_canceled()) { + _data.result->error_message = _u8L("User cancelled."); + } +} + +void OAuthJob::finalize(bool canceled, std::exception_ptr& e) +{ + // Make sure it's stopped + local_authorization_server.stop(); + + wxCommandEvent event(EVT_OAUTH_COMPLETE_MESSAGE); + event.SetEventObject(m_event_handle); + wxPostEvent(m_event_handle, event); +} + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Jobs/OAuthJob.hpp b/src/slic3r/GUI/Jobs/OAuthJob.hpp new file mode 100644 index 0000000000..dd64e966be --- /dev/null +++ b/src/slic3r/GUI/Jobs/OAuthJob.hpp @@ -0,0 +1,62 @@ +#ifndef __OAuthJob_HPP__ +#define __OAuthJob_HPP__ + +#include "Job.hpp" +#include "slic3r/GUI/HttpServer.hpp" + +namespace Slic3r { +namespace GUI { + +class Plater; + +struct OAuthParams +{ + std::string login_url; + std::string client_id; + boost::asio::ip::port_type callback_port; + std::string callback_url; + std::string scope; + std::string response_type; + std::string auth_success_redirect_url; + std::string auth_fail_redirect_url; + std::string token_url; + std::string verification_code; + std::string state; +}; + +struct OAuthResult +{ + bool success{false}; + std::string error_message{""}; + std::string access_token{""}; + std::string refresh_token{""}; +}; + +struct OAuthData +{ + OAuthParams params; + std::shared_ptr result; +}; + +class OAuthJob : public Job +{ + HttpServer local_authorization_server; + OAuthData _data; + wxWindow* m_event_handle{nullptr}; + +public: + explicit OAuthJob(const OAuthData& input); + + void process(Ctl& ctl) override; + void finalize(bool canceled, std::exception_ptr& e) override; + + void set_event_handle(wxWindow* hanle) { m_event_handle = hanle; } + + static void parse_token_response(const std::string& body, bool error, OAuthResult& result); +}; + +wxDECLARE_EVENT(EVT_OAUTH_COMPLETE_MESSAGE, wxCommandEvent); + +}} // namespace Slic3r::GUI + +#endif // OAUTHJOB_HPP diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 94bbedf5a8..7e06f6e17d 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -562,7 +562,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ m_print_enable = get_enable_print_status(); m_print_btn->Enable(m_print_enable); if (m_print_enable) { - if (wxGetApp().preset_bundle->is_bbl_vendor()) + if (wxGetApp().preset_bundle->use_bbl_network()) wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_PRINT_PLATE)); else wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SEND_GCODE)); @@ -1600,7 +1600,7 @@ wxBoxSizer* MainFrame::create_side_tools() SidePopup* p = new SidePopup(this); if (wxGetApp().preset_bundle - && !wxGetApp().preset_bundle->is_bbl_vendor()) { + && !wxGetApp().preset_bundle->use_bbl_network()) { // ThirdParty Buttons SideButton* export_gcode_btn = new SideButton(p, _L("Export G-code file"), ""); export_gcode_btn->SetCornerRadius(0); @@ -3584,7 +3584,7 @@ void MainFrame::load_printer_url(wxString url, wxString apikey) void MainFrame::load_printer_url() { PresetBundle &preset_bundle = *wxGetApp().preset_bundle; - if (preset_bundle.is_bbl_vendor()) + if (preset_bundle.use_bbl_network()) return; auto cfg = preset_bundle.printers.get_edited_preset().config; diff --git a/src/slic3r/GUI/OAuthDialog.cpp b/src/slic3r/GUI/OAuthDialog.cpp new file mode 100644 index 0000000000..eef8fe9b10 --- /dev/null +++ b/src/slic3r/GUI/OAuthDialog.cpp @@ -0,0 +1,84 @@ +#include "OAuthDialog.hpp" + +#include "GUI_App.hpp" +#include "Jobs/BoostThreadWorker.hpp" +#include "Jobs/PlaterWorker.hpp" +#include "wxExtensions.hpp" + +namespace Slic3r { +namespace GUI { + + +#define BORDER_W FromDIP(10) + +OAuthDialog::OAuthDialog(wxWindow* parent, OAuthParams params) + : DPIDialog(parent, wxID_ANY, _L("Login"), wxDefaultPosition, wxSize(45 * wxGetApp().em_unit(), -1), wxDEFAULT_DIALOG_STYLE) + , _params(params) +{ + SetFont(wxGetApp().normal_font()); + SetBackgroundColour(*wxWHITE); + + m_worker = std::make_unique>(this, nullptr, "auth_worker"); + + wxStdDialogButtonSizer* btns = this->CreateStdDialogButtonSizer(wxCANCEL); + btnCancel = static_cast(this->FindWindowById(wxID_CANCEL, this)); + wxGetApp().UpdateDarkUI(btnCancel); + btnCancel->Bind(wxEVT_BUTTON, &OAuthDialog::on_cancel, this); + + const auto message_sizer = new wxBoxSizer(wxVERTICAL); + const auto message = new wxStaticText(this, wxID_ANY, _L("Authorizing..."), wxDefaultPosition, wxDefaultSize, 0); + message->SetForegroundColour(*wxBLACK); + message_sizer->Add(message, 0, wxEXPAND | wxLEFT | wxTOP | wxBOTTOM, BORDER_W); + + const auto topSizer = new wxBoxSizer(wxVERTICAL); + topSizer->Add(message_sizer, 0, wxEXPAND | wxALL, BORDER_W); + topSizer->Add(btns, 0, wxEXPAND | wxALL, BORDER_W); + + Bind(wxEVT_CLOSE_WINDOW, &OAuthDialog::on_cancel, this); + + SetSizer(topSizer); + topSizer->SetSizeHints(this); + this->CenterOnParent(); + wxGetApp().UpdateDlgDarkUI(this); +} + +void OAuthDialog::on_cancel(wxEvent& event) +{ + m_worker->cancel_all(); + m_worker->wait_for_idle(); + EndModal(wxID_NO); +} + +bool OAuthDialog::Show(bool show) +{ + if (show) { + // Prepare login job + _result = std::make_shared(); + auto job = std::make_unique(OAuthData{_params, _result}); + job->set_event_handle(this); + Bind(EVT_OAUTH_COMPLETE_MESSAGE, [this](wxCommandEvent& evt) { EndModal(wxID_NO); }); + + // Start auth job + replace_job(*m_worker, std::move(job)); + + // Open login URL in external browser + wxLaunchDefaultBrowser(_params.login_url); + } + + return DPIDialog::Show(show); +} + +void OAuthDialog::on_dpi_changed(const wxRect& suggested_rect) +{ + const int& em = em_unit(); + + msw_buttons_rescale(this, em, {wxID_CANCEL}); + + const wxSize& size = wxSize(45 * em, 35 * em); + SetMinSize(size); + + Fit(); + Refresh(); +} + +}} diff --git a/src/slic3r/GUI/OAuthDialog.hpp b/src/slic3r/GUI/OAuthDialog.hpp new file mode 100644 index 0000000000..55164588fb --- /dev/null +++ b/src/slic3r/GUI/OAuthDialog.hpp @@ -0,0 +1,34 @@ +#ifndef __OAuthDialog_HPP__ +#define __OAuthDialog_HPP__ + +#include "GUI_Utils.hpp" +#include "Jobs/OAuthJob.hpp" +#include "Jobs/Worker.hpp" + +namespace Slic3r { +namespace GUI { + +class OAuthDialog : public DPIDialog +{ +private: + OAuthParams _params; + std::shared_ptr _result; + + wxButton* btnCancel{nullptr}; + std::unique_ptr m_worker; + + void on_cancel(wxEvent& event); + +protected: + bool Show(bool show) override; + void on_dpi_changed(const wxRect& suggested_rect) override; + +public: + OAuthDialog(wxWindow* parent, OAuthParams params); + + OAuthResult get_result() { return *_result; } +}; + +}} // namespace Slic3r::GUI + +#endif diff --git a/src/slic3r/GUI/OptionsGroup.cpp b/src/slic3r/GUI/OptionsGroup.cpp index 81c1a4b61c..eb25999cc8 100644 --- a/src/slic3r/GUI/OptionsGroup.cpp +++ b/src/slic3r/GUI/OptionsGroup.cpp @@ -194,6 +194,13 @@ void OptionsGroup::show_field(const t_config_option_key& opt_key, bool show/* = } } +void OptionsGroup::enable_field(const t_config_option_key& opt_key, bool enable) +{ + if (Field* f = get_field(opt_key); f) { + f->toggle(enable); + } +} + void OptionsGroup::set_name(const wxString& new_name) { stb->SetLabel(new_name); diff --git a/src/slic3r/GUI/OptionsGroup.hpp b/src/slic3r/GUI/OptionsGroup.hpp index 1ec5fdc1b3..8c7585cd83 100644 --- a/src/slic3r/GUI/OptionsGroup.hpp +++ b/src/slic3r/GUI/OptionsGroup.hpp @@ -173,6 +173,9 @@ class OptionsGroup { void show_field(const t_config_option_key& opt_key, bool show = true); void hide_field(const t_config_option_key& opt_key) { show_field(opt_key, false); } + void enable_field(const t_config_option_key& opt_key, bool enable = true); + void disable_field(const t_config_option_key& opt_key) { enable_field(opt_key, false); } + void set_name(const wxString& new_name); inline void enable() { for (auto& field : m_fields) field.second->enable(); } diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.cpp b/src/slic3r/GUI/PhysicalPrinterDialog.cpp index d746f96f01..20ecc917a6 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.cpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp @@ -34,6 +34,8 @@ #include "BitmapCache.hpp" #include "BonjourDialog.hpp" #include "MsgDialog.hpp" +#include "OAuthDialog.hpp" +#include "SimplyPrint.hpp" namespace Slic3r { namespace GUI { @@ -174,20 +176,24 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr result = host->test(msg); if (!result && host->is_cloud()) { - PrinterCloudAuthDialog dlg(this->GetParent(), host.get()); - dlg.ShowModal(); - - auto api_key = dlg.GetApiKey(); - m_config->opt_string("printhost_apikey") = api_key; - result = !api_key.empty(); - if (result) { - if (Field* print_host_webui_field = this->m_optgroup->get_field("printhost_apikey"); print_host_webui_field) { - if (TextInput* temp_input = dynamic_cast(print_host_webui_field->getWindow()); temp_input) { - if (wxTextCtrl* temp = temp_input->GetTextCtrl()) { - temp->SetValue(wxString(api_key)); - } - } + if (const auto h = dynamic_cast(host.get()); h) { + OAuthDialog dlg(this, h->get_oauth_params()); + dlg.ShowModal(); + + const auto& r = dlg.get_result(); + result = r.success; + if (r.success) { + h->save_oauth_credential(r); + } else { + msg = r.error_message; } + } else { + PrinterCloudAuthDialog dlg(this->GetParent(), host.get()); + dlg.ShowModal(); + + const auto api_key = dlg.GetApiKey(); + m_config->opt_string("printhost_apikey") = api_key; + result = !api_key.empty(); } } } @@ -195,11 +201,36 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr show_info(this, host->get_test_ok_msg(), _L("Success!")); else show_error(this, host->get_test_failed_msg(msg)); + + update(); }); return sizer; }; + auto print_host_logout = [&](wxWindow* parent) { + auto sizer = create_sizer_with_btn(parent, &m_printhost_logout_btn, "", _L("Log Out")); + + m_printhost_logout_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { + std::unique_ptr host(PrintHost::get_print_host(m_config)); + if (!host) { + const wxString text = _L("Could not get a valid Printer Host reference"); + show_error(this, text); + return; + } + + wxString msg_text = _L("Are you sure to log out?"); + MessageDialog dialog(this, msg_text, "", wxICON_QUESTION | wxYES_NO); + + if (dialog.ShowModal() == wxID_YES) { + host->log_out(); + update(); + } + }); + + return sizer; + }; + auto print_host_printers = [this, create_sizer_with_btn](wxWindow* parent) { //add_scaled_button(parent, &m_printhost_port_browse_btn, "browse", _(L("Refresh Printers")), wxBU_LEFT | wxBU_EXACTFIT); auto sizer = create_sizer_with_btn(parent, &m_printhost_port_browse_btn, "monitor_signal_strong", _(L("Refresh Printers"))); @@ -215,6 +246,7 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr Line host_line = m_optgroup->create_single_option_line(option); host_line.append_widget(printhost_browse); host_line.append_widget(print_host_test); + host_line.append_widget(print_host_logout); m_optgroup->append_line(host_line); option = m_optgroup->get_option("print_host_webui"); @@ -375,7 +407,9 @@ void PhysicalPrinterDialog::update_printhost_buttons() std::unique_ptr host(PrintHost::get_print_host(m_config)); if (host) { m_printhost_test_btn->Enable(!m_config->opt_string("print_host").empty() && host->can_test()); - m_printhost_browse_btn->Enable(host->has_auto_discovery()); + m_printhost_browse_btn->Show(host->has_auto_discovery()); + m_printhost_logout_btn->Show(host->is_logged_in()); + m_printhost_test_btn->SetLabel(host->is_cloud() ? _L("Login/Test") : _L("Test")); } } @@ -469,15 +503,29 @@ void PhysicalPrinterDialog::update(bool printer_change) const auto opt = m_config->option>("host_type"); m_optgroup->show_field("host_type"); - // hide PrusaConnect address + m_optgroup->enable_field("print_host"); + m_optgroup->enable_field("print_host_webui"); + m_optgroup->enable_field("printhost_cafile"); + m_optgroup->enable_field("printhost_ssl_ignore_revoke"); + if (m_printhost_cafile_browse_btn) + m_printhost_cafile_browse_btn->Enable(); + + // hide pre-configured address, in case user switched to a different host type if (Field* printhost_field = m_optgroup->get_field("print_host"); printhost_field) { - if (wxTextCtrl* temp = dynamic_cast(printhost_field->getWindow()); temp && temp->GetValue() == L"https://connect.prusa3d.com") { - temp->SetValue(wxString()); + if (wxTextCtrl* temp = dynamic_cast(printhost_field)->text_ctrl(); temp) { + const auto current_host = temp->GetValue(); + if (current_host == L"https://connect.prusa3d.com" || + current_host == L"https://app.obico.io" || + current_host == "https://simplyprint.io") { + temp->SetValue(wxString()); + } } - - if (TextInput* temp_input = dynamic_cast(printhost_field->getWindow()); temp_input) { - if (wxTextCtrl* temp = temp_input->GetTextCtrl(); temp &&temp->GetValue() == L"https://app.obico.io") { - temp->SetValue(wxString()); + } + if (Field* printhost_webui_field = m_optgroup->get_field("print_host_webui"); printhost_webui_field) { + if (wxTextCtrl* temp = dynamic_cast(printhost_webui_field)->text_ctrl(); temp) { + const auto current_host = temp->GetValue(); + if (current_host == "https://simplyprint.io/panel") { + temp->SetValue(wxString()); } } } @@ -492,25 +540,42 @@ void PhysicalPrinterDialog::update(bool printer_change) m_optgroup->show_field("printhost_apikey", true); for (const std::string& opt_key : std::vector{ "printhost_user", "printhost_password" }) m_optgroup->hide_field(opt_key); - supports_multiple_printers = opt && opt->value == htRepetier; + supports_multiple_printers = opt->value == htRepetier || opt->value == htObico; + if (opt->value == htPrusaConnect) { // automatically show default prusaconnect address if (Field* printhost_field = m_optgroup->get_field("print_host"); printhost_field) { - if (wxTextCtrl* temp = dynamic_cast(printhost_field->getWindow()); temp && temp->GetValue().IsEmpty()) { + if (wxTextCtrl* temp = dynamic_cast(printhost_field)->text_ctrl(); temp && temp->GetValue().IsEmpty()) { temp->SetValue(L"https://connect.prusa3d.com"); } } - } - } - - if (opt->value == htObico) { - supports_multiple_printers = true; - if (Field* printhost_field = m_optgroup->get_field("print_host"); printhost_field) { - if (TextInput* temp_input = dynamic_cast(printhost_field->getWindow()); temp_input) { - if (wxTextCtrl* temp = temp_input->GetTextCtrl(); temp && temp->GetValue().IsEmpty()) { + } else if (opt->value == htObico) { // automatically show default obico address + if (Field* printhost_field = m_optgroup->get_field("print_host"); printhost_field) { + if (wxTextCtrl* temp = dynamic_cast(printhost_field)->text_ctrl(); temp && temp->GetValue().IsEmpty()) { temp->SetValue(L"https://app.obico.io"); m_config->opt_string("print_host") = "https://app.obico.io"; } } + } else if (opt->value == htSimplyPrint) { + // Set the host url + if (Field* printhost_field = m_optgroup->get_field("print_host"); printhost_field) { + printhost_field->disable(); + if (wxTextCtrl* temp = dynamic_cast(printhost_field)->text_ctrl(); temp && temp->GetValue().IsEmpty()) { + temp->SetValue("https://simplyprint.io"); + } + m_config->opt_string("print_host") = "https://simplyprint.io"; + } + if (Field* printhost_webui_field = m_optgroup->get_field("print_host_webui"); printhost_webui_field) { + printhost_webui_field->disable(); + if (wxTextCtrl* temp = dynamic_cast(printhost_webui_field)->text_ctrl(); temp && temp->GetValue().IsEmpty()) { + temp->SetValue("https://simplyprint.io/panel"); + } + m_config->opt_string("print_host_webui") = "https://simplyprint.io/panel"; + } + m_optgroup->hide_field("printhost_apikey"); + m_optgroup->disable_field("printhost_cafile"); + m_optgroup->disable_field("printhost_ssl_ignore_revoke"); + if (m_printhost_cafile_browse_btn) + m_printhost_cafile_browse_btn->Disable(); } } @@ -599,6 +664,7 @@ void PhysicalPrinterDialog::on_dpi_changed(const wxRect& suggested_rect) m_printhost_browse_btn->msw_rescale(); m_printhost_test_btn->msw_rescale(); + m_printhost_logout_btn->msw_rescale(); if (m_printhost_cafile_browse_btn) m_printhost_cafile_browse_btn->msw_rescale(); diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.hpp b/src/slic3r/GUI/PhysicalPrinterDialog.hpp index 9a38e7d8db..855370568f 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.hpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.hpp @@ -30,6 +30,7 @@ class PhysicalPrinterDialog : public DPIDialog ScalableButton* m_printhost_browse_btn {nullptr}; ScalableButton* m_printhost_test_btn {nullptr}; + ScalableButton* m_printhost_logout_btn {nullptr}; ScalableButton* m_printhost_cafile_browse_btn {nullptr}; ScalableButton* m_printhost_client_cert_browse_btn {nullptr}; ScalableButton* m_printhost_port_browse_btn {nullptr}; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 8f4700e653..6fd95de599 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1132,13 +1132,14 @@ void Sidebar::update_all_preset_comboboxes() const auto print_tech = preset_bundle.printers.get_edited_preset().printer_technology(); bool is_bbl_vendor = preset_bundle.is_bbl_vendor(); + const bool use_bbl_network = preset_bundle.use_bbl_network(); // Orca:: show device tab based on vendor type auto p_mainframe = wxGetApp().mainframe; - p_mainframe->show_device(is_bbl_vendor); + p_mainframe->show_device(use_bbl_network); auto cfg = preset_bundle.printers.get_edited_preset().config; - if (is_bbl_vendor) { + if (use_bbl_network) { //only show connection button for not-BBL printer connection_btn->Hide(); //only show sync-ams button for BBL printer @@ -1270,7 +1271,7 @@ void Sidebar::update_presets(Preset::Type preset_type) } Preset& printer_preset = wxGetApp().preset_bundle->printers.get_edited_preset(); - bool isBBL = preset_bundle.is_bbl_vendor(); + bool isBBL = preset_bundle.use_bbl_network(); wxGetApp().mainframe->show_calibration_button(!isBBL); if (auto printer_structure_opt = printer_preset.config.option>("printer_structure")) { @@ -6836,7 +6837,7 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) sidebar_layout.show = new_sel == MainFrame::tp3DEditor || new_sel == MainFrame::tpPreview; update_sidebar(); int old_sel = e.GetOldSelection(); - if (wxGetApp().preset_bundle && wxGetApp().preset_bundle->is_bbl_vendor() && new_sel == MainFrame::tpMonitor) { + if (wxGetApp().preset_bundle && wxGetApp().preset_bundle->use_bbl_network() && new_sel == MainFrame::tpMonitor) { if (!wxGetApp().getAgent()) { e.Veto(); BOOST_LOG_TRIVIAL(info) << boost::format("skipped tab switch from %1% to %2%, lack of network plugins") % old_sel % new_sel; diff --git a/src/slic3r/GUI/PrinterCloudAuthDialog.cpp b/src/slic3r/GUI/PrinterCloudAuthDialog.cpp index f46aa3c2bf..4d29d9b1c1 100644 --- a/src/slic3r/GUI/PrinterCloudAuthDialog.cpp +++ b/src/slic3r/GUI/PrinterCloudAuthDialog.cpp @@ -23,7 +23,7 @@ namespace Slic3r { namespace GUI { PrinterCloudAuthDialog::PrinterCloudAuthDialog(wxWindow* parent, PrintHost* host) - : wxDialog((wxWindow*) (wxGetApp().mainframe), wxID_ANY, "Login"), m_host(host) + : wxDialog((wxWindow*) (wxGetApp().mainframe), wxID_ANY, "Login") { SetBackgroundColour(*wxWHITE); // Url @@ -91,7 +91,6 @@ void PrinterCloudAuthDialog::OnScriptMessage(wxWebViewEvent& evt) wxString strCmd = j["command"]; if (strCmd == "login_token") { auto token = j["data"]["token"]; - m_host->set_api_key(token); m_apikey = token; } Close(); diff --git a/src/slic3r/GUI/PrinterCloudAuthDialog.hpp b/src/slic3r/GUI/PrinterCloudAuthDialog.hpp index 9da01b3206..6ee3779156 100644 --- a/src/slic3r/GUI/PrinterCloudAuthDialog.hpp +++ b/src/slic3r/GUI/PrinterCloudAuthDialog.hpp @@ -29,7 +29,6 @@ class PrinterCloudAuthDialog : public wxDialog wxString m_javascript; wxString m_response_js; - PrintHost* m_host; std::string m_apikey; public: diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index b149c38e94..f4f17909ca 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -3546,6 +3546,7 @@ void TabPrinter::build_fff() optgroup = page->new_optgroup(L("Advanced"), L"param_advanced"); optgroup->append_single_option_line("printer_structure"); optgroup->append_single_option_line("gcode_flavor"); + optgroup->append_single_option_line("bbl_use_printhost"); optgroup->append_single_option_line("disable_m73"); option = optgroup->get_option("thumbnails"); option.opt.full_width = true; @@ -4188,7 +4189,7 @@ void TabPrinter::toggle_options() // SoftFever: hide BBL specific settings for (auto el : - {"scan_first_layer", "machine_load_filament_time", "machine_unload_filament_time", "bbl_calib_mark_logo"}) + {"scan_first_layer", "machine_load_filament_time", "machine_unload_filament_time", "bbl_calib_mark_logo", "bbl_use_printhost"}) toggle_line(el, is_BBL_printer); // SoftFever: hide non-BBL settings diff --git a/src/slic3r/Utils/Obico.cpp b/src/slic3r/Utils/Obico.cpp index edd439f3bb..5541d1c683 100644 --- a/src/slic3r/Utils/Obico.cpp +++ b/src/slic3r/Utils/Obico.cpp @@ -42,8 +42,6 @@ Obico::Obico(DynamicPrintConfig* config) : const char* Obico::get_name() const { return "Obico"; } -void Obico::set_api_key(const std::string auth_api_key) { m_apikey = auth_api_key; } - std::string Obico::get_host() const { return m_host; } diff --git a/src/slic3r/Utils/Obico.hpp b/src/slic3r/Utils/Obico.hpp index c8c6e3a73d..9fd3d50f6b 100644 --- a/src/slic3r/Utils/Obico.hpp +++ b/src/slic3r/Utils/Obico.hpp @@ -24,7 +24,6 @@ class Obico : public PrintHost bool has_auto_discovery() const override { return false; } bool is_cloud() const override { return true; } bool get_login_url(wxString& auth_url) const override; - void set_api_key(const std::string auth_api_key) override; std::string get_host() const override; wxString get_test_ok_msg() const override; diff --git a/src/slic3r/Utils/PrintHost.cpp b/src/slic3r/Utils/PrintHost.cpp index f20c99ad50..cf77d4ff5c 100644 --- a/src/slic3r/Utils/PrintHost.cpp +++ b/src/slic3r/Utils/PrintHost.cpp @@ -22,6 +22,7 @@ #include "../GUI/PrintHostDialogs.hpp" #include "Obico.hpp" #include "Flashforge.hpp" +#include "SimplyPrint.hpp" namespace fs = boost::filesystem; using boost::optional; @@ -58,6 +59,7 @@ PrintHost* PrintHost::get_print_host(DynamicPrintConfig *config) case htMKS: return new MKS(config); case htObico: return new Obico(config); case htFlashforge: return new Flashforge(config); + case htSimplyPrint: return new SimplyPrint(config); default: return nullptr; } } else { diff --git a/src/slic3r/Utils/PrintHost.hpp b/src/slic3r/Utils/PrintHost.hpp index ea5c8a3baf..50699cb4d8 100644 --- a/src/slic3r/Utils/PrintHost.hpp +++ b/src/slic3r/Utils/PrintHost.hpp @@ -72,8 +72,9 @@ class PrintHost //Support for cloud webui login virtual bool is_cloud() const { return false; } + virtual bool is_logged_in() const { return false; } + virtual void log_out() const {} virtual bool get_login_url(wxString& auth_url) const { return false; } - virtual void set_api_key(const std::string auth_api_key) {} protected: virtual wxString format_error(const std::string &body, const std::string &error, unsigned status) const; diff --git a/src/slic3r/Utils/SimplyPrint.cpp b/src/slic3r/Utils/SimplyPrint.cpp new file mode 100644 index 0000000000..9c5603c7d7 --- /dev/null +++ b/src/slic3r/Utils/SimplyPrint.cpp @@ -0,0 +1,296 @@ +#include "SimplyPrint.hpp" + +#include +#include + +#include "nlohmann/json.hpp" +#include "libslic3r/Utils.hpp" +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/format.hpp" + + +namespace Slic3r { + +static constexpr boost::asio::ip::port_type CALLBACK_PORT = 21328; +static const std::string CALLBACK_URL = "http://localhost:21328/callback"; +static const std::string RESPONSE_TYPE = "code"; +static const std::string CLIENT_ID = "simplyprintorcaslicer"; +static const std::string CLIENT_SCOPES = "user.read files.temp_upload"; +static const std::string OAUTH_CREDENTIAL_PATH = "simplyprint_oauth.json"; +static const std::string TOKEN_URL = "https://simplyprint.io/api/oauth2/Token"; + +static std::string generate_verification_code(int code_length = 32) +{ + std::stringstream ss; + for (auto i = 0; i < code_length; i++) { + ss << std::hex << std::setw(2) << std::setfill('0') << (rand() % 0x100); + } + + return ss.str(); +} + +static std::string sha256b64(const std::string& inputStr) +{ + unsigned char hash[SHA256_DIGEST_LENGTH]; + const unsigned char* data = (const unsigned char*) inputStr.c_str(); + SHA256(data, inputStr.size(), hash); + + std::string b64; + b64.resize(boost::beast::detail::base64::encoded_size(sizeof(hash))); + b64.resize(boost::beast::detail::base64::encode(&b64[0], hash, sizeof(hash))); + + // uses '-' instead of '+' and '_' instead of '/' for url-safe + std::replace(b64.begin(), b64.end(), '+', '-'); + std::replace(b64.begin(), b64.end(), '/', '_'); + + // Stripping "=" is for RFC 7636 compliance + b64.erase(std::remove(b64.begin(), b64.end(), '='), b64.end()); + + return b64; +} + +static std::string url_encode(const std::vector> query) +{ + std::vector q; + q.reserve(query.size()); + + std::transform(query.begin(), query.end(), std::back_inserter(q), [](const auto& kv) { + return Http::url_encode(kv.first) + "=" + Http::url_encode(kv.second); + }); + + return boost::algorithm::join(q, "&"); +} + +static void set_auth(Http& http, const std::string& access_token) { http.header("Authorization", "Bearer " + access_token); } + +SimplyPrint::SimplyPrint(DynamicPrintConfig* config) +{ + cred_file = (boost::filesystem::path(data_dir()) / OAUTH_CREDENTIAL_PATH).make_preferred().string(); + load_oauth_credential(); +} + +GUI::OAuthParams SimplyPrint::get_oauth_params() const +{ + const auto verification_code = generate_verification_code(); + // SimplyPrint uses S256 for PKCE + const auto code_challenge = sha256b64(verification_code); + const auto state = generate_verification_code(); + + const std::vector> query_parameters{ + {"client_id", CLIENT_ID}, + {"redirect_uri", CALLBACK_URL}, + {"scope", CLIENT_SCOPES}, + {"response_type", RESPONSE_TYPE}, + {"state", state}, + {"code_challenge", code_challenge}, + {"code_challenge_method", "S256"}, + }; + const auto login_url = (boost::format("https://simplyprint.io/panel/oauth2/authorize?%s") % url_encode(query_parameters)).str(); + + return GUI::OAuthParams{ + login_url, + CLIENT_ID, + CALLBACK_PORT, + CALLBACK_URL, + CLIENT_SCOPES, + RESPONSE_TYPE, + "https://simplyprint.io/login-success", + "https://simplyprint.io/login-success", + TOKEN_URL, + verification_code, + state, + }; +} + +void SimplyPrint::load_oauth_credential() +{ + cred.clear(); + if (boost::filesystem::exists(cred_file)) { + nlohmann::json j; + try { + boost::nowide::ifstream ifs(cred_file); + ifs >> j; + ifs.close(); + + cred["access_token"] = j["access_token"]; + cred["refresh_token"] = j["refresh_token"]; + } catch (std::exception& err) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": parse " << cred_file << " failed, reason = " << err.what(); + cred.clear(); + } + } +} + +void SimplyPrint::save_oauth_credential(const GUI::OAuthResult& cred) const +{ + nlohmann::json j; + j["access_token"] = cred.access_token; + j["refresh_token"] = cred.refresh_token; + + boost::nowide::ofstream c; + c.open(cred_file, std::ios::out | std::ios::trunc); + c << std::setw(4) << j << std::endl; + c.close(); +} + +wxString SimplyPrint::get_test_ok_msg() const { return _(L("Connected to SimplyPrint successfully!")); } + +wxString SimplyPrint::get_test_failed_msg(wxString& msg) const +{ + return GUI::format_wxstr("%s: %s", _L("Could not connect to SimplyPrint"), msg.Truncate(256)); +} + +void SimplyPrint::log_out() const +{ + boost::nowide::remove(cred_file.c_str()); +} + +bool SimplyPrint::do_api_call(std::function build_request, + std::function on_complete, + std::function on_error) const +{ + if (cred.find("access_token") == cred.end()) { + return false; + } + + bool res = true; + + const auto create_request = [this, &build_request, &res, &on_complete](const std::string& access_token, bool is_retry) { + auto http = build_request(is_retry); + set_auth(http, access_token); + http.header("User-Agent", "SimplyPrint Orca Plugin") + .on_complete([&](std::string body, unsigned http_status) { + res = on_complete(body, http_status); + }); + + return http; + }; + + create_request(cred.at("access_token"), false) + .on_error([&res, &on_error, this, &create_request](std::string body, std::string error, unsigned http_status) { + if (http_status == 401) { + // Refresh token + BOOST_LOG_TRIVIAL(warning) << boost::format("SimplyPrint: Access token invalid: %1%, HTTP %2%, body: `%3%`") % error % + http_status % body; + BOOST_LOG_TRIVIAL(info) << "SimplyPrint: Attempt to refresh access token"; + + auto http = Http::post(TOKEN_URL); + http.timeout_connect(5) + .timeout_max(5) + .form_add("grant_type", "refresh_token") + .form_add("client_id", CLIENT_ID) + .form_add("refresh_token", cred.at("refresh_token")) + .on_complete([this, &res, &on_error, &create_request](std::string body, unsigned http_status) { + GUI::OAuthResult r; + GUI::OAuthJob::parse_token_response(body, false, r); + if (r.success) { + BOOST_LOG_TRIVIAL(info) << "SimplyPrint: Successfully refreshed access token"; + this->save_oauth_credential(r); + + // Run the api call again + create_request(r.access_token, true) + .on_error([&res, &on_error](std::string body, std::string error, unsigned http_status) { + res = on_error(body, error, http_status); + }) + .perform_sync(); + } else { + BOOST_LOG_TRIVIAL(error) + << boost::format("SimplyPrint: Failed to refresh access token: %1%, body: `%2%`") % r.error_message % body; + res = on_error(body, r.error_message, http_status); + } + }) + .on_error([&res, &on_error](std::string body, std::string error, unsigned http_status) { + BOOST_LOG_TRIVIAL(error) + << boost::format("SimplyPrint: Failed to refresh access token: %1%, HTTP %2%, body: `%3%`") % error % + http_status % body; + res = on_error(body, error, http_status); + }) + .perform_sync(); + } else { + res = on_error(body, error, http_status); + } + }) + .perform_sync(); + + return res; +} + + +bool SimplyPrint::test(wxString& curl_msg) const +{ + if (cred.find("access_token") == cred.end()) { + return false; + } + + return do_api_call( + [](bool is_retry) { + auto http = Http::get("https://api.simplyprint.io/oauth2/TokenInfo"); + http.header("Accept", "application/json"); + return http; + }, + [](std::string body, unsigned) { + BOOST_LOG_TRIVIAL(info) << boost::format("SimplyPrint: Got token info: %1%") % body; + return true; + }, + [](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("SimplyPrint: Error getting token info: %1%, HTTP %2%, body: `%3%`") % error % + status % body; + return false; + }); +} + +bool SimplyPrint::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const +{ + if (cred.find("access_token") == cred.end()) { + error_fn(_L("SimplyPrint account not linked. Go to Connect options to set it up.")); + return false; + } + + // If file is over 100 MB, fail + if (boost::filesystem::file_size(upload_data.source_path) > 104857600ull) { + error_fn(_L("File size exceeds the 100MB upload limit. Please upload your file through the panel.")); + return false; + } + + const auto filename = upload_data.upload_path.filename().string(); + + return do_api_call( + [&upload_data, &prorgess_fn, &filename](bool is_retry) { + auto http = Http::post("https://simplyprint.io/api/files/TempUpload"); + http.form_add_file("file", upload_data.source_path.string(), filename) + .on_progress([&prorgess_fn](Http::Progress progress, bool& cancel) { prorgess_fn(std::move(progress), cancel); }); + + return http; + }, + [&error_fn, &filename](std::string body, unsigned status) { + BOOST_LOG_TRIVIAL(info) << boost::format("SimplyPrint: File uploaded: HTTP %1%: %2%") % status % body; + + // Get file UUID + const auto j = nlohmann::json::parse(body, nullptr, false, true); + if (j.is_discarded()) { + BOOST_LOG_TRIVIAL(error) << "SimplyPrint: Invalid or no JSON data on token response: " << body; + error_fn(_L("Unknown error")); + return false; + } + + if (j.find("uuid") == j.end()) { + BOOST_LOG_TRIVIAL(error) << "SimplyPrint: Invalid or no JSON data on token response: " << body; + error_fn(_L("Unknown error")); + return false; + } + const std::string uuid = j["uuid"]; + + // Launch external browser for file importing after uploading + const auto url = "https://simplyprint.io/panel?" + url_encode({{"import", "tmp:" + uuid}, {"filename", filename}}); + wxLaunchDefaultBrowser(url); + + return true; + }, + [this, &error_fn](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("SimplyPrint: Error uploading file : %1%, HTTP %2%, body: `%3%`") % error % status % body; + error_fn(format_error(body, error, status)); + return false; + }); +} + +} // namespace Slic3r diff --git a/src/slic3r/Utils/SimplyPrint.hpp b/src/slic3r/Utils/SimplyPrint.hpp new file mode 100644 index 0000000000..b2c06a04e6 --- /dev/null +++ b/src/slic3r/Utils/SimplyPrint.hpp @@ -0,0 +1,45 @@ +#ifndef slic3r_SimplyPrint_hpp_ +#define slic3r_SimplyPrint_hpp_ + +#include "PrintHost.hpp" +#include "slic3r/GUI/Jobs/OAuthJob.hpp" + +namespace Slic3r { + +class DynamicPrintConfig; +class Http; +class SimplyPrint : public PrintHost +{ + std::string cred_file; + std::map cred; + + void load_oauth_credential(); + + bool do_api_call(std::function build_request, + std::function on_complete, + std::function on_error) const; + +public: + SimplyPrint(DynamicPrintConfig* config); + ~SimplyPrint() override = default; + + const char* get_name() const override { return "SimplyPrint"; } + bool can_test() const override { return true; } + bool has_auto_discovery() const override { return false; } + bool is_cloud() const override { return true; } + std::string get_host() const override { return "https://simplyprint.io"; } + + GUI::OAuthParams get_oauth_params() const; + void save_oauth_credential(const GUI::OAuthResult& cred) const; + + wxString get_test_ok_msg() const override; + wxString get_test_failed_msg(wxString& msg) const override; + bool test(wxString& curl_msg) const override; + PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::QueuePrint; } + bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const override; + bool is_logged_in() const override { return !cred.empty(); } + void log_out() const override; +}; +} // namespace Slic3r + +#endif \ No newline at end of file