/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2020 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 6 End-User License Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). End User License Agreement: www.juce.com/juce-6-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ #pragma once //============================================================================== namespace LicenseHelpers { inline LicenseState::Type licenseTypeForString (const String& licenseString) { if (licenseString == "juce-pro") return LicenseState::Type::pro; if (licenseString == "juce-indie") return LicenseState::Type::indie; if (licenseString == "juce-edu") return LicenseState::Type::educational; if (licenseString == "juce-personal") return LicenseState::Type::personal; jassertfalse; // unknown type return LicenseState::Type::none; } using LicenseVersionAndType = std::pair; inline LicenseVersionAndType findBestLicense (std::vector&& licenses) { if (licenses.size() == 1) return licenses[0]; auto getValueForLicenceType = [] (LicenseState::Type type) { switch (type) { case LicenseState::Type::pro: return 4; case LicenseState::Type::indie: return 3; case LicenseState::Type::educational: return 2; case LicenseState::Type::personal: return 1; case LicenseState::Type::gpl: case LicenseState::Type::none: default: return -1; } }; std::sort (licenses.begin(), licenses.end(), [getValueForLicenceType] (const LicenseVersionAndType& l1, const LicenseVersionAndType& l2) { if (l1.first > l2.first) return true; if (l1.first == l2.first) return getValueForLicenceType (l1.second) > getValueForLicenceType (l2.second); return false; }); auto findFirstLicense = [&licenses] (bool isPaid) { auto iter = std::find_if (licenses.begin(), licenses.end(), [isPaid] (const LicenseVersionAndType& l) { auto proOrIndie = (l.second == LicenseState::Type::pro || l.second == LicenseState::Type::indie); return isPaid ? proOrIndie : ! proOrIndie; }); return iter != licenses.end() ? *iter : LicenseVersionAndType(); }; auto newestPaid = findFirstLicense (true); auto newestFree = findFirstLicense (false); if (newestPaid.first >= projucerMajorVersion || newestPaid.first >= newestFree.first) return newestPaid; return newestFree; } } //============================================================================== class LicenseQueryThread { public: enum class ErrorType { busy, cancelled, connectionError, webResponseError }; using ErrorMessageAndType = std::pair; using LicenseQueryCallback = std::function; //============================================================================== LicenseQueryThread() = default; void checkLicenseValidity (const LicenseState& state, LicenseQueryCallback completionCallback) { if (jobPool.getNumJobs() > 0) { completionCallback ({ {}, ErrorType::busy }, {}); return; } jobPool.addJob ([this, state, completionCallback] { auto updatedState = state; auto result = runTask (std::make_unique (state.authToken), updatedState); WeakReference weakThis (this); MessageManager::callAsync ([weakThis, result, updatedState, completionCallback] { if (weakThis != nullptr) completionCallback (result, updatedState); }); }); } void doSignIn (const String& email, const String& password, LicenseQueryCallback completionCallback) { cancelRunningJobs(); jobPool.addJob ([this, email, password, completionCallback] { LicenseState state; auto result = runTask (std::make_unique (email, password), state); if (result == ErrorMessageAndType()) result = runTask (std::make_unique (state.authToken), state); if (result != ErrorMessageAndType()) state = {}; WeakReference weakThis (this); MessageManager::callAsync ([weakThis, result, state, completionCallback] { if (weakThis != nullptr) completionCallback (result, state); }); }); } void cancelRunningJobs() { jobPool.removeAllJobs (true, 500); } private: //============================================================================== struct AccountEnquiryBase { virtual ~AccountEnquiryBase() = default; virtual bool isPOSTLikeRequest() const = 0; virtual String getEndpointURLSuffix() const = 0; virtual StringPairArray getParameterNamesAndValues() const = 0; virtual String getExtraHeaders() const = 0; virtual int getSuccessCode() const = 0; virtual String errorCodeToString (int) const = 0; virtual bool parseServerResponse (const String&, LicenseState&) = 0; }; struct UserLogin : public AccountEnquiryBase { UserLogin (const String& e, const String& p) : userEmail (e), userPassword (p) { } bool isPOSTLikeRequest() const override { return true; } String getEndpointURLSuffix() const override { return "/authenticate/projucer"; } int getSuccessCode() const override { return 200; } StringPairArray getParameterNamesAndValues() const override { StringPairArray namesAndValues; namesAndValues.set ("email", userEmail); namesAndValues.set ("password", userPassword); return namesAndValues; } String getExtraHeaders() const override { return "Content-Type: application/json"; } String errorCodeToString (int errorCode) const override { switch (errorCode) { case 400: return "Please enter your email and password to sign in."; case 401: return "Your email and password are incorrect."; case 451: return "Access denied."; default: return "Something went wrong, please try again."; } } bool parseServerResponse (const String& serverResponse, LicenseState& licenseState) override { auto json = JSON::parse (serverResponse); licenseState.authToken = json.getProperty ("token", {}).toString(); licenseState.username = json.getProperty ("user", {}).getProperty ("username", {}).toString(); return (licenseState.authToken.isNotEmpty() && licenseState.username.isNotEmpty()); } String userEmail, userPassword; }; struct UserLicenseQuery : public AccountEnquiryBase { UserLicenseQuery (const String& authToken) : userAuthToken (authToken) { } bool isPOSTLikeRequest() const override { return false; } String getEndpointURLSuffix() const override { return "/user/licences/projucer"; } int getSuccessCode() const override { return 200; } StringPairArray getParameterNamesAndValues() const override { return {}; } String getExtraHeaders() const override { return "x-access-token: " + userAuthToken; } String errorCodeToString (int errorCode) const override { switch (errorCode) { case 401: return "User not found or could not be verified."; default: return "User licenses info fetch failed (unknown error)."; } } bool parseServerResponse (const String& serverResponse, LicenseState& licenseState) override { auto json = JSON::parse (serverResponse); if (auto* licensesJson = json.getArray()) { std::vector licenses; for (auto& license : *licensesJson) { auto version = license.getProperty ("product_version", {}).toString().trim(); auto type = license.getProperty ("licence_type", {}).toString(); auto status = license.getProperty ("status", {}).toString(); if (status == "active" && type.isNotEmpty() && version.isNotEmpty()) licenses.push_back ({ version.getIntValue(), LicenseHelpers::licenseTypeForString (type) }); } if (! licenses.empty()) { auto bestLicense = LicenseHelpers::findBestLicense (std::move (licenses)); licenseState.version = bestLicense.first; licenseState.type = bestLicense.second; } return true; } return false; } String userAuthToken; }; //============================================================================== static String postDataStringAsJSON (const StringPairArray& parameters) { DynamicObject::Ptr d (new DynamicObject()); for (auto& key : parameters.getAllKeys()) d->setProperty (key, parameters[key]); return JSON::toString (var (d.get())); } static ErrorMessageAndType runTask (std::unique_ptr accountEnquiryTask, LicenseState& state) { const ErrorMessageAndType cancelledError ("Cancelled.", ErrorType::cancelled); const String endpointURL ("https://api.juce.com/api/v1"); URL url (endpointURL + accountEnquiryTask->getEndpointURLSuffix()); auto isPOST = accountEnquiryTask->isPOSTLikeRequest(); if (isPOST) url = url.withPOSTData (postDataStringAsJSON (accountEnquiryTask->getParameterNamesAndValues())); if (ThreadPoolJob::getCurrentThreadPoolJob()->shouldExit()) return cancelledError; int statusCode = 0; auto urlStream = url.createInputStream (URL::InputStreamOptions (isPOST ? URL::ParameterHandling::inPostData : URL::ParameterHandling::inAddress) .withExtraHeaders (accountEnquiryTask->getExtraHeaders()) .withConnectionTimeoutMs (5000) .withStatusCode (&statusCode)); if (urlStream == nullptr) return { "Failed to connect to the web server.", ErrorType::connectionError }; if (statusCode != accountEnquiryTask->getSuccessCode()) return { accountEnquiryTask->errorCodeToString (statusCode), ErrorType::webResponseError }; if (ThreadPoolJob::getCurrentThreadPoolJob()->shouldExit()) return cancelledError; String response; for (;;) { char buffer [8192] = ""; auto num = urlStream->read (buffer, sizeof (buffer)); if (ThreadPoolJob::getCurrentThreadPoolJob()->shouldExit()) return cancelledError; if (num <= 0) break; response += buffer; } if (ThreadPoolJob::getCurrentThreadPoolJob()->shouldExit()) return cancelledError; if (! accountEnquiryTask->parseServerResponse (response, state)) return { "Failed to parse server response.", ErrorType::webResponseError }; return {}; } //============================================================================== ThreadPool jobPool { 1 }; //============================================================================== JUCE_DECLARE_WEAK_REFERENCEABLE (LicenseQueryThread) JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LicenseQueryThread) };