* Add OrcaCloud sync platform and preset bundle sharing system Introduce OrcaCloud, a cloud sync platform for user presets, alongside a preset bundle system that enables sharing printer/filament/process profiles as local exportable bundles or subscribed cloud bundles. OrcaCloud platform: - Auth to Orca Cloud - Encrypted token storage (file-based or system keychain) - User preset sync with - Profile migration from default/bambu folders on first login - Homepage integration with entrance to cloud.orcaslicer.com Preset bundles: - Local bundle import/export with bundle_structure.json metadata - Subscribed cloud bundles with version-based update checking - Thread-safe concurrent bundle access with read-write mutex - Canonical bundle preset naming (_local/<id>/... and _subscribed/<id>/...) - Bundle presets are read-only; grouped under subheaders in combo boxes - PresetBundleDialog with auto-sync toggle, refresh, update notifications - Hyperlinked bundle names to cloud bundle pages Co-authored-by: Sabriel Koh <sabrielkcr@gmail.com> Co-authored-by: Derrick <derrick992110@gmail.com> Co-authored-by: Mykola Nahirnyi <mnahirnyi@amcbridge.com> Co-authored-by: Ian Chua <iancrb00@gmail.com> Co-authored-by: Draginraptor <draginraptor@gmail.com> Co-authored-by: ExPikaPaka <112851715+ExPikaPaka@users.noreply.github.com> Co-authored-by: Ian Bassi <ian.bassi@outlook.com> Co-authored-by: Ocraftyone <Ocraftyone@users.noreply.github.com> Co-authored-by: yw4z <ywsyildiz@gmail.com> Co-authored-by: peterm-m <101202951+peterm-m@users.noreply.github.com> * Fixed an issue on Windows it failed to login Orca Cloud with Google account
438 lines
17 KiB
C++
438 lines
17 KiB
C++
#include "HttpServer.hpp"
|
|
#include <boost/log/trivial.hpp>
|
|
#include "GUI_App.hpp"
|
|
#include "slic3r/Utils/Http.hpp"
|
|
#include "slic3r/Utils/NetworkAgent.hpp"
|
|
#include "slic3r/Utils/BBLNetworkPlugin.hpp"
|
|
|
|
namespace Slic3r {
|
|
namespace GUI {
|
|
|
|
std::string url_get_param(const std::string& url, const std::string& key)
|
|
{
|
|
size_t start = url.find(key);
|
|
if (start == std::string::npos) return "";
|
|
size_t eq = url.find('=', start);
|
|
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 == std::string::npos) end = url.length(); // Last param
|
|
std::string result = url.substr(start, end - start);
|
|
return result;
|
|
}
|
|
|
|
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<std::vector<char>> bufptr = std::make_shared<std::vector<char>>(nbuffer);
|
|
async_read(socket, boost::asio::buffer(*bufptr, nbuffer),
|
|
[this, self, bufptr](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) {
|
|
std::cout << "Request received: " << headers.method << " " << headers.get_url();
|
|
if (headers.method == "OPTIONS") {
|
|
// Ignore http OPTIONS
|
|
server.stop(self);
|
|
return;
|
|
}
|
|
|
|
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<std::string> str = std::make_shared<std::string>(ssOut.str());
|
|
async_write(socket, boost::asio::buffer(str->c_str(), str->length()),
|
|
[this, self, str](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<session>(*this, std::move(socket));
|
|
start(ss);
|
|
}
|
|
|
|
do_accept();
|
|
});
|
|
}
|
|
|
|
void HttpServer::IOServer::start(std::shared_ptr<session> session)
|
|
{
|
|
sessions.insert(session);
|
|
session->start();
|
|
}
|
|
|
|
void HttpServer::IOServer::stop(std::shared_ptr<session> 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) {}
|
|
|
|
HttpServer::~HttpServer()
|
|
{
|
|
stop();
|
|
}
|
|
|
|
void HttpServer::start()
|
|
{
|
|
if (start_http_server)
|
|
return;
|
|
|
|
BOOST_LOG_TRIVIAL(info) << "start_http_service...";
|
|
server_ = std::make_unique<IOServer>(*this);
|
|
IOServer* io_server = server_.get();
|
|
start_http_server = true;
|
|
m_http_server_thread = create_thread([io_server] {
|
|
set_current_thread_name("http_server");
|
|
io_server->acceptor.listen();
|
|
|
|
io_server->do_accept();
|
|
|
|
io_server->io_service.run();
|
|
});
|
|
}
|
|
|
|
void HttpServer::stop()
|
|
{
|
|
start_http_server = false;
|
|
if (server_) {
|
|
IOServer* io_server = server_.get();
|
|
boost::asio::post(io_server->io_service, [io_server] {
|
|
boost::system::error_code ec;
|
|
io_server->acceptor.cancel(ec);
|
|
io_server->acceptor.close(ec);
|
|
io_server->stop_all();
|
|
io_server->io_service.stop();
|
|
});
|
|
}
|
|
if (m_http_server_thread.joinable())
|
|
m_http_server_thread.join();
|
|
server_.reset();
|
|
}
|
|
|
|
void HttpServer::set_request_handler(const std::function<std::shared_ptr<Response>(const std::string&)>& request_handler)
|
|
{
|
|
this->m_request_handler = request_handler;
|
|
}
|
|
|
|
std::shared_ptr<HttpServer::Response> HttpServer::bbl_auth_handle_request(const std::string& url)
|
|
{
|
|
return auth_handle_request(url, BBL_CLOUD_PROVIDER);
|
|
}
|
|
|
|
std::shared_ptr<HttpServer::Response> HttpServer::auth_handle_request(const std::string& url, const std::string& provider)
|
|
{
|
|
BOOST_LOG_TRIVIAL(info) << "thirdparty_login: get_response";
|
|
|
|
const std::string auth_code = url_get_param(url, "code");
|
|
if (!auth_code.empty()) {
|
|
std::string state = url_get_param(url, "orca_state");
|
|
if (state.empty()) {
|
|
state = url_get_param(url, "state"); // fallback
|
|
}
|
|
NetworkAgent* agent = wxGetApp().getAgent();
|
|
if (!agent) {
|
|
return std::make_shared<ResponseNotFound>();
|
|
}
|
|
|
|
json payload;
|
|
payload["command"] = "user_login";
|
|
payload["data"]["code"] = auth_code;
|
|
payload["data"]["state"] = state;
|
|
|
|
agent->change_user(payload.dump(), provider);
|
|
const bool login_ok = agent->is_user_login(provider);
|
|
if (login_ok) {
|
|
wxGetApp().request_user_login(1, provider);
|
|
GUI::wxGetApp().CallAfter([] { wxGetApp().ShowUserLogin(false); });
|
|
}
|
|
|
|
const std::string title = login_ok ? "Authentication complete" : "Authentication failed";
|
|
const std::string message = login_ok
|
|
? "You can return to OrcaSlicer. This window will close automatically."
|
|
: "Something went wrong. Please return to OrcaSlicer and try again.";
|
|
const std::string html =
|
|
"<html><head><meta charset=\"utf-8\">"
|
|
"<style>body{font-family:Arial,sans-serif;background:#f7f7f7;color:#222;margin:32px;}"
|
|
"a.button{display:inline-block;padding:10px 16px;margin-top:12px;background:#0f8bff;color:#fff;text-decoration:none;border-radius:6px;}"
|
|
"</style></head><body><div class=\"container\">"
|
|
"<h2>" + title + "</h2>"
|
|
"<p>" + message + "</p>"
|
|
"<script>setTimeout(function(){try{window.close();}catch(e){}},1500);</script>"
|
|
"</div></body></html>";
|
|
return std::make_shared<ResponseHtml>(html);
|
|
}
|
|
|
|
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, provider);
|
|
if (result == 0) {
|
|
std::string user_id;
|
|
std::string user_name;
|
|
std::string user_account;
|
|
std::string user_avatar;
|
|
try {
|
|
json user_j = json::parse(http_body);
|
|
if (user_j.contains("uidStr"))
|
|
user_id = user_j["uidStr"].get<std::string>();
|
|
if (user_j.contains("name"))
|
|
user_name = user_j["name"].get<std::string>();
|
|
if (user_j.contains("avatar"))
|
|
user_avatar = user_j["avatar"].get<std::string>();
|
|
if (user_j.contains("account"))
|
|
user_account = user_j["account"].get<std::string>();
|
|
} catch (...) {
|
|
;
|
|
}
|
|
json j;
|
|
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;
|
|
agent->change_user(j.dump(), provider);
|
|
if (agent->is_user_login(provider)) {
|
|
wxGetApp().request_user_login(1, provider);
|
|
}
|
|
GUI::wxGetApp().CallAfter([] { wxGetApp().ShowUserLogin(false); });
|
|
std::string location_str = (boost::format("%1%?result=success") % redirect_url).str();
|
|
return std::make_shared<ResponseRedirect>(location_str);
|
|
} else {
|
|
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<ResponseRedirect>(location_str);
|
|
}
|
|
}
|
|
|
|
// Ticket-based redirect: Bambu Lab's auth server redirects here after a
|
|
// third-party (Google) OAuth so that the access token never travels through
|
|
// the URL. We exchange the ticket via the network plugin's get_my_token,
|
|
// then run the same get_my_profile + change_user flow as access_token.
|
|
// Skip entirely on legacy plugins missing bambu_network_get_my_token —
|
|
// those clients pin X-BBL-Client-Version so the server stays on the legacy
|
|
// ?access_token= redirect path and never sends ?ticket= here.
|
|
const std::string ticket = url_get_param(url, "ticket");
|
|
const std::string ticket_redirect_url = url_get_param(url, "redirect_url");
|
|
if (!ticket.empty() && !ticket_redirect_url.empty() &&
|
|
BBLNetworkPlugin::instance().get_get_my_token() != nullptr) {
|
|
BOOST_LOG_TRIVIAL(info) << "thirdparty_login: ticket flow";
|
|
NetworkAgent* agent = wxGetApp().getAgent();
|
|
if (!agent) {
|
|
std::string location_str = (boost::format("%1%?result=fail&error=no_agent") % ticket_redirect_url).str();
|
|
return std::make_shared<ResponseRedirect>(location_str);
|
|
}
|
|
|
|
auto fail_redirect = [&ticket_redirect_url](const std::string& reason) {
|
|
std::string location_str = (boost::format("%1%?result=fail&error=%2%") % ticket_redirect_url % reason).str();
|
|
return std::make_shared<ResponseRedirect>(location_str);
|
|
};
|
|
|
|
unsigned int token_http_code = 0;
|
|
std::string token_body;
|
|
int token_result = agent->get_my_token(ticket, &token_http_code, &token_body, provider);
|
|
if (token_result != 0) {
|
|
BOOST_LOG_TRIVIAL(warning) << "thirdparty_login: get_my_token failed, http_code=" << token_http_code;
|
|
return fail_redirect("get_my_token_error_" + std::to_string(token_result));
|
|
}
|
|
|
|
std::string access_token;
|
|
std::string refresh_token;
|
|
std::string expires_in_str;
|
|
std::string refresh_expires_in_str;
|
|
try {
|
|
json token_j = json::parse(token_body);
|
|
if (token_j.contains("accessToken"))
|
|
access_token = token_j["accessToken"].get<std::string>();
|
|
if (token_j.contains("refreshToken"))
|
|
refresh_token = token_j["refreshToken"].get<std::string>();
|
|
if (token_j.contains("expiresIn"))
|
|
expires_in_str = std::to_string(token_j["expiresIn"].get<double>());
|
|
if (token_j.contains("refreshExpiresIn"))
|
|
refresh_expires_in_str = std::to_string(token_j["refreshExpiresIn"].get<double>());
|
|
} catch (...) {
|
|
return fail_redirect("token_parse_error");
|
|
}
|
|
|
|
if (access_token.empty()) {
|
|
return fail_redirect("token_missing");
|
|
}
|
|
|
|
unsigned int profile_http_code = 0;
|
|
std::string profile_body;
|
|
int profile_result = agent->get_my_profile(access_token, &profile_http_code, &profile_body, provider);
|
|
if (profile_result != 0) {
|
|
BOOST_LOG_TRIVIAL(warning) << "thirdparty_login: get_my_profile failed, http_code=" << profile_http_code;
|
|
return fail_redirect("get_user_profile_error_" + std::to_string(profile_result));
|
|
}
|
|
|
|
std::string user_id;
|
|
std::string user_name;
|
|
std::string user_account;
|
|
std::string user_avatar;
|
|
try {
|
|
json user_j = json::parse(profile_body);
|
|
if (user_j.contains("uidStr"))
|
|
user_id = user_j["uidStr"].get<std::string>();
|
|
if (user_j.contains("name"))
|
|
user_name = user_j["name"].get<std::string>();
|
|
if (user_j.contains("avatar"))
|
|
user_avatar = user_j["avatar"].get<std::string>();
|
|
if (user_j.contains("account"))
|
|
user_account = user_j["account"].get<std::string>();
|
|
} catch (...) {
|
|
BOOST_LOG_TRIVIAL(warning) << "thirdparty_login: profile JSON parse failed";
|
|
}
|
|
|
|
json j;
|
|
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;
|
|
agent->change_user(j.dump(), provider);
|
|
if (agent->is_user_login(provider)) {
|
|
wxGetApp().request_user_login(1, provider);
|
|
}
|
|
GUI::wxGetApp().CallAfter([] { wxGetApp().ShowUserLogin(false); });
|
|
std::string location_str = (boost::format("%1%?result=success") % ticket_redirect_url).str();
|
|
return std::make_shared<ResponseRedirect>(location_str);
|
|
}
|
|
|
|
return std::make_shared<ResponseNotFound>();
|
|
}
|
|
|
|
void HttpServer::ResponseNotFound::write_response(std::stringstream& ssOut)
|
|
{
|
|
const std::string sHTML = "<html><body><h1>404 Not Found</h1><p>There's nothing here.</p></body></html>";
|
|
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::ResponseRedirect::write_response(std::stringstream& ssOut)
|
|
{
|
|
const std::string sHTML =
|
|
"<html><head><meta charset=\"utf-8\">"
|
|
"<meta http-equiv=\"refresh\" content=\"0;url=" + location_str + "\">"
|
|
"<style>body{font-family:Arial,sans-serif;background:#f7f7f7;color:#222;margin:32px;}"
|
|
"a.button{display:inline-block;padding:10px 16px;margin-top:12px;background:#0f8bff;color:#fff;text-decoration:none;border-radius:6px;}"
|
|
"</style></head><body><div class=\"container\">"
|
|
"<h2>Authentication complete</h2>"
|
|
"<p>You can return to OrcaSlicer. If your browser does not redirect automatically, use the button below.</p>"
|
|
"<a class=\"button\" href=\"" + location_str + "\">Continue</a>"
|
|
"<script>setTimeout(function(){try{window.close();}catch(e){}},1500);</script>"
|
|
"</div></body></html>";
|
|
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;
|
|
}
|
|
|
|
void HttpServer::ResponseHtml::write_response(std::stringstream& ssOut)
|
|
{
|
|
ssOut << "HTTP/1.1 200 OK" << std::endl;
|
|
ssOut << "content-type: text/html" << std::endl;
|
|
ssOut << "content-length: " << html.length() << std::endl;
|
|
ssOut << std::endl;
|
|
ssOut << html;
|
|
}
|
|
|
|
} // GUI
|
|
} //Slic3r
|