372 lines
14 KiB
C
372 lines
14 KiB
C
|
/*
|
||
|
==============================================================================
|
||
|
|
||
|
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<int, LicenseState::Type>;
|
||
|
|
||
|
inline LicenseVersionAndType findBestLicense (std::vector<LicenseVersionAndType>&& 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<String, ErrorType>;
|
||
|
using LicenseQueryCallback = std::function<void (ErrorMessageAndType, LicenseState)>;
|
||
|
|
||
|
//==============================================================================
|
||
|
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<UserLicenseQuery> (state.authToken), updatedState);
|
||
|
|
||
|
WeakReference<LicenseQueryThread> 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<UserLogin> (email, password), state);
|
||
|
|
||
|
if (result == ErrorMessageAndType())
|
||
|
result = runTask (std::make_unique<UserLicenseQuery> (state.authToken), state);
|
||
|
|
||
|
if (result != ErrorMessageAndType())
|
||
|
state = {};
|
||
|
|
||
|
WeakReference<LicenseQueryThread> 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<LicenseHelpers::LicenseVersionAndType> 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<AccountEnquiryBase> 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)
|
||
|
};
|