diff --git a/Makefile b/Makefile index 4ebc2723..fc24f213 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ ifeq ($(ARCH), lin) LDFLAGS += -rdynamic \ -lpthread -lGL -ldl \ $(shell pkg-config --libs gtk+-2.0) \ - -Ldep/lib -lGLEW -lglfw -ljansson -lsamplerate -lcurl -lzip -lrtaudio -lrtmidi + -Ldep/lib -lGLEW -lglfw -ljansson -lsamplerate -lcurl -lzip -lrtaudio -lrtmidi -lcrypto TARGET = Rack endif diff --git a/include/plugin.hpp b/include/plugin.hpp index bd243473..327249ac 100644 --- a/include/plugin.hpp +++ b/include/plugin.hpp @@ -107,7 +107,8 @@ void pluginInit(); void pluginDestroy(); void pluginLogIn(std::string email, std::string password); void pluginLogOut(); -void pluginRefresh(); +/** Returns whether a new plugin is available, and downloads it unless doing a dry run */ +bool pluginSync(bool dryRun); void pluginCancelDownload(); bool pluginIsLoggedIn(); bool pluginIsDownloading(); diff --git a/include/util/request.hpp b/include/util/request.hpp index d0405155..7ae510c7 100644 --- a/include/util/request.hpp +++ b/include/util/request.hpp @@ -19,5 +19,6 @@ json_t *requestJson(RequestMethod method, std::string url, json_t *dataJ); /** Returns the filename, blank if unsuccessful */ bool requestDownload(std::string url, std::string filename, float *progress); std::string requestEscape(std::string s); +std::string requestSHA256File(std::string filename); } // namespace rack diff --git a/src/app.cpp b/src/app.cpp index 18b07e30..84795945 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -11,6 +11,7 @@ std::string gApplicationVersion = ""; #endif std::string gApiHost = "https://api.vcvrack.com"; +// std::string gApiHost = "http://localhost:8081"; RackWidget *gRackWidget = NULL; Toolbar *gToolbar = NULL; diff --git a/src/app/PluginManagerWidget.cpp b/src/app/PluginManagerWidget.cpp index c246c325..096eb071 100644 --- a/src/app/PluginManagerWidget.cpp +++ b/src/app/PluginManagerWidget.cpp @@ -1,11 +1,57 @@ #include #include "app.hpp" #include "plugin.hpp" +#include "gui.hpp" +#include "../ext/osdialog/osdialog.h" namespace rack { +struct SyncButton : Button { + bool checked = false; + bool available = false; + bool completed = false; + + void step() override { + if (!checked) { + std::thread t([this]() { + if (pluginSync(true)) + available = true; + }); + t.detach(); + checked = true; + } + if (completed) { + if (osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, "All plugins have been updated. Close Rack and re-launch it to load new updates.")) { + guiClose(); + } + completed = false; + } + } + void draw(NVGcontext *vg) override { + Button::draw(vg); + if (available) { + // Notification circle + nvgBeginPath(vg); + nvgCircle(vg, 3, 3, 4.0); + nvgFillColor(vg, nvgRGBf(1.0, 0.0, 0.0)); + nvgFill(vg); + nvgStrokeColor(vg, nvgRGBf(0.5, 0.0, 0.0)); + nvgStroke(vg); + } + } + void onAction(EventAction &e) override { + available = false; + std::thread t([this]() { + if (pluginSync(false)) + completed = true; + }); + t.detach(); + } +}; + + PluginManagerWidget::PluginManagerWidget() { box.size.y = BND_WIDGET_HEIGHT; float margin = 5; @@ -91,19 +137,13 @@ PluginManagerWidget::PluginManagerWidget() { manageWidget->addChild(manageButton); pos.x += manageButton->box.size.x; - struct RefreshButton : Button { - void onAction(EventAction &e) override { - std::thread t(pluginRefresh); - t.detach(); - } - }; pos.x += margin; - Button *refreshButton = new RefreshButton(); - refreshButton->box.pos = pos; - refreshButton->box.size.x = 125; - refreshButton->text = "Refresh plugins"; - manageWidget->addChild(refreshButton); - pos.x += refreshButton->box.size.x; + Button *syncButton = new SyncButton(); + syncButton->box.pos = pos; + syncButton->box.size.x = 125; + syncButton->text = "Update plugins"; + manageWidget->addChild(syncButton); + pos.x += syncButton->box.size.x; struct LogOutButton : Button { void onAction(EventAction &e) override { diff --git a/src/plugin.cpp b/src/plugin.cpp index e3adc0a4..7931efab 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -7,11 +7,12 @@ #include #include // for MAXPATHLEN #include +#include #include #include -#if ARCH_WIN +#if defined(ARCH_WIN) #include #include #define mkdir(_dir, _perms) _mkdir(_dir) @@ -225,35 +226,55 @@ static int extractZip(const char *filename, const char *dir) { return err; } -static void refreshPurchase(json_t *pluginJ) { +static void syncPlugin(json_t *pluginJ) { json_t *slugJ = json_object_get(pluginJ, "slug"); if (!slugJ) return; std::string slug = json_string_value(slugJ); + info("Syncing plugin %s", slug.c_str()); json_t *nameJ = json_object_get(pluginJ, "name"); if (!nameJ) return; std::string name = json_string_value(nameJ); - json_t *versionJ = json_object_get(pluginJ, "version"); - if (!versionJ) return; - std::string version = json_string_value(versionJ); - - // Check whether the plugin is already loaded - for (Plugin *plugin : gPlugins) { - if (plugin->slug == slug && plugin->version == version) { - return; + std::string download; + std::string sha256; + + json_t *downloadsJ = json_object_get(pluginJ, "downloads"); + if (downloadsJ) { +#if defined(ARCH_WIN) + #define DOWNLOADS_ARCH "win" +#elif defined(ARCH_MAC) + #define DOWNLOADS_ARCH "mac" +#elif defined(ARCH_LIN) + #define DOWNLOADS_ARCH "lin" +#endif + json_t *archJ = json_object_get(downloadsJ, DOWNLOADS_ARCH); + if (archJ) { + // Get download URL + json_t *downloadJ = json_object_get(archJ, "download"); + if (downloadJ) + download = json_string_value(downloadJ); + // Get SHA256 hash + json_t *sha256J = json_object_get(archJ, "sha256"); + if (sha256J) + sha256 = json_string_value(sha256J); } } - // Append token and version to download URL - std::string url = gApiHost; - url += "/download"; - url += "?product="; - url += slug; - url += "&version="; - url += requestEscape(gApplicationVersion); - url += "&token="; - url += requestEscape(gToken); + json_t *productIdJ = json_object_get(pluginJ, "productId"); + if (productIdJ) { + download = gApiHost; + download += "/download"; + download += "?slug="; + download += slug; + download += "&token="; + download += requestEscape(gToken); + } + + if (download.empty()) { + warn("Could not get download URL for plugin %s", slug.c_str()); + return; + } // If plugin is not loaded, download the zip file to /plugins downloadName = name; @@ -263,21 +284,127 @@ static void refreshPurchase(json_t *pluginJ) { std::string pluginsDir = assetLocal("plugins"); std::string pluginPath = pluginsDir + "/" + slug; std::string zipPath = pluginPath + ".zip"; - bool success = requestDownload(url, zipPath, &downloadProgress); + bool success = requestDownload(download, zipPath, &downloadProgress); if (success) { + if (!sha256.empty()) { + // Check SHA256 hash + std::string actualSha256 = requestSHA256File(zipPath); + if (actualSha256 != sha256) { + warn("Plugin %s does not match expected SHA256 checksum", slug.c_str()); + return; + } + } + // Unzip file int err = extractZip(zipPath.c_str(), pluginsDir.c_str()); if (!err) { // Delete zip remove(zipPath.c_str()); // Load plugin - loadPlugin(pluginPath); + // loadPlugin(pluginPath); } } downloadName = ""; } +static bool trySyncPlugin(json_t *pluginJ, json_t *communityPluginsJ, bool dryRun) { + std::string slug = json_string_value(pluginJ); + + // Find community plugin + size_t communityIndex; + json_t *communityPluginJ = NULL; + json_array_foreach(communityPluginsJ, communityIndex, communityPluginJ) { + json_t *communitySlugJ = json_object_get(communityPluginJ, "slug"); + if (communitySlugJ) { + std::string communitySlug = json_string_value(communitySlugJ); + if (slug == communitySlug) + break; + } + } + if (communityIndex == json_array_size(communityPluginsJ)) { + warn("Plugin sync error: %s not found in community", slug.c_str()); + return false; + } + + // Get community version + std::string version; + json_t *versionJ = json_object_get(communityPluginJ, "version"); + if (versionJ) { + version = json_string_value(versionJ); + } + + // Check whether we already have a plugin with the same slug and version + for (Plugin *plugin : gPlugins) { + if (plugin->slug == slug) { + // plugin->version might be blank, so adding a version of the manifest will update the plugin + if (plugin->version == version) + return false; + } + } + + if (!dryRun) + syncPlugin(communityPluginJ); + return true; +} + +bool pluginSync(bool dryRun) { + if (gToken.empty()) + return false; + + bool available = false; + + // Download my plugins + json_t *reqJ = json_object(); + json_object_set(reqJ, "version", json_string(gApplicationVersion.c_str())); + json_object_set(reqJ, "token", json_string(gToken.c_str())); + json_t *resJ = requestJson(METHOD_GET, gApiHost + "/plugins", reqJ); + json_decref(reqJ); + + // Download community plugins + json_t *communityResJ = requestJson(METHOD_GET, gApiHost + "/community/plugins", NULL); + + if (!dryRun) { + isDownloading = true; + downloadProgress = 0.0; + downloadName = ""; + } + + if (resJ && communityResJ) { + json_t *errorJ = json_object_get(resJ, "error"); + json_t *communityErrorJ = json_object_get(resJ, "error"); + if (errorJ) { + warn("Plugin sync error: %s", json_string_value(errorJ)); + } + else if (communityErrorJ) { + warn("Plugin sync error: %s", json_string_value(communityErrorJ)); + } + else { + // Check each plugin in list of my plugins + json_t *pluginsJ = json_object_get(resJ, "plugins"); + json_t *communityPluginsJ = json_object_get(communityResJ, "plugins"); + size_t index; + json_t *pluginJ; + json_array_foreach(pluginsJ, index, pluginJ) { + if (trySyncPlugin(pluginJ, communityPluginsJ, dryRun)) + available = true; + } + } + } + + if (resJ) + json_decref(resJ); + + if (communityResJ) + json_decref(communityResJ); + + if (!dryRun) { + isDownloading = false; + } + + return available; +} + //////////////////// // plugin API //////////////////// @@ -306,10 +433,10 @@ void pluginInit() { void pluginDestroy() { for (Plugin *plugin : gPlugins) { // Free library handle -#if ARCH_WIN +#if defined(ARCH_WIN) if (plugin->handle) FreeLibrary((HINSTANCE)plugin->handle); -#elif ARCH_LIN || ARCH_MAC +#elif defined(ARCH_LIN) || defined(ARCH_MAC) if (plugin->handle) dlclose(plugin->handle); #endif @@ -350,40 +477,6 @@ void pluginLogOut() { gToken = ""; } -void pluginRefresh() { - if (gToken.empty()) - return; - - isDownloading = true; - downloadProgress = 0.0; - downloadName = ""; - - json_t *reqJ = json_object(); - json_object_set(reqJ, "version", json_string(gApplicationVersion.c_str())); - json_object_set(reqJ, "token", json_string(gToken.c_str())); - json_t *resJ = requestJson(METHOD_GET, gApiHost + "/purchases", reqJ); - json_decref(reqJ); - - if (resJ) { - json_t *errorJ = json_object_get(resJ, "error"); - if (errorJ) { - const char *errorStr = json_string_value(errorJ); - warn("Plugin refresh error: %s", errorStr); - } - else { - json_t *purchasesJ = json_object_get(resJ, "purchases"); - size_t index; - json_t *purchaseJ; - json_array_foreach(purchasesJ, index, purchaseJ) { - refreshPurchase(purchaseJ); - } - } - json_decref(resJ); - } - - isDownloading = false; -} - void pluginCancelDownload() { // TODO } diff --git a/src/util/request.cpp b/src/util/request.cpp index c046bbb7..2c9ea524 100644 --- a/src/util/request.cpp +++ b/src/util/request.cpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace rack { @@ -130,6 +131,7 @@ bool requestDownload(std::string url, std::string filename, float *progress) { curl_easy_setopt(curl, CURLOPT_WRITEDATA, file); curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, xferInfoCallback); curl_easy_setopt(curl, CURLOPT_XFERINFODATA, progress); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true); info("Downloading %s", url.c_str()); CURLcode res = curl_easy_perform(curl); @@ -153,5 +155,36 @@ std::string requestEscape(std::string s) { return ret; } +std::string requestSHA256File(std::string filename) { + FILE *f = fopen(filename.c_str(), "rb"); + if (!f) + return ""; + + uint8_t hash[SHA256_DIGEST_LENGTH]; + SHA256_CTX sha256; + SHA256_Init(&sha256); + const int bufferLen = 1 << 15; + uint8_t *buffer = new uint8_t[bufferLen]; + int len = 0; + while ((len = fread(buffer, 1, bufferLen, f))) { + SHA256_Update(&sha256, buffer, len); + } + SHA256_Final(hash, &sha256); + delete[] buffer; + fclose(f); + + // Convert binary hash to hex + char hashHex[64]; + const char hexTable[] = "0123456789abcdef"; + for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) { + uint8_t h = hash[i]; + hashHex[2*i + 0] = hexTable[h >> 4]; + hashHex[2*i + 1] = hexTable[h & 0x0f]; + } + + std::string str(hashHex, sizeof(hashHex)); + return str; +} + } // namespace rack